关于java:在JPA中使用固定值映射枚举?

Map enum in JPA with fixed values?

我正在寻找使用JPA映射枚举的不同方法。我特别想设置每个枚举条目的整数值,并只保存整数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name ="AUTHORITY_")
public class Authority implements Serializable {

  public enum Right {
      READ(100), WRITE(200), EDITOR (300);

      private int value;

      Right(int value) { this.value = value; }

      public int getValue() { return value; }
  };

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name ="AUTHORITY_ID")
  private Long id;

  // the enum to map :
  private Right right;
}

一个简单的解决方案是使用EnumType注释和EnumType.ORDINAL:

1
2
3
@Column(name ="RIGHT")
@Enumerated(EnumType.ORDINAL)
private Right right;

但在这种情况下,JPA映射枚举索引(0,1,2)而不是我想要的值(100,200,300)。

我找到的两个解决方案似乎并不简单......

第一解决方案

这里提出的解决方案使用@PrePersist和@PostLoad将枚举转换为其他字段并将枚举字段标记为瞬态:

1
2
3
4
5
6
7
8
9
10
11
12
@Basic
private int intValueForAnEnum;

@PrePersist
void populateDBFields() {
  intValueForAnEnum = right.getValue();
}

@PostLoad
void populateTransientFields() {
  right = Right.valueOf(intValueForAnEnum);
}

二解决方案

这里提出的第二个解决方案提出了一个通用的转换对象,但仍然看起来很重,并且面向hibernate(Java EE中似乎不存在@Type):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Type(
    type ="org.appfuse.tutorial.commons.hibernate.GenericEnumUserType",
    parameters = {
            @Parameter(
                name  ="enumClass",                      
                value ="Authority$Right"),
            @Parameter(
                name  ="identifierMethod",
                value ="toInt"),
            @Parameter(
                name  ="valueOfMethod",
                value ="fromInt")
            }
)

还有其他解决方案吗?

我有几点想法,但我不知道它们是否存在于JPA中:

  • 在加载和保存Authority对象时,使用权限类的权限成员的setter和getter方法
  • 一个相同的想法是告诉JPA将enum转换为int和int转换为enum的Right enum的方法是什么
  • 因为我使用Spring,有没有办法告诉JPA使用特定的转换器(RightEditor)?


对于JPA 2.1之前的版本,JPA只提供了两种处理枚举的方法,即nameordinal。标准JPA不支持自定义类型。所以:

  • 如果要进行自定义类型转换,则必须使用提供程序扩展(使用Hibernate UserType,EclipseLink Converter等)。 (第二种解决方案)。 ?或?
  • 你必须使用@PrePersist和@PostLoad技巧(第一个解决方案)。 ?或?
  • 注释getter和setter获取并返回int值?或?
  • 在实体级别使用整数属性,并在getter和setter中执行转换。

我将说明最新的选项(这是一个基本的实现,根据需要进行调整):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Entity
@Table(name ="AUTHORITY_")
public class Authority implements Serializable {

    public enum Right {
        READ(100), WRITE(200), EDITOR (300);

        private int value;

        Right(int value) { this.value = value; }    

        public int getValue() { return value; }

        public static Right parse(int id) {
            Right right = null; // Default
            for (Right item : Right.values()) {
                if (item.getValue()==id) {
                    right = item;
                    break;
                }
            }
            return right;
        }

    };

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name ="AUTHORITY_ID")
    private Long id;

    @Column(name ="RIGHT_ID")
    private int rightId;

    public Right getRight () {
        return Right.parse(this.rightId);
    }

    public void setRight(Right right) {
        this.rightId = right.getValue();
    }

}


现在可以使用JPA 2.1:

1
2
3
@Column(name ="RIGHT")
@Enumerated(EnumType.STRING)
private Right right;

更多详情:


最好的方法是将唯一ID映射到每个枚举类型,从而避免ORDINAL和STRING的陷阱。请参阅这篇文章,其中概述了映射枚举的5种方法。

取自上面的链接:

1和2。使用@Enumerated

目前有两种方法可以使用@Enumerated注释在JPA实体中映射枚举。不幸的是,EnumType.STRING和EnumType.ORDINAL都有其局限性。

