Hibernate 映射枚举(Enum) 类型的属性

在数据库中我们一般用整数或字符串来表示枚举值(有些数据库(如 MySQL)本身带有枚举类型), 而在使用 Hibernate 时实体对象中也用 Integer 或 String 来表示枚举就不那么友好了。试想来我们这样定义实体对象的两个属性
@Entity
public class User {
  ....   public Integer type;  //0: Individual 类型,1: Company 类型
  public String gender;  //可取值 Male 和 Female
}
这样的定义很不严谨,type 和 gender 理论上可取任何值,这会造成表中数据的混乱。其实 Hibernate 在 Java 实体对象中是可以直接用枚举类型与数据库中的整数或字符串映射,需用到 @Enumerated 注解,用法如下:
 1import javax.persistence.*;
 2
 3@Entity
 4public class User {
 5  enmu Type { Individual, Company }
 6  enum Gender { Male, Female }
 7
 8  @Id @GeneratedValue
 9  @Column(name = "id")
10  public Long id;
11
12  @Enumerated(EnumType.ORDINAL)
13  @Column(name = "type")
14  public Type type;
15
16  @Enumerated(EnumType.STRING)
17  @Column(name = "gender")
18  public Gender gender;
19
20  public User() {}
21
22  public User(Type type, Gender gender) {
23    this.type = type;
24    this.gender = gender;
25  }
26}

type 和 gender 分别是两个枚举类型, @Enumerated 可选 value 属性有 ORDINAL 和 STRING. 稍后会说明这两种类型的不同,先看下相对应的 user 表的 Schema 定义
1create table user (
2  id int auto_increment,
3  type int,
4  gender varchar(8),
5  primary key(id)
6)

上面是 MySQL 创建表的语法,不同数据库需稍加改动。

现在我们可以进行 User 对象的持久化与加载操作了,假如有一个 UserRepository
1import org.springframework.data.jpa.repository.JpaRepository;
2import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
3
4public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor {
5}

