Как использовать перечисления с 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, когда дело доходит до обработки перечислений. С перечислениями у вас есть два варианта:

  1. Храните их как число, равное Enum.ordinal()это ужасная идея (imho); или же
  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).

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:

  1. Ограничение использования name() и ordinal(). Почему бы просто не пометить геттер с помощью @Id, как мы делаем с @Entity?
  2. 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')")
Другие вопросы по тегам