如果使用EnumType.String,则重命名其中一个枚举类型将导致枚举值与数据库中保存的值不同步。如果使用EnumType.ORDINAL,则在枚举中删除或重新排序类型将导致数据库中保存的值映射到错误的枚举类型。

这两个选项都很脆弱。如果在不执行数据库迁移的情况下修改枚举,则可能会破坏数据的完整性。

3.生命周期回调

一种可能的解决方案是使用JPA生命周期回调注释,@ PrePersist和@PostLoad。这感觉非常难看,因为您现在在实体中有两个变量。一个映射存储在数据库中的值,另一个映射存储在实际的枚举中。

4.将唯一ID映射到每个枚举类型

首选解决方案是将枚举映射到枚举中定义的固定值或ID。映射到预定义的固定值使您的代码更加健壮。对枚举类型的顺序或名称的重构的任何修改都不会产生任何不利影响。

5.使用Java EE7 @Convert

如果您使用的是JPA 2.1,则可以选择使用新的@Convert注释。这需要创建一个使用@Converter注释的转换器类,您可以在其中定义为每个枚举类型保存到数据库中的值。在您的实体中,您将使用@Convert注释您的枚举。

我的偏好:( 4号)

我更喜欢在enum中定义我的ID而不是使用转换器的原因是良好的封装。只有枚举类型应该知道它的ID,并且只有实体应该知道它如何将枚举映射到数据库。

有关代码示例,请参阅原始帖子。


从JPA 2.1开始,您可以使用AttributeConverter。

