如何在JPA中使用枚举类型

37

我有一个现有的电影租赁系统数据库。每个电影都有一个评级属性。在SQL中,他们使用了约束来限制该属性允许的值。

CONSTRAINT film_rating_check CHECK 
    ((((((((rating)::text = ''::text) OR 
          ((rating)::text = 'G'::text)) OR 
          ((rating)::text = 'PG'::text)) OR 
          ((rating)::text = 'PG-13'::text)) OR 
          ((rating)::text = 'R'::text)) OR 
          ((rating)::text = 'NC-17'::text)))

我认为使用Java枚举将约束映射到对象世界会很好。但由于"PG-13"和"NC-17"中的特殊字符,无法简单地取允许的值。因此我实现了以下枚举:

public enum Rating {

    UNRATED ( "" ),
    G ( "G" ), 
    PG ( "PG" ),
    PG13 ( "PG-13" ),
    R ( "R" ),
    NC17 ( "NC-17" );

    private String rating;

    private Rating(String rating) {
        this.rating = rating;
    }

    @Override
    public String toString() {
        return rating;
    }
}

@Entity
public class Film {
    ..
    @Enumerated(EnumType.STRING)
    private Rating rating;
    ..
通过toString()方法,将方向枚举类型转换为字符串很容易,但是字符串转换为枚举类型不起作用。我遇到了以下异常:
[TopLink Warning]: 2008.12.09 01:30:57.434--ServerSession(4729123)--Exception [TOPLINK-116] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.DescriptorException Exception Description: No conversion value provided for the value [NC-17] in field [FILM.RATING]. Mapping: oracle.toplink.essentials.mappings.DirectToFieldMapping[rating-->FILM.RATING] Descriptor: RelationalDescriptor(de.fhw.nsdb.entities.Film --> [DatabaseTable(FILM)])
祝好,
timo

你是故意省略了字段的@Column属性吗?没有它,持久化任何东西都会很困难... - Yuval
你需要在枚举内部创建一个静态哈希映射表,以及一个名为“getByRating”的静态方法。 - JeeBee
12个回答

33

你尝试过存储序数值吗?如果没有与该值关联的字符串,则存储字符串值是可以的。

@Enumerated(EnumType.ORDINAL)

3
考虑到@Enumerated是JPA1的一部分,我无法相信这个问题没有更多的赞。就个人而言,我认为EnumType.STRING会更好,因为cletus提出了一些理由。 - Powerlord
18
@electrotype 是的,从技术上讲是可以的,但这远非最佳实践。将枚举按其数字存储是一个可怕的想法。即使进行非常基本的更改而不改变代码的功能,也很容易使您持久化的所有数据无效。例如,重新排列枚举值的顺序或在列表开头添加新值。 - spaaarky21
@spaaarky21 在大多数情况下我同意这个观点,但是对于那些永远不会改变的值,比如星期几,你可以存储序数(并将其作为TINYINT(1)持久化,就像星期几一样)。 - Jelle Blaauw
1
@JelleBlaauw 我会按照惯例在所有地方都这样做。甚至星期几也可能会有问题。一周只有七天,但不同的库对待它们的方式不同。主要的区别在于一周是否以星期日开始(例如,Java Calendar),还是以星期一开始(例如,Java 8 DayOfWeek),以及值是基于0(例如,枚举序数)还是基于1(例如,Joda常量)。想象一下,你将项目从一个库更改为另一个库,一个好心的开发人员重新排列了你的DayOfWeek枚举值以匹配新库。 - spaaarky21

27

你在这里遇到了一个问题,那就是JPA在处理枚举时的能力有限。对于枚举,你有两个选择:

  1. 将它们存储为等于Enum.ordinal() 的数字,这是一个糟糕的选择(我的看法);或者
  2. 将它们存储为等于Enum.name()的字符串。请注意:不是toString(),尤其是因为Enum.toString()的默认行为是返回name()

个人认为最好的选项是(2)。

现在你有一个问题,那就是你定义的值在Java中不表示有效的实例名称(即使用连字符)。所以你有以下选择:

  • 更改你的数据;
  • 将String字段持久化并隐式转换它们到/从你的对象中的枚举;或者
  • 使用像TypeConverters这样的非标准扩展。

我会按照这个顺序(从前往后)进行,作为首选项的顺序。

有人建议使用Oracle TopLink的转换器,但你可能正在使用Toplink Essentials,它是参考JPA 1.0实现的子集,而非商业版Oracle Toplink产品。

作为另一个建议,我强烈建议切换到EclipseLink。它比Toplink Essentials更完整的实现,而且Eclipselink将在发布时成为JPA 2.0的参考实现(预计在明年JavaOne中发布)。


8

5
public enum Rating {

    UNRATED ( "" ),
    G ( "G" ), 
    PG ( "PG" ),
    PG13 ( "PG-13" ),
    R ( "R" ),
    NC17 ( "NC-17" );

    private String rating;

    private static Map<String, Rating> ratings = new HashMap<String, Rating>();
    static {
        for (Rating r : EnumSet.allOf(Rating.class)) {
            ratings.put(r.toString(), r);
        }
    }

    private static Rating getRating(String rating) {
        return ratings.get(rating);
    }

    private Rating(String rating) {
        this.rating = rating;
    }

    @Override
    public String toString() {
        return rating;
    }
}

我不知道如何在注释的TopLink方面进行映射。


2
我不了解 TopLink 的内部细节,但我的猜测是:它使用 "Rating.valueOf(String s)" 方法来进行反向映射。由于无法重写 valueOf() 方法,因此必须遵循 Java 命名约定,以允许正确的 valueOf 方法。
public enum Rating {

