Как использовать перечисления с JPA
У меня есть база данных системы проката фильмов. Каждый фильм имеет атрибут рейтинга. В 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() направление enum -> String работает нормально, а String -> enum не работает. Я получаю следующее исключение:
[Предупреждение 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] в поле [FILM.RATING]. Отображение: oracle.toplink.essentials.mappings.DirectToFieldMapping[rating ->FILM.RATING] Дескриптор: RelationalDescriptor(de.fhw.nsdb.entities.Film -> [DatabaseTable(FILM)])
ура
Timo
12 ответов
Похоже, вам нужно добавить поддержку для пользовательского типа:
Расширение OracleAS TopLink для поддержки пользовательских преобразований типов
Вы пытались сохранить порядковый номер. Сохранение строкового значения работает нормально, если у вас нет связанной строки со значением:
@Enumerated(EnumType.ORDINAL)
У вас есть проблема, и это ограниченные возможности JPA, когда дело доходит до обработки перечислений. С перечислениями у вас есть два варианта:
- Храните их как число, равное
Enum.ordinal()
это ужасная идея (imho); или же - Храните их как строку, равную
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).
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.
Я не знаю внутренностей 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.
В JPA 2.0 способ сохраненияname()
ниordinal()
можно сделать, завернув вEmbeddable
сорт.
Предположим, у нас есть следующее со значением, предназначенным для хранения в базе данных:
public enum ECourseType {
PACS004("pacs.004"), PACS008("pacs.008");
private String code;
ECourseType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
Обратите внимание, что значения нельзя использовать в качестве имен для, поскольку они содержат точки. Это замечание оправдывает предложенный нами обходной путь.
Мы можем создать неизменяемый класс (как объект-значение), обертывающийcode
значение перечисления со статическим методомfrom()
построить его изenum
, так :
@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 генерирует (для 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
ниordinal
но обеспечить чистое моделирование объекта, полагающегося на него. Это может быть особенно полезно для устаревших, когда значения, хранящиеся в db, не соответствуют синтаксису java имен перечислений и порядковых номеров. Это также позволяет проводить рефакторинг имен перечислений без изменения значений в db.
Используя существующие enum Rating
. Ты можешь использовать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);
}
}
Проблема, я думаю, в том, что JPA никогда не задумывался с мыслью о том, что у нас уже может быть сложная существующая схема.
Я думаю, что это связано с двумя основными недостатками, характерными для Enum:
- Ограничение использования name() и ordinal(). Почему бы просто не пометить геттер с помощью @Id, как мы делаем с @Entity?
- Enum обычно имеют представление в базе данных, чтобы разрешить ассоциацию со всеми видами метаданных, включая собственное имя, описательное имя, может быть что-то с локализацией и т. Д. Нам нужно простое использование Enum в сочетании с гибкостью Entity.
Помоги моему делу и проголосуй за JPA_SPEC-47
Как насчет этого
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;
}
при этом вы используете рейтинги как String как для вашей БД, так и для сущности, но используйте перечисление для всего остального.
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;
}
}
сравнить -> Enum
открытый класс RatingComparator реализует Comparator {
public int compare(final ParentalControlLevelsEnum o1, final ParentalControlLevelsEnum o2) {
if (o1.ordinal() < o2.ordinal()) {
return -1;
} else {
return 1;
}
}
}
Решено!!! Где я нашел ответ: http://programming.itags.org/development-tools/65254/
Вкратце, конверсия ищет имя enum, а не значение атрибута 'rating'. В вашем случае: если у вас есть в базе данных значения "NC-17", вы должны иметь в своем перечислении:
enum Rating {
(...)
NC-17 ("NC-17");
(...)
Используйте эту аннотацию
@Column(columnDefinition="ENUM('User', 'Admin')")