用固定值在JPA中映射枚举?


192

我正在寻找使用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一起使用:

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

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

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

第一个解决方案

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

@Basic
private int intValueForAnEnum;

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

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

第二解决方案

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

@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对象时,使用Authority类的正确成员的setter和getter方法
  • 一个等效的想法是告诉JPA,将枚举转换为int并将int转换为enum的Right枚举的方法是什么
  • 因为我使用的是Spring,有什么方法可以告诉JPA使用特定的转换器(RightEditor)?

7
使用ORDINAL很奇怪,有时有人会更改枚举中项目的位置,而数据库将成为灾难
Natasha KP

2
使用Name可能会有所不同-有人可能会更改枚举名称,并且它们又与数据库不同步...
topchef 2011年

2
我同意@NatashaKP。不要使用序数。要更改名称,没有这样的事情。实际上,您是在删除旧的枚举并添加一个具有新名称的新枚举,因此,是的,所有存储的数据都不同步(语义,也许是:P)。
Svend Hansen 2012年

是的,我知道5种解决方案。请参阅下面的答案,这里有详细的答案。
克里斯·里奇

Answers:


168

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

  • 如果要进行自定义类型转换,则必须使用提供程序扩展(使用Hibernate UserType,EclipseLinkConverter等)。(第二个解决方案)。〜或〜
  • 您必须使用@PrePersist和@PostLoad技巧(第一个解决方案)。〜或〜
  • 注释getter和setter获取并返回int值〜或〜
  • 在实体级别使用整数属性,并在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();
    }

}

50
因此,可悲的JPA没有对此的本地支持
Jaime Hablutzel

20
@jaime同意!认为开发人员可能希望将枚举作为其字段/属性之一的值而不是其int值或名称来保存是不是很疯狂?两者都极其“脆弱”且重构不友好。并且使用该名称还假定您在Java和数据库中都使用相同的命名约定。以性别为例。在数据库中可以将其简单定义为“ M”或“ F”,但这不应阻止我在Java中使用Gender.MALE和Gender.FEMALE,而不是Gender.M或Gender.F。
spaaarky21 2011年

2
我猜可能是因为名称和序数都保证是唯一的,而其他任何值或字段都不是唯一的。的确,顺序可以更改(不要使用序数),名称也可以更改(不要更改枚举名称:P),但是其他任何值也可以...我不确定我看到了什么增加存储不同价值的能力带来的巨大价值。
Svend Hansen 2012年

2
其实,我确实看到了这个价值……如果我仍然可以编辑它,我会删除上面我的评论的那部分:P:D
Svend Hansen 2012年

13
JPA2.1将本身具有转换器支持。请参阅somethoughtsonjava.blogspot.fi/2013/10/…–
drodil

69

现在可以使用JPA 2.1:

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

更多详细信息:


2
现在有什么可能?当然可以使用@Converter,但是enum开箱即用时应该更加优雅!
YoYo

4
“答案”引用了2条有关使用AttributeConverterBUT的链接,这些链接引用了一些不执行任何操作且不回答OP的代码。

@ DN1随时进行改进
Tvaroh

1
这是您的“答案”,因此您应该“改善”。已经有一个答案AttributeConverter

1
添加后完美工作:@Convert(converter = Converter.class) @Enumerated(EnumType.STRING)
Kaushal18年

22

从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()”
Curtis Yallop

17

最好的方法是将唯一的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)

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

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


1
javax.persistence.Converter很简单,使用@Converter(autoApply = true),可以使域类中没有@Convert注释
Rostislav

9

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

我认为由此引起的两个主要缺点是Enum特有的:

  1. 使用name()和ordinal()的限制。为什么不只用@Id标记吸气剂呢?
  2. 枚举通常在数据库中具有表示形式,以允许与各种元数据相关联,包括专有名称,描述性名称,可能还包含本地化名称等。我们需要易于使用的枚举以及实体的灵活性。

帮助我的事业并投票 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;
  }
}

4

可能关闭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();
    }

}

2

我会做以下事情:

在自己的文件中分别声明枚举:

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;

}

通过这种方式,您不能直接设置为枚举字段。但是,您可以使用以下命令在“授权”中设置“权限”字段

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

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

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

我认为这很酷。这不是完全正确,因为转换器不适合与enum一起使用。实际上,文档说不要将其用于此目的,而应使用@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注释进行注释。


2

我自己的解决此类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;
    }

}

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

第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-将步骤2的转换器扩展Right为步骤3 的枚举:

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在您的枚举上;
  • 扩展 EnumDbValueConverter仅用3行代码(您也可以在您的实体内执行此操作,以避免创建单独的类);
  • @Convertfrom javax.persistence包注释enum属性。

希望这可以帮助。


1
public enum Gender{ 
    MALE, FEMALE 
}



@Entity
@Table( name="clienti" )
public class Cliente implements Serializable {
...

// **1 case** - If database column type is number (integer) 
// (some time for better search performance)  -> we should use 
// EnumType.ORDINAL as @O.Badr noticed. e.g. inserted number will
// index of constant starting from 0... in our example for MALE - 0, FEMALE - 1.
// **Possible issue (advice)**: you have to add the new values at the end of
// your enum, in order to keep the ordinal correct for future values.

@Enumerated(EnumType.ORDINAL)
    private Gender gender;


// **2 case** - If database column type is character (varchar) 
// and you want to save it as String constant then ->

@Enumerated(EnumType.STRING)
    private Gender gender;

...
}

// in all case on code level you will interact with defined 
// type of Enum constant but in Database level

第一种情况(EnumType.ORDINAL

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood      0   
  2  Geoff Dalgas     0   
  3 Jarrod Jesica     1   
  4  Joel Lucy        1   
╚════╩══════════════╩════════╝

第二种情况(EnumType.STRING

╔════╦══════════════╦════════╗
 ID     NAME       GENDER 
╠════╬══════════════╬════════╣
  1  Jeff Atwood    MALE  
  2  Geoff Dalgas   MALE  
  3 Jarrod Jesica  FEMALE 
  4  Joel Lucy     FEMALE 
╚════╩══════════════╩════════╝
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.