例如下面那样的操作
1@Injected
2private UserRepository userRepository;
3
4userRepository.save(new User(Type.Individual, Gender.Male);
5userRepository.save(new User(null, Gender.Female);
6
7User user1 = userRepository.findOne(1); 
8User user2 = userRepository.findOne(2);

我们会看到
  1. type: Individual 时保存为 0, Company 时保存为 1
  2. gender: Male 时保存为字符串 'Male', Female 时保存为字符串 'Female'
  3. Java 属性为 null 值时,数据库中也是 NULL 值
  4. 当数据库中字段 type 为 NULL 时,得到的 User 对象的 type 也是 null 值; 对于 gender 字段也是一样的
  5. 当数据库的 type 或  gender 字段保存了非预期的值时,生成 User 对象时会产生异常, 如 type 是  -1, 2 等,gender 是 'Other'

枚举字段在 HQL 中如何查询

把把枚举字段以在数据库中的实际类型对象,如
1from User u where u.type=1 and u.gender='Female'
2select u.type, u.gender from User u; //返回的两个字段分别是 Type  Gender 的枚举类型

现在回过头来看 @Enumerated 的两个取值,全部实现都在这里 org.hibernate.type.EnumType

  1. EnumType.ORDINAL:要求被映射的数据库字段类型为整形
    Java 中的枚举项都有一个内部的 ordinal 值,从 0 开始编排,不能像 C/C++ 那样定制,所以如果选用这个类型时。持久化时调用 enum.oridinal() 得到整数值,加载数据时按照整数索引找到相应的枚举值。
    实现为 OrdinaEnumValueMapper

    持久化时从枚举值转换为整形时调用 setValue() 方法
    1 public void setValue(PreparedStatement st, Enum value, int index) throws SQLException {
    2   final Object jdbcValue = value == null ? null : extractJdbcValue( value );
    3   if ( jdbcValue == null ) {
    4     st.setNull( index, getSqlType() );
    5     return;
    6   }
    7   st.setObject( index, jdbcValue, EnumType.this.sqlType );
    8 }

    它所调用的 extractJdbcValue(value) 是:
    1protected Object extractJdbcValue(Enum value) {
    2  return value.ordinal();
    3}

    由上可知 Java 属性值为 null, 数据库字段也会是 NULL, 非 null 时直接调用枚举的 ordinal() 获得整形值。

    从数据库中加载数据生成 Java 的枚举值时调用 getValue() 方法
    1 public Enum getValue(ResultSet rs, String[] names) throws SQLException {
    2   final int ordinal = rs.getInt( names[0] );
    3   if ( rs.wasNull() ) {
    4     return null;
    5   }
    6   return fromOrdinal( ordinal );
    7 }

    它调用的 fromOrdinal(ordinal) 方法如下:
    1 private Enum fromOrdinal(int ordinal) {
    2   final Enum[] enumsByOrdinal = enumsByOrdinal();
    3   if ( ordinal < 0 || ordinal >= enumsByOrdinal.length ) {
    4     throw new IllegalArgumentException(
    5       String.format("Unknown ordinal value [%s] for enum class [%s]", ordinal, enumClass.getName()));
    6   }
    7   return enumsByOrdinal[ordinal];
    8 }

    当数据库表中存的值为 NULL, 得到的枚举值也是 null, 当为越界的整形值是报出异常,这是合理的,既然定义该字段映射为 Java 枚举值,那么就不能乱填值。
  2. EnumType.STRING:要求被映射的数据库字段类型为字符串
    就是枚举的字面名 name,持久化时调用 enum.name() 获得这个名称,加载数据时调用 Enum.valueOf() 方法来获得枚举值
    实现类为 NamedEnumValueMapper.

    持久化时从枚举值转换为字符是调用与上同一个 setValue() 方法,只是 extractJdbcValue(value) 不一样
    1protected Object extractJdbcValue(Enum value) {
    2  return value.name();
    3}

    类似的,如果枚举值为  null, 保存到数据库后也是 NULL, 否则调用枚举的 name() 方法获得字符串存入数据库

    从数据库中加载数据生成 Java 的枚举值是调用 getValue() 方法
    1 public Enum getValue(ResultSet rs, String[] names) throws SQLException {
    2   final String value = rs.getString( names[0] );
    3   if ( rs.wasNull() ) {
    4     return null;
    5   }
    6   return fromName( value );
    7 }

    它调用 fromName(value) 方法
     1 private Enum fromName(String name) {
     2   try {
     3     if (name == null) {
     4       return null;
     5     }
     6     return Enum.valueOf( enumClass, name.trim() );
     7   }catch ( IllegalArgumentException iae ) {
     8     throw new IllegalArgumentException(
     9      String.format("Unknown name value [%s] for enum class [%s]",name, enumClass.getName()));
    10   }
    11 }

    数据库中是 NULL 值,没问题,得到的枚举值也是 null, 但非预期的字符串就要报出异常,正常的话调用 Enum.valueOf() 方法获得相应的枚举值。

 Hibernate 在使用枚举能安全的进行 null 值映身从分别调用 getValue() 和 setValue() 的入口方法就知道,入口方法各自叫做 nullSafeGet() 和 nullSafeSet()

 1 public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws SQLException {
 2   if ( enumValueMapper == null ) {
 3     throw new AssertionFailure( "EnumType (" + enumClass.getName() + ") not properly, fully configured" );
 4   }
 5   return enumValueMapper.getValue( rs, names );
 6 }
 7
 8 public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
 9   if ( enumValueMapper == null ) {
10     throw new AssertionFailure( "EnumType (" + enumClass.getName() + ") not properly, fully configured" );
11   }
12   enumValueMapper.setValue( st, (Enum) value, index );
13 }

以上源代码来自 Hibernate 官方代码库,但进行了重排并移除了日志相关的代码。通过阅读 Hibernate EnmuType 源代码我们可以非常的清楚它是如何工作的,以及什么情况下会出现何种状况,也就是千万不去乱改数据库中的值。

使用枚举类型进行映射有一个弊端就是,将来有一天修改了枚举类型的定义会造成数据库中的数据无法被加载,所以如果对改动的枚举定义(如顺序调整了-- ORDINAL; 或名称改了; 或增减了选项) 时一定要同步 update 数据库中的记录,这对于产品数据库也是个麻烦事。这一点上还是需要谨慎的思考是否真要用 Java 的枚举值来映射

而用 Integer 或 String 来作为 Java 的属性时则不会造成数据加载的异常,顶多是数据混乱,或有些值无法理解而已。

相关链接:

  1. Hibernate Enum Type Mapping Example
永久链接 https://yanbin.blog/hibernate-map-enum-type-field/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。