使用固定值映射 JPA 中的枚举?

Posted

技术标签:

【中文标题】使用固定值映射 JPA 中的枚举?【英文标题】:Map enum in JPA with fixed values? 【发布时间】:2010-05-01 22:14:59 【问题描述】:

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

@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.ORDINAL 的 Enumerated 注解:

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

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

我发现的两个解决方案似乎并不简单......

第一个解决方案

解决方案proposed here 使用@PrePersist 和@PostLoad 将枚举转换为其他字段并将枚举字段标记为瞬态:

@Basic
private int intValueForAnEnum;

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


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

第二种解决方案

第二个解决方案proposed here 提出了一个通用的转换对象,但仍然显得笨重且面向hibernate(@Type 在Java EE 中似乎不存在):

@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 中:

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

【问题讨论】:

使用ORDINAL很奇怪,有时有人会改变枚举中项目的位置,数据库会变成灾难 不会同样适用于使用名称 - 有人可能会更改枚举名称并且他们再次与数据库不同步... 我同意@NatashaKP。不要使用序数。换个名字,就没有这回事了。您实际上是在删除旧枚举并添加一个具有新名称的新枚举,所以是的,任何存储的数据都会不同步(语义,也许是 :P )。 是的,我知道有 5 个解决方案。请参阅下面的答案,我有一个详细的答案。 【参考方案1】:

对于早于 JPA 2.1 的版本,JPA 仅提供两种处理枚举的方法,通过它们的name 或通过它们的ordinal。并且标准 JPA 不支持自定义类型。所以:

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

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

@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 没有对此的原生支持 @jaime 同意!认为开发人员可能希望将枚举作为其字段/属性之一的值而不是其 int 值或名称来持久化是不是很疯狂?这两者都非常“脆弱”并且对重构不友好。并且使用该名称还假定您在 Java 和数据库中使用相同的命名约定。以性别为例。这可能在数据库中被简单地定义为“M”或“F”,但这不应该阻止我在 Java 中使用 Gender.MALE 和 Gender.FEMALE,而不是 Gender.M 或 Gender.F。 我想一个原因可能是名称和序数都保证是唯一的,而任何其他值或字段都不是。确实,顺序可以改变(不要使用序数)并且名称也可以改变(不要改变枚举名称:P),但是任何其他值也可以改变......我不确定我看到增加存储不同值的能力带来的巨大价值。 实际上,我确实看到了价值......如果我仍然可以编辑它,我会删除我上面评论的那部分:P:D JPA2.1 将原生支持转换器。请看somethoughtsonjava.blogspot.fi/2013/10/…【参考方案2】:

现在 JPA 2.1 可以做到这一点:

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

更多细节:

https://dzone.com/articles/mapping-enums-done-right http://www.thoughts-on-java.org/jpa-21-how-to-implement-type-converter/

【讨论】:

现在有什么可能?当然我们可以使用@Converter,但enum 应该开箱即用地更优雅地处理! "Answer" 引用了 2 个链接,这些链接谈到使用 AttributeConverter 但引用了一些不执行任何操作且不回答 OP 的代码。 @DN1 随时改进 这是你的“答案”,所以你应该“改进它”。 AttributeConverter已经有答案了 添加后完美运行:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)【参考方案3】:

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

像这样创建一个枚举类:

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;
    

然后像这样创建一个转换器:

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);
    

在您只需要的实体上:

@Column(name = "node_type_code")

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

【讨论】:

“values()”应该是“NodeType.values()” 你能把@Converter(autoApply = true)@Convert(converter = NodeTypeConverter.class)放在一起,还是必须是@Converter@Convert,两者不能一起使用?【参考方案4】:

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

取自以上链接:

1&2。使用@Enumerated

目前有 2 种方法可以使用 @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 位)

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

代码示例见原文post。

【讨论】:

javax.persistence.Converter 很简单,使用 @Converter(autoApply = true) 它允许域类不使用 @Convert 注释【参考方案5】:

我认为,问题在于,JPA 从来没有考虑到我们可以拥有一个复杂的预先存在的模式。

