如何在 JPA 中使用枚举

发布于 2024-07-10 18:20:55 字数 1500 浏览 9 评论 0原文

我有一个电影租赁系统的现有数据库。 每部电影都有一个评级属性。 在 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警告]:2008.12.09 01:30:57.434--ServerSession(4729123)--异常 [TOPLINK-116](Oracle TopLink Essentials - 2.0.1(构建 b09d-fcs (12/06/2007)): oracle.toplink.essentials.exceptions.DescriptorException 异常 说明:没有为 [NC-17] 中的值提供转换值。 字段[电影评级]。 映射: oracle.toplink.essentials.mappings.DirectToFieldMapping[评级-->FILM.RATING] 描述符:RelationalDescriptor(de.fhw.nsdb.entities.Film --> [数据库表(FILM)])

欢呼

蒂莫

I have an existing database of a film rental system. Each film has a has a rating attribute. In SQL they used a constraint to limit the allowed values of this attribute.

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

I think it would be nice to use a Java enum to map the constraint into the object world. But it's not possible to simply take the allowed values because of the special char in "PG-13" and "NC-17". So I implemented the following enum:

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

With the toString() method the direction enum -> String works fine, but String -> enum does not work. I get the following exception:

[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)])

cheers

timo

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

七禾 2024-07-17 18:20:55

您是否尝试过存储序数值? 如果您没有与该值关联的字符串,则存储字符串值可以正常工作:

@Enumerated(EnumType.ORDINAL)

have you tried to store the ordinal value. Store the string value works fine if you don't have an associated String to the value:

@Enumerated(EnumType.ORDINAL)
故事与诗 2024-07-17 18:20:55

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

  1. 将它们存储为等于 Enum.ordinal() 的数字,这是一个糟糕的主意(恕我直言); 或
  2. 将它们存储为等于 Enum.name() 的字符串。 注意: 不是您所期望的 toString(),特别是因为 Enum.toString() 的默认行为是返回 name( )

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

现在您遇到的问题是您定义的值不代表 Java 中的有效实例名称(即使用连字符)。 所以你的选择是:

  • 更改你的数据;
  • 保留字符串字段并隐式地将它们与对象中的枚举进行转换; 或
  • 使用非标准扩展,例如 TypeConverters。

我会按照优先顺序(从第一个到最后一个)执行它们。

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

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

You have a problem here and that is the limited capabilities of JPA when it comes to handling enums. With enums you have two choices:

  1. Store them as a number equalling Enum.ordinal(), which is a terrible idea (imho); or
  2. Store them as a string equalling Enum.name(). Note: not toString() as you might expect, especially since the default behaviourfor Enum.toString() is to return name().

Personally I think the best option is (2).

Now you have a problem in that you're defining values that don't represent vailid instance names in Java (namely using a hyphen). So your choices are:

  • Change your data;
  • Persist String fields and implicitly convert them to or from enums in your objects; or
  • Use nonstandard extensions like TypeConverters.

I would do them in that order (first to last) as an order of preference.

Someone suggested Oracle TopLink's converter but you're probably using Toplink Essentials, being the reference JPA 1.0 implementation, which is a subset of the commercial Oracle Toplink product.

As another suggestion, I'd strongly recommend switching to EclipseLink. It is a far more complete implementation than Toplink Essentials and Eclipselink will be the reference implementation of JPA 2.0 when released (expected by JavaOne mid next year).

思念满溢 2024-07-17 18:20:55

听起来您需要添加对自定义类型的支持:

Extending OracleAS TopLink to支持自定义类型转换

Sounds like you need to add support for a custom type:

Extending OracleAS TopLink to Support Custom Type Conversions

清晨说晚安 2024-07-17 18:20:55
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 方面进行映射。

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

I don't know how to do the mappings in the annotated TopLink side of things however.

旧伤还要旧人安 2024-07-17 18:20:55

我不知道 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 产生“人类可读”的评级。 请注意,枚举标识符中不允许使用“-”字符。

当然,您必须将这些值存储在 DB 中作为 NC_17。

i don't know internals of toplink, but my educated guess is the following: it uses the Rating.valueOf(String s) method to map in the other direction. it is not possible to override valueOf(), so you must stick to the naming convention of java, to allow a correct valueOf method.

public enum Rating {

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

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

getRating produces the "human-readable" rating. note that the "-" chanracter is not allowed in the enum identifier.

of course you will have to store the values in the DB as NC_17.

轻拂→两袖风尘 2024-07-17 18:20:55

我认为,问题在于,JPA 从未考虑过我们可以拥有一个复杂的预先存在的架构。

我认为,对于 Enum 来说,有两个主要缺点:

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

帮助我的事业并对 JPA_SPEC-47 进行投票

The problem is, I think, that JPA was never incepted with the idea in mind that we could have a complex preexisting Schema already in place.

I think there are two main shortcomings resulting from this, specific to Enum:

  1. The limitation of using name() and ordinal(). Why not just mark a getter with @Id, the way we do with @Entity?
  2. Enum's have usually representation in the database to allow association with all sorts of metadata, including a proper name, a descriptive name, maybe something with localization etc. We need the easy of use of an Enum combined with the flexibility of an Entity.

Help my cause and vote on JPA_SPEC-47

物价感观 2024-07-17 18:20:55

使用您现有的枚举评级。 您可以使用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);
    }
}