像这样创建一个枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum NodeType {

    ROOT("root-node"),
    BRANCH("branch-node"),
    LEAF("leaf-node");

    private final String code;

    private NodeType(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

并创建一个像这样的转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class NodeTypeConverter implements AttributeConverter<NodeType, String> {

    @Override
    public String convertToDatabaseColumn(NodeType nodeType) {
        return nodeType.getCode();
    }

    @Override
    public NodeType convertToEntityAttribute(String dbData) {
        for (NodeType nodeType : NodeType.values()) {
            if (nodeType.getCode().equals(dbData)) {
                return nodeType;
            }
        }

        throw new IllegalArgumentException("Unknown database value:" + dbData);
    }
}

在您需要的实体上:

1
@Column(name ="node_type_code")

你运气@Converter(autoApply = true)可能因容器而异,但经过测试可以在Wildfly 8.1.0上运行。如果它不起作用,您可以在实体类列上添加@Convert(converter = NodeTypeConverter.class)


问题是,我认为,JPA从未接受过这样的想法,即我们可能已经有一个复杂的预先存在的Schema。

我认为这有两个主要的缺点,特别是Enum:

  • 使用name()和ordinal()的限制。为什么不用@Id标记一个getter,就像我们使用@Entity一样?
  • Enum通常在数据库中表示允许与各种元数据相关联,包括正确的名称,描述性名称,可能具有本地化的东西等。我们需要易于使用Enum并结合实体的灵活性。
  • 帮助我的事业并投票JPA_SPEC-47

    这不比使用@Converter解决问题更优雅吗?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // Note: this code won't work!!
    // it is just a sample of how I *would* want it to work!
    @Enumerated
    public enum Language {
      ENGLISH_US("en-US"),
      ENGLISH_BRITISH("en-BR"),
      FRENCH("fr"),
      FRENCH_CANADIAN("fr-CA");
      @ID
      private String code;
      @Column(name="DESCRIPTION")
      private String description;

      Language(String code) {
        this.code = code;
      }

      public String getCode() {
        return code;
      }

      public String getDescription() {
        return description;
      }
    }

    可能关闭Pascal的相关代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    @Entity
    @Table(name ="AUTHORITY_")
    public class Authority implements Serializable {

        public enum Right {
            READ(100), WRITE(200), EDITOR(300);

            private Integer value;

            private Right(Integer value) {
                this.value = value;
            }

            // Reverse lookup Right for getting a Key from it's values
            private static final Map<Integer, Right> lookup = new HashMap<Integer, Right>();
            static {
                for (Right item : Right.values())
                    lookup.put(item.getValue(), item);
            }

            public Integer getValue() {
                return value;
            }

            public static Right getKey(Integer value) {
                return lookup.get(value);
            }

        };

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name ="AUTHORITY_ID")
        private Long id;

        @Column(name ="RIGHT_ID")
        private Integer rightId;

        public Right getRight() {
            return Right.getKey(this.rightId);
        }

        public void setRight(Right right) {
            this.rightId = right.getValue();
        }

    }


    我会做以下事情:

    在枚举中声明枚举,在它自己的文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public enum RightEnum {
          READ(100), WRITE(200), EDITOR (300);

          private int value;

          private RightEnum (int value) { this.value = value; }


          @Override
          public static Etapa valueOf(Integer value){
               for( RightEnum r : RightEnum .values() ){
                  if ( r.getValue().equals(value))
                     return r;
               }
               return null;//or throw exception
         }

          public int getValue() { return value; }


    }

    声明一个名为Right的新JPA实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Entity
    public class Right{
        @Id
        private Integer id;
        //FIElDS

        // constructor
        public Right(RightEnum rightEnum){
              this.id = rightEnum.getValue();
        }

        public Right getInstance(RightEnum rightEnum){
              return new Right(rightEnum);
        }


    }

    您还需要一个用于接收此值的转换器(仅限JPA 2.1,这里我将不讨论使用转换器直接保留这些枚举的问题,因此它将只是单向路径)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import mypackage.RightEnum;
    import javax.persistence.AttributeConverter;
    import javax.persistence.Converter;

    /**
     *
     *
     */

    @Converter(autoApply = true)
    public class RightEnumConverter implements AttributeConverter<RightEnum, Integer>{

        @Override //this method shoudn′t be used, but I implemented anyway, just in case
        public Integer convertToDatabaseColumn(RightEnum attribute) {
            return attribute.getValue();
        }

        @Override
        public RightEnum convertToEntityAttribute(Integer dbData) {
            return RightEnum.valueOf(dbData);
        }

    }

    管理局实体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Entity
    @Table(name ="AUTHORITY_")
    public class Authority implements Serializable {


      @Id
      @GeneratedValue(strategy = GenerationType.AUTO)
      @Column(name ="AUTHORITY_ID")
      private Long id;

      // the **Entity** to map :
      private Right right;

      // the **Enum** to map (not to be persisted or updated) :
      @Column(name="COLUMN1", insertable = false, updatable = false)
      @Convert(converter = RightEnumConverter.class)
      private RightEnum rightEnum;

    }

    通过这种方式,您无法直接设置枚举字段。但是,您可以使用权限设置权限字段

    1
    autorithy.setRight( Right.getInstance( RightEnum.READ ) );//for example

    如果你需要比较,你可以使用:

    1
    authority.getRight().equals( RightEnum.READ ); //for example

    我觉得这很酷。这不完全正确,因为转换器不打算与枚举一起使用。实际上,文档说从不将它用于此目的,您应该使用@Enumerated注释。问题是只有两种枚举类型:ORDINAL或STRING,但ORDINAL很棘手且不安全。

    但是,如果它不能满足你,你可以做一些更简单(或更简单)的事情。

    让我们来看看。

    RightEnum:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public enum RightEnum {
          READ(100), WRITE(200), EDITOR (300);

          private int value;

          private RightEnum (int value) {
                try {
                      this.value= value;
                      final Field field = this.getClass().getSuperclass().getDeclaredField("ordinal");
                      field.setAccessible(true);
                      field.set(this, value);
                 } catch (Exception e) {//or use more multicatch if you use JDK 1.7+
                      throw new RuntimeException(e);
                }
          }


          @Override
          public static Etapa valueOf(Integer value){
               for( RightEnum r : RightEnum .values() ){
                  if ( r.getValue().equals(value))
                     return r;
               }
               return null;//or throw exception
         }

          public int getValue() { return value; }


    }

    和管理局实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Entity
    @Table(name ="AUTHORITY_")
    public class Authority implements Serializable {


      @Id
      @GeneratedValue(strategy = GenerationType.AUTO)
      @Column(name ="AUTHORITY_ID")
      private Long id;


      // the **Enum** to map (to be persisted or updated) :
      @Column(name="COLUMN1")
      @Enumerated(EnumType.ORDINAL)
      private RightEnum rightEnum;

    }

    在第二个想法中,它不是一个完美的情况,因为我们破解了序数属性,但它是一个小得多的编码。

    我认为JPA规范应该包含EnumType.ID,其中枚举值字段应该使用某种@EnumId注释进行注释。