37

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

Buhake Sindi
  • 87,898
  • 29
  • 167
  • 228
Timo
  • 597
  • 1
  • 6
  • 11
  • did you omit the @Column attribute for the field intentionally? Without it, it would be hard to persist anything... – Yuval Dec 09 '08 at 12:53
  • You need a static hashmap of your enums inside your enum, and a static method "getByRating" – JeeBee Dec 09 '08 at 14:06

12 Answers12

33

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)
aledbf
  • 777
  • 7
  • 7
  • 3
    I can't believe this doesn't have more upvotes, considering that @Enumerated is [part of JPA1](http://docs.oracle.com/javaee/5/api/javax/persistence/Enumerated.html). Personally, I think `EnumType.STRING` would be better for reasons cletus stated, though. – Powerlord May 15 '12 at 19:39
  • 18
    @electrotype Yes, technically it works but it's far from being a best practice. Storing an enumeration by its numeral is a HORRIBLE idea. Very basic changes that don't change the functionality of your code can easily make all of your persisted data invalid. For example, rearranging the order of the enum values or adding a new value at the beginning of the list. – spaaarky21 Aug 29 '12 at 22:14
  • @spaaarky21 I agree with this in most cases, however, storing values which will never change, like weekdays, you may as well store the ordinal (and persist it as a TINYINT(1), in case of weekdays that is). – Jelle Blaauw Sep 18 '17 at 09:54
  • 1
    @JelleBlaauw I would do it same everywhere as a convention. Even the day of the week could be problematic. There will only ever be seven days in the week but different libraries treat them differently. The major differences are whether the week starts with Sunday (e.g., Java Calendar) or Monday (e.g., Java 8 DayOfWeek), and whether the values are 0-based (e.g., enum ordinals) or 1-based (e.g., Joda constants.) Imagine that you change your project from one library to another and a well-meaning developer reorders your `DayOfWeek` enum values to match. – spaaarky21 Sep 18 '17 at 18:26
27

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

cletus
  • 616,129
  • 168
  • 910
  • 942
8

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

Extending OracleAS TopLink to Support Custom Type Conversions

blahdiblah
  • 33,069
  • 21
  • 98
  • 152
Dan Vinton
  • 26,401
  • 9
  • 37
  • 79
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;
    }
}

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

JeeBee
  • 17,476
  • 5
  • 50
  • 60
2

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.

Andreas Petersson
  • 16,248
  • 11
  • 59
  • 91
1

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

Martin
  • 2,573
  • 28
  • 22
YoYo
  • 9,157
  • 8
  • 57
  • 74
1

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);
    }
}
Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265
1

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.

0

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.

Mg.
  • 1,469
  • 2
  • 16
  • 29
-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;
    }
}

}

-1

use this annotation

@Column(columnDefinition="ENUM('User', 'Admin')")
  • 4
    I'm pretty sure that won't work. Telling JPA how a column is defined won't instruct it to lookup a value for the database in a different way. In Hibernate, giving a column definition helps with type mismatches - like if Hibernate thinks it should be using one type of blob for a column but the database uses another. – spaaarky21 Feb 20 '12 at 08:42
-2

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" );
(...)

pingpong
  • 17
  • 2