Перейти к основному содержимому

Сохранение перечислений в JPA

· 8 мин. чтения

1. Обзор

В JPA версии 2.0 и ниже нет удобного способа сопоставления значений Enum со столбцом базы данных. Каждый вариант имеет свои ограничения и недостатки. Этих проблем можно избежать, используя функции JPA 2.1.

В этом руководстве мы рассмотрим различные возможности сохранения перечислений в базе данных с использованием JPA. Мы также опишем их преимущества и недостатки, а также приведем простые примеры кода.

2. Использование аннотации @Enumerated

Наиболее распространенным вариантом сопоставления значения перечисления с его представлением базы данных в JPA до версии 2.1 является использование аннотации @Enumerated . Таким образом, мы можем указать провайдеру JPA преобразовать перечисление в его порядковое или строковое значение.

В этом разделе мы рассмотрим оба варианта.

Но давайте сначала создадим простой @Entity , который мы будем использовать в этом руководстве:

@Entity
public class Article {
@Id
private int id;

private String title;

// standard constructors, getters and setters
}

2.1. Сопоставление порядкового значения

Если мы поместим аннотацию @Enumerated(EnumType.ORDINAL) в поле enum, JPA будет использовать значение Enum.ordinal() при сохранении данного объекта в базе данных.

Введем первое перечисление:

public enum Status {
OPEN, REVIEW, APPROVED, REJECTED;
}

Затем добавим его в класс Article и аннотируем с помощью @Enumerated(EnumType.ORDINAL) :

@Entity
public class Article {
@Id
private int id;

private String title;

@Enumerated(EnumType.ORDINAL)
private Status status;
}

Теперь при сохранении объекта статьи :

Article article = new Article();
article.setId(1);
article.setTitle("ordinal title");
article.setStatus(Status.OPEN);

JPA вызовет следующий оператор SQL:

insert 
into
Article
(status, title, id)
values
(?, ?, ?)
binding parameter [1] as [INTEGER] - [0]
binding parameter [2] as [VARCHAR] - [ordinal title]
binding parameter [3] as [INTEGER] - [1]

Проблема возникает с таким отображением, когда нам нужно изменить наше перечисление. Если мы добавим новое значение в середине или изменим порядок перечисления, мы сломаем существующую модель данных.

Такие проблемы может быть трудно обнаружить, а также проблематично исправить, поскольку нам придется обновить все записи базы данных.

2.2. Сопоставление строкового значения

Аналогично, JPA будет использовать значение Enum.name() при сохранении объекта, если мы аннотируем поле перечисления с помощью @Enumerated(EnumType.STRING) .

Давайте создадим второе перечисление:

public enum Type {
INTERNAL, EXTERNAL;
}

И давайте добавим его в наш класс Article и аннотируем с помощью @Enumerated(EnumType.STRING) :

@Entity
public class Article {
@Id
private int id;

private String title;

@Enumerated(EnumType.ORDINAL)
private Status status;

@Enumerated(EnumType.STRING)
private Type type;
}

Теперь при сохранении объекта статьи :

Article article = new Article();
article.setId(2);
article.setTitle("string title");
article.setType(Type.EXTERNAL);

JPA выполнит следующую инструкцию SQL:

insert 
into
Article
(status, title, type, id)
values
(?, ?, ?, ?)
binding parameter [1] as [INTEGER] - [null]
binding parameter [2] as [VARCHAR] - [string title]
binding parameter [3] as [VARCHAR] - [EXTERNAL]
binding parameter [4] as [INTEGER] - [2]

С помощью @Enumerated(EnumType.STRING) мы можем безопасно добавлять новые значения перечисления или изменять порядок нашего перечисления. Однако переименование значения перечисления все равно нарушит данные базы данных.

Кроме того, несмотря на то, что это представление данных намного удобнее для чтения по сравнению с параметром @Enumerated(EnumType.ORDINAL) , оно также занимает гораздо больше места, чем необходимо. Это может оказаться серьезной проблемой, когда нам нужно иметь дело с большим объемом данных.

3. Использование аннотаций @PostLoad и @PrePersist

Другой вариант, который мы должны иметь дело с сохраняющимися перечислениями в базе данных, — это использовать стандартные методы обратного вызова JPA. Мы можем отображать наши перечисления туда и обратно в событиях @PostLoad и @PrePersist .

Идея состоит в том, чтобы иметь два атрибута в сущности. Первый сопоставляется со значением базы данных, а второй представляет собой поле @Transient , которое содержит реальное значение перечисления. Затем переходный атрибут используется кодом бизнес-логики.

Чтобы лучше понять концепцию, давайте создадим новое перечисление и используем его значение int в логике сопоставления:

public enum Priority {
LOW(100), MEDIUM(200), HIGH(300);

private int priority;

private Priority(int priority) {
this.priority = priority;
}

public int getPriority() {
return priority;
}

public static Priority of(int priority) {
return Stream.of(Priority.values())
.filter(p -> p.getPriority() == priority)
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}

Мы также добавили метод Priority.of() , чтобы упростить получение экземпляра Priority на основе его значения int .

Теперь, чтобы использовать его в нашем классе Article , нам нужно добавить два атрибута и реализовать методы обратного вызова:

@Entity
public class Article {

@Id
private int id;

private String title;

@Enumerated(EnumType.ORDINAL)
private Status status;

@Enumerated(EnumType.STRING)
private Type type;

@Basic
private int priorityValue;

@Transient
private Priority priority;

@PostLoad
void fillTransient() {
if (priorityValue > 0) {
this.priority = Priority.of(priorityValue);
}
}

@PrePersist
void fillPersistent() {
if (priority != null) {
this.priorityValue = priority.getPriority();
}
}
}

Теперь при сохранении объекта статьи :

Article article = new Article();
article.setId(3);
article.setTitle("callback title");
article.setPriority(Priority.HIGH);

JPA вызовет следующий SQL-запрос:

insert 
into
Article
(priorityValue, status, title, type, id)
values
(?, ?, ?, ?, ?)
binding parameter [1] as [INTEGER] - [300]
binding parameter [2] as [INTEGER] - [null]
binding parameter [3] as [VARCHAR] - [callback title]
binding parameter [4] as [VARCHAR] - [null]
binding parameter [5] as [INTEGER] - [3]

Несмотря на то, что этот вариант дает нам больше гибкости в выборе представления значения базы данных по сравнению с ранее описанными решениями, он не идеален. Просто неправильно иметь два атрибута, представляющих одно перечисление в сущности. Кроме того, если мы используем этот тип сопоставления, мы не сможем использовать значение перечисления в запросах JPQL.

4. Использование аннотации JPA 2.1 @Converter

Чтобы преодолеть ограничения показанных выше решений, в выпуске JPA 2.1 представлен новый стандартизированный API, который можно использовать для преобразования атрибута объекта в значение базы данных и наоборот. Все, что нам нужно сделать, это создать новый класс, реализующий javax.persistence.AttributeConverter , и аннотировать его с помощью @Converter .

Давайте посмотрим на практический пример.

Во-первых, мы создадим новое перечисление:

public enum Category {
SPORT("S"), MUSIC("M"), TECHNOLOGY("T");

private String code;

private Category(String code) {
this.code = code;
}

public String getCode() {
return code;
}
}

Нам также нужно добавить его в класс Article :

@Entity
public class Article {

@Id
private int id;

private String title;

@Enumerated(EnumType.ORDINAL)
private Status status;

@Enumerated(EnumType.STRING)
private Type type;

@Basic
private int priorityValue;

@Transient
private Priority priority;

private Category category;
}

Теперь давайте создадим новый CategoryConverter :

@Converter(autoApply = true)
public class CategoryConverter implements AttributeConverter<Category, String> {

@Override
public String convertToDatabaseColumn(Category category) {
if (category == null) {
return null;
}
return category.getCode();
}

@Override
public Category convertToEntityAttribute(String code) {
if (code == null) {
return null;
}

return Stream.of(Category.values())
.filter(c -> c.getCode().equals(code))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}

Мы установили значение autoApply @Converter в true , чтобы JPA автоматически применяла логику преобразования ко всем сопоставленным атрибутам типа категории . В противном случае нам пришлось бы поместить аннотацию @Converter непосредственно в поле сущности.

Теперь давайте сохраним объект статьи :

Article article = new Article();
article.setId(4);
article.setTitle("converted title");
article.setCategory(Category.MUSIC);

Затем JPA выполнит следующую инструкцию SQL:

insert 
into
Article
(category, priorityValue, status, title, type, id)
values
(?, ?, ?, ?, ?, ?)
Converted value on binding : MUSIC -> M
binding parameter [1] as [VARCHAR] - [M]
binding parameter [2] as [INTEGER] - [0]
binding parameter [3] as [INTEGER] - [null]
binding parameter [4] as [VARCHAR] - [converted title]
binding parameter [5] as [VARCHAR] - [null]
binding parameter [6] as [INTEGER] - [4]

Как мы видим, мы можем просто установить свои собственные правила преобразования перечислений в соответствующее значение базы данных, если мы используем интерфейс AttributeConverter . Более того, мы можем безопасно добавлять новые значения перечисления или изменять существующие, не нарушая уже сохраненные данные.

Общее решение простое в реализации и устраняет все недостатки вариантов, представленных в предыдущих разделах.

5. Использование перечислений в JPQL

Давайте теперь посмотрим, насколько просто использовать перечисления в запросах JPQL.

Чтобы найти все сущности Article с категорией Category.SPORT , нам нужно выполнить следующую инструкцию:

String jpql = "select a from Article a where a.category = com.foreach.jpa.enums.Category.SPORT";

List<Article> articles = em.createQuery(jpql, Article.class).getResultList();

Важно отметить, что в этом случае нам нужно использовать полное имя перечисления.

Конечно, мы не ограничиваемся статическими запросами.

Совершенно законно использовать именованные параметры:

String jpql = "select a from Article a where a.category = :category";

TypedQuery<Article> query = em.createQuery(jpql, Article.class);
query.setParameter("category", Category.TECHNOLOGY);

List<Article> articles = query.getResultList();

В этом примере представлен очень удобный способ формирования динамических запросов.

Кроме того, нам не нужно использовать полные имена.

6. Заключение

В этой статье мы рассмотрели различные способы сохранения значений перечисления в базе данных. Мы представили наши варианты использования JPA в версии 2.0 и ниже, а также новый API, доступный в JPA 2.1 и выше.

Стоит отметить, что это не единственные возможности работы с перечислениями в JPA. Некоторые базы данных, такие как PostgreSQL, предоставляют специальный тип столбца для хранения значений перечисления. Однако такие решения выходят за рамки данной статьи.

Как правило, мы всегда должны использовать интерфейс AttributeConverter и аннотацию @Converter , если мы используем JPA 2.1 или более позднюю версию.

Как обычно, все примеры кода доступны в нашем репозитории GitHub .