我认为这导致了 Enum 特有的两个主要缺点:

    使用 name() 和 ordinal() 的限制。为什么不像我们使用@Entity 那样用@Id 标记getter? 枚举通常在数据库中有表示,以允许与各种元数据关联,包括专有名称、描述性名称,可能还有本地化等。我们需要枚举的易用性和实体的灵活性.

帮助我的事业并为JPA_SPEC-47投票

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

// 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;
  

【讨论】:

【参考方案6】:

我自己解决这种 Enum JPA 映射的方法如下。

第 1 步 - 编写以下接口,我们将用于我们想要映射到 db 列的所有枚举:

public interface IDbValue<T extends java.io.Serializable> 

    T getDbVal();


第 2 步 - 实现自定义通用 JPA 转换器,如下所示:

import javax.persistence.AttributeConverter;

public abstract class EnumDbValueConverter<T extends java.io.Serializable, E extends Enum<E> & IDbValue<T>>
        implements AttributeConverter<E, T> 

    private final Class<E> clazz;

    public EnumDbValueConverter(Class<E> clazz)
        this.clazz = clazz;
    

    @Override
    public T convertToDatabaseColumn(E attribute) 
        if (attribute == null) 
            return null;
        
        return attribute.getDbVal();
    

    @Override
    public E convertToEntityAttribute(T dbData) 
        if (dbData == null) 
            return null;
        
        for (E e : clazz.getEnumConstants()) 
            if (dbData.equals(e.getDbVal())) 
                return e;
            
        
        // handle error as you prefer, for example, using slf4j:
        // log.error("Unable to convert  to enum .", dbData, clazz.getCanonicalName());
        return null;
    


此类将通过使用枚举E 上的getDbVal() 将枚举值E 转换为T 类型的数据库字段(例如String),反之亦然。

第 3 步 - 让原始枚举实现我们在第 1 步中定义的接口:

public enum Right implements IDbValue<Integer> 
    READ(100), WRITE(200), EDITOR (300);

    private final Integer dbVal;

    private Right(Integer dbVal) 
        this.dbVal = dbVal;
    

    @Override
    public Integer getDbVal() 
        return dbVal;
    

第 4 步 - 为第 3 步的 Right 枚举扩展第 2 步的转换器:

public class RightConverter extends EnumDbValueConverter<Integer, Right> 
    public RightConverter() 
        super(Right.class);
    

第 5 步 - 最后一步是对实体中的字段进行如下注释:

@Column(name = "RIGHT")
@Convert(converter = RightConverter.class)
private Right right;

结论

恕我直言,如果您有许多要映射的枚举并且您想使用枚举本身的特定字段作为映射值,那么这是最简洁和最优雅的解决方案。

对于您项目中所有其他需要类似映射逻辑的枚举,您只需重复步骤 3 到 5,即:

在您的枚举上实现接口IDbValue; 仅用 3 行代码扩展 EnumDbValueConverter(您也可以在实体中执行此操作以避免创建单独的类); 使用来自javax.persistence 包的@Convert 注释枚举属性。

希望这会有所帮助。

【讨论】:

【参考方案7】:

可能关闭Pascal的相关代码

@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();
    


【讨论】:

【参考方案8】:

我会做以下事情:

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

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 实体

@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,并且存在一个问题,我不会在这里讨论这些枚举要直接使用转换器进行持久化,因此它只是单向路)

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);
    


权威实体:

@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;


通过这种方式,您不能直接设置枚举字段。但是,您可以使用

在权限中设置 Right 字段
autorithy.setRight( Right.getInstance( RightEnum.READ ) );//for example

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

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

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


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

让我们看看。

RightEnum:

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; 



和管理局实体

@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 注释进行注释。

【讨论】:

以上是关于使用固定值映射 JPA 中的枚举?的主要内容,如果未能解决你的问题,请参考以下文章

在运行时从 db 枚举为 jpa 枚举生成值

如何使用 JPA 将 MySQL 枚举映射到 Java 枚举? [复制]

如何使用 JPA 和 Hibernate 映射 PostgreSQL 枚举

将一个值映射到JPA中的多个列

使用 Jackson Annotation 将 json 数据映射到 Spring Data JPA 中的预期值

cuPrintf 啥都不做(程序使用固定+映射内存,CUBLAS 也是)