Статьи

Отображение перечислений сделано правильно с @Convert в JPA 2.1

Если вы когда-либо работали с перечислениями Java в JPA, вы наверняка знаете об их ограничениях и ловушках. Использование enum в качестве свойства вашего @Entity часто является очень хорошим выбором, однако JPA до 2.1 не очень хорошо с ними справлялся. Это дало вам 2 + 1 выбора:

Torshovdalen

Torshovdalen

  1. @Enumerated(EnumType.ORDINAL) (по умолчанию) отобразит значения enum с помощью Enum.ordinal() . В основном первое перечисляемое значение будет отображаться в 0 в столбце базы данных, второе в 1 и т. Д. Это очень компактно и прекрасно работает до того момента, когда вы захотите изменить свое перечисление. Удаление или добавление значения в середине или перестановка их полностью сломает существующие записи. Ой! Что еще хуже, модульные и интеграционные тесты часто работают на чистой базе данных, поэтому они не улавливают расхождения в старых данных.
  2. @Enumerated(EnumType.STRING) намного безопаснее, потому что он хранит строковое представление enum . Теперь вы можете безопасно добавлять новые значения и перемещать их. Однако переименование enum в коде Java по-прежнему нарушает существующие записи в БД. Что еще более важно, такое представление очень многословно, излишне потребляя ресурсы базы данных.
  3. Вы также можете использовать необработанное представление (например, один char или int ) и отображать его вручную в @PostLoad / @PrePersist / @PreUpdate . Наиболее гибкий и безопасный с точки зрения базы данных, но довольно уродливый.

К счастью, Java Persistence API 2.1 ( JSR-388 ), выпущенный несколько дней назад, предоставляет стандартизированный механизм подключаемых преобразователей данных . Такой API издавна присутствовал в проприетарных формах, и это не совсем ракетостроение, но иметь его в составе JPA — большое улучшение. Насколько мне известно, Eclipselink является единственной реализацией JPA 2.1, доступной на сегодняшний день, поэтому мы будем использовать ее для экспериментов.

Мы начнем с примера приложения Spring, разработанного в рамках статьи « CRUD для бедных: jqGrid, REST, AJAX и Spring MVC в одном доме » . У этой версии не было постоянства, поэтому мы добавим тонкий слой DAO поверх Spring Data JPA при поддержке Eclipselink. Единственная сущность на данный момент — это Book :

01
02
03
04
05
06
07
08
09
10
11
12
13
@Entity
public class Book {
  
    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Integer id;
  
    //...
  
    private Cover cover;
  
    //...
}

Где Cover это enum :

1
2
3
4
5
public enum Cover {
  
    PAPERBACK, HARDCOVER, DUST_JACKET
  
}

Ни ORDINAL ни STRING не являются хорошим выбором здесь. Первая из-за того, что перестановка первых трех значений каким-либо образом нарушит загрузку существующих записей. Последнее слишком многословно. Вот где в игру вступают пользовательские конвертеры в JPA:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
  
@Converter
public class CoverConverter implements AttributeConverter<Cover, String> {
  
    @Override
    public String convertToDatabaseColumn(Cover attribute) {
        switch (attribute) {
            case DUST_JACKET:
                return "D";
            case HARDCOVER:
                return "H";
            case PAPERBACK:
                return "P";
            default:
                throw new IllegalArgumentException("Unknown" + attribute);
        }
    }
  
    @Override
    public Cover convertToEntityAttribute(String dbData) {
        switch (dbData) {
            case "D":
                return DUST_JACKET;
            case "H":
                return HARDCOVER;
            case "P":
                return PAPERBACK;
            default:
                throw new IllegalArgumentException("Unknown" + dbData);
        }
    }
}

Хорошо, я не буду оскорблять вас, мой дорогой читатель, объясняя это. Преобразование enum во что угодно будет храниться в реляционной базе данных и наоборот. Теоретически JPA-провайдер должен применять конвертеры автоматически, если они объявлены с:

1
@Converter(autoApply = true

Это не сработало для меня. Более того, объявление их явно вместо @Enumerated в классе @Entity также не сработало:

1
2
3
4
5
6
import javax.persistence.Convert;
  
//...
  
@Convert(converter = CoverConverter.class)
private Cover cover;

В результате чего происходит исключение:

1
2
3
Exception Description: The converter class [com.blogspot.nurkiewicz.CoverConverter]
specified on the mapping attribute [cover] from the class [com.blogspot.nurkiewicz.Book] was not found.
Please ensure the converter class name is correct and exists with the persistence unit definition.

Ошибка или особенность, я должен был упомянуть конвертер в orm.xml :

1
2
3
4
<?xml version="1.0"?>
<entity-mappings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/orm" version="2.1">
    <converter class="com.blogspot.nurkiewicz.CoverConverter"/>
</entity-mappings>

И это летит! У меня есть свобода изменения моего перечисления Cover (добавление, перестановка, переименование) без влияния на существующие записи.

Один совет, которым я хотел бы поделиться с вами, связан с ремонтопригодностью. Каждый раз, когда у вас есть фрагмент кода из или для enum , убедитесь, что он проверен правильно. И я не имею в виду тестирование всех возможных существующих значений вручную. Я больше после теста, чтобы убедиться, что новые значения enum отражены в коде отображения. Подсказка: приведенный ниже код потерпит неудачу (с помощью IllegalArgumentException ), если вы добавите новое значение enum но забудете добавить код сопоставления из него:

1
2
3
for (Cover cover : Cover.values()) {
    new CoverConverter().convertToDatabaseColumn(cover);
}

Пользовательские конвертеры в JPA 2.1 гораздо полезнее, чем мы видели. Если вы объединяете JPA со Scala, вы можете использовать @Converter для непосредственного scala.math.BigDecimal столбцов базы данных с scala.math.BigDecimal , scala.Option или небольшим регистром классов. В Java наконец появится портативный способ отображения времени Joda . И последнее, но не менее важное: если вам нравится (очень) строго типизированный домен, вы можете PhoneNumber класс PhoneNumberisInternational() , getCountryCode() и настраиваемой логикой проверки) вместо String или long . Это небольшое дополнение в JPA 2.1, безусловно, значительно улучшит качество доменных объектов.

Если вы хотите немного поиграть с этой функцией, пример веб-приложения Spring доступен на GitHub .