    UNRATED,
    G, 
    PG,
    PG_13 ,
    R ,
    NC_17 ;

    public String getRating() {
        return name().replace("_","-");;
    }
}

getRating函数产生可读性强的评级。请注意,枚举标识符中不允许使用“-”字符。

当然,您需要将值存储在数据库中作为NC_17。


1
在 JPA 2.0 中,可以通过将枚举类型包装在一个 Embeddable 类中,而不使用 name() 或 ordinal() 方法来持久化枚举类型。假设我们有如下的枚举类型,其中 code 值需要存储在数据库中:
  public enum ECourseType {
    PACS004("pacs.004"), PACS008("pacs.008");

    private String code;

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

    public String getCode() {
        return code;
    }
}

请注意,由于包含点号,code值不能用作enum的名称。这个备注解释了我们提供的解决方法。
我们可以构建一个不可变类(作为值对象),用静态方法from()enum中的code值包装起来,像这样构建它:
@Embeddable
public class CourseType {

private static Map<String, ECourseType> codeToEnumCache = 
Arrays.stream(ECourseType.values())
            .collect(Collectors.toMap( e -> e.getCode(), e -> e));

    private String value;

    private CourseType() {};

    public static CourseType from(ECourseType en) {
        CourseType toReturn = new CourseType();
        toReturn.value = en.getCode();
        return toReturn;
    }

    public ECourseType getEnum() {
        return codeToEnumCache.get(value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass() ) return false;

        CourseType that = (CourseType) o;
        return Objects.equals(value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

编写正确的equals()hashcode()方法对于确保该类的“值对象”目标非常重要。

如果需要,可以添加CourseType和ECourseType之间的等价方法(但不要与equals()混淆):

public boolean isEquiv(ECourseType eCourseType) {
    return Objects.equals(eCourseType, getEnum());
}

现在,这个类可以嵌入到实体类中:

    public class Course {
    
    @Id
    @GeneratedValue
    @Column(name = "COU_ID")
    private Long pk;

    @Basic
    @Column(name = "COURSE_NAME")
    private String name;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "value", column = @Column(name = "COURSE_TYPE")),
    })
    private CourseType type;

    public void setType(CourseType type) {
        this.type = type;
    }

    public void setType(ECourseType type) {
        this.type = CourseType.from(type);
    }

}

请注意,为方便起见添加了 setType(ECourseType type) 设置器。类似的获取器可以添加以获取 type 作为 ECourseType
使用此建模,Hibernate 会生成以下 SQL 表(对于 H2 数据库):
CREATE TABLE "PUBLIC"."COU_COURSE"
(
   COU_ID bigint PRIMARY KEY NOT NULL,
   COURSE_NAME varchar(255),
   COURSE_TYPE varchar(255)
)
;

枚举类型的“code”值将存储在COURSE_TYPE中。

Course实体可以使用如下简单的查询进行搜索:

    public List<Course> findByType(CourseType type) {
    manager.clear();
    Query query = manager.createQuery("from Course c where c.type = :type");
    query.setParameter("type", type);
    return (List<Course>) query.getResultList();
}

结论:

这展示了如何持久化一个枚举,既不使用name也不使用ordinal,但确保实体的干净建模依赖于它。 这对于遗留代码特别有用,当存储在数据库中的值不符合枚举名称和序数的java语法时。 它还允许重构枚举名称而无需更改数据库中的值。


1
使用您现有的枚举评级。您可以使用AttributeCoverter。
@Converter(autoApply = true)
public class RatingConverter implements AttributeConverter<Rating, String> {

    @Override
    public String convertToDatabaseColumn(Rating rating) {
        if (rating == null) {
            return null;
        }
        return rating.toString();
    }

    @Override
    public Rating convertToEntityAttribute(String code) {
        if (code == null) {
            return null;
        }

        return Stream.of(Rating.values())
          .filter(c -> c.toString().equals(code))
          .findFirst()
          .orElseThrow(IllegalArgumentException::new);
    }
}

1
问题在于,我认为JPA从未考虑过我们可能已经有一个复杂的现有模式的想法。
我认为由此产生了两个主要的缺点,特别是对于枚举:
1. 使用name()和ordinal()的限制。为什么不像@Entity一样用@Id标记getter呢? 2. 枚举通常在数据库中具有表示形式,以允许与各种元数据(包括适当的名称、描述性名称、可能是本地化等)相关联。我们需要将Enum的易用性与Entity的灵活性相结合。
支持我的事业并在JPA_SPEC-47上投票。

0

这个怎么样?

public String getRating{  
   return rating.toString();
}

pubic void setRating(String rating){  
   //parse rating string to rating enum
   //JPA will use this getter to set the values when getting data from DB   
}  

@Transient  
public Rating getRatingValue(){  
   return rating;
}

@Transient  
public Rating setRatingValue(Rating rating){  
   this.rating = rating;
}

使用此方法,您可以将评级作为字符串在数据库和实体上使用,但在其他所有情况下都使用枚举。


-1
请使用这个注解。
@Column(columnDefinition="ENUM('User', 'Admin')")

4
我很确定那样做行不通。告诉 JPA 列的定义方式并不能指示它以不同的方式在数据库中查找值。在 Hibernate 中,提供列的定义有助于解决类型不匹配的问题——例如,如果 Hibernate 认为应该对某一列使用一种类型的 blob,但数据库使用另一种类型,则列定义可以帮助解决这个问题。 - spaaarky21

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接