Using your existing enum Rating. You can use AttributeCoverters.

@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);
    }
}
时光磨忆 2024-07-17 18:20:55

在 JPA 2.0 中,既不使用 name() 也不使用 ordinal() 来持久化 enum 的方法可以通过包装 Embeddable 类中的 enum

假设我们有以下 enum,其中 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() 包装枚举的 code 值,以从 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);
    }

}

请注意 setter setType(ECourseType type)。 可以添加类似的 getter 来获取 type 作为 ECourseType

使用此建模,hibernate 生成(对于 H2 db)以下 SQL 表:

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也不使用name来持久化枚举ordinal 但确保依赖它的实体的干净建模。
当 db 中存储的值不符合枚举名称和序号的 java 语法时,这对于遗留系统特别有用。
它还允许重构枚举名称,而无需更改数据库中的值。

In JPA 2.0, a way to persist an enum using neither the name() nor ordinal() can be done by wrapping the enum in a Embeddable class.

Assume we have the following enum, with a code value intended to be stored in the database :

  public enum ECourseType {
    PACS004("pacs.004"), PACS008("pacs.008");

    private String code;

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

    public String getCode() {
        return code;
    }
}

Please note that the code values could not be used as names for the enum since they contain dots. This remark justifies the workaround we are providing.

We can build an immutable class (as a value object) wrapping the code value of the enum with a static method from() to build it from the enum, like this :

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

Writing proper equals() and hashcode() is important to insure the "value object" aim of this class.

If needed, an equivalence method between the CourseType et ECourseType may be added (but not mixed with equals()) :

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

This class can now be embedded in an entity class :

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

}

Please note that the setter setType(ECourseType type) has been added for convenience. A similar getter could be added to get the type as ECourseType.

Using this modeling, hibernate generates (for H2 db) the following SQL table :

CREATE TABLE "PUBLIC"."COU_COURSE"
(
   COU_ID bigint PRIMARY KEY NOT NULL,
   COURSE_NAME varchar(255),
   COURSE_TYPE varchar(255)
)
;

The "code" values of the enum will be stored in the COURSE_TYPE.

And the Course entities can be searched with a query as simple as this :

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

Conclusion:

This shows how to persist an enum using neither the name nor the ordinal but insure a clean modelling of an entity relying on it.
This is can be particularly useful for legacy when the values stored in db are not compliant to the java syntax of enum names and ordinals.
It also allows refactoring the enum names without having to change values in db.

窝囊感情。 2024-07-17 18:20:55

对此,

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

您在数据库和实体上都将评级用作字符串,但对其他所有内容都使用枚举。

What about this

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

with this you use the ratings as String both on your DB and entity, but use the enum for everything else.

ι不睡觉的鱼゛ 2024-07-17 18:20:55

使用这个注释

@Column(columnDefinition="ENUM('User', 'Admin')")

use this annotation

@Column(columnDefinition="ENUM('User', 'Admin')")
蔚蓝源自深海 2024-07-17 18:20:55

枚举
公共枚举 ParentalControlLevelsEnum {
U(“U”),PG(“PG”),_12(“12”),_15(“15”),_18(“18”);

private final String value;

ParentalControlLevelsEnum(final String value) {
    this.value = value;
}

public String getValue() {
    return value;
}

public static ParentalControlLevelsEnum fromString(final String value) {
    for (ParentalControlLevelsEnum level : ParentalControlLevelsEnum.values()) {
        if (level.getValue().equalsIgnoreCase(value)) {
            return level;
        }
    }
    return null;
}

}

比较 -> 枚举

公共类RatingComparator 实现Comparator {

public int compare(final ParentalControlLevelsEnum o1, final ParentalControlLevelsEnum o2) {
    if (o1.ordinal() < o2.ordinal()) {
        return -1;
    } else {
        return 1;
    }
}

}

Enum
public enum ParentalControlLevelsEnum {
U("U"), PG("PG"), _12("12"), _15("15"), _18("18");

private final String value;

ParentalControlLevelsEnum(final String value) {
    this.value = value;
}

public String getValue() {
    return value;
}

public static ParentalControlLevelsEnum fromString(final String value) {
    for (ParentalControlLevelsEnum level : ParentalControlLevelsEnum.values()) {
        if (level.getValue().equalsIgnoreCase(value)) {
            return level;
        }
    }
    return null;
}

}

compare -> Enum

public class RatingComparator implements Comparator {

public int compare(final ParentalControlLevelsEnum o1, final ParentalControlLevelsEnum o2) {
    if (o1.ordinal() < o2.ordinal()) {
        return -1;
    } else {
        return 1;
    }
}

}

掀纱窥君容 2024-07-17 18:20:55

解决!!!
我在哪里找到了答案: http://programming.itags.org/development-tools/65254/

简而言之,转换查找枚举的名称,而不是属性“评级”的值。
在您的情况下:如果您的数据库值中有“NC-17”,则您的枚举中需要有:

enum Rating {
(...)
NC-17(“NC-17”);
(...)

Resolved!!!
Where I found the answer: http://programming.itags.org/development-tools/65254/

Briefly, the convertion looks for the name of enum, not the value of attribute 'rating'.
In your case: If you have in the db values "NC-17", you need to have in your enum:

enum Rating {
(...)
NC-17 ( "NC-17" );
(...)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文