Если вы когда-либо работали с перечислениями Java в JPA, вы наверняка знаете об их ограничениях и ловушках. Использование enum
в качестве свойства вашего @Entity
часто является очень хорошим выбором, однако JPA до 2.1 не очень хорошо с ними справлялся. Это дало вам 2 + 1 выбора:
-
@Enumerated(EnumType.ORDINAL)
(по умолчанию) отобразит значенияenum
с помощьюEnum.ordinal()
. В основном первое перечисляемое значение будет отображаться в0
в столбце базы данных, второе в1
и т. Д. Это очень компактно и прекрасно работает до того момента, когда вы захотите изменить свое перечисление. Удаление или добавление значения в середине или перестановка их полностью сломает существующие записи. Ой! Что еще хуже, модульные и интеграционные тесты часто работают на чистой базе данных, поэтому они не улавливают расхождения в старых данных. -
@Enumerated(EnumType.STRING)
намного безопаснее, потому что он хранит строковое представлениеenum
. Теперь вы можете безопасно добавлять новые значения и перемещать их. Однако переименованиеenum
в коде Java по-прежнему нарушает существующие записи в БД. Что еще более важно, такое представление очень многословно, излишне потребляя ресурсы базы данных. - Вы также можете использовать необработанное представление (например, один
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" ?> < 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
класс PhoneNumber
(с isInternational()
, getCountryCode()
и настраиваемой логикой проверки) вместо String
или long
. Это небольшое дополнение в JPA 2.1, безусловно, значительно улучшит качество доменных объектов.
Если вы хотите немного поиграть с этой функцией, пример веб-приложения Spring доступен на GitHub .