Рубрики
Без рубрики

Сохраняющиеся enums в JPA

Быстрое и практическое руководство по сохраняющиеся enums в JPA.

Автор оригинала: Krzysztof Woyke.

1. Введение

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

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

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

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

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

Но сначала давайте создадим простую @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() значение при емме данной сущности в базе данных.

Давайте представим первый enum:

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

Далее, давайте добавим его в Статья класса и аннотировать его с @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 вызовет следующее заявление S’L:

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]

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

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

2.2. Отображение значения строки

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

Давайте создадим второй enum:

public enum Type {
    INTERNAL, EXTERNAL;
}

И давайте добавим его к нашему Статья класса и аннотировать его с @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 выполнит следующее заявление S’L:

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) , мы можем безопасно добавить новые значения enum или изменить заказ нашего enum. Однако переименование значения enum все равно нарушит данные базы данных.

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

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

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

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

Чтобы лучше понять концепцию, давайте создадим новый enum и использовать его 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);
    }
}

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

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

@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 запустит следующий запрос S’L:

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]

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

4. Использование АННОТАЦИИ JPA 2.1 @Converter

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

Рассмотрим практический пример. Но сначала, как обычно, мы создадим новый enum:

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

    private String code;

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

    public String getCode() {
        return code;
    }
}

Мы также должны добавить его в Статья класс:

@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;
}

Теперь давайте создадим новую КатегорияКонвертер :

@Converter(autoApply = true)
public class CategoryConverter implements AttributeConverter {
 
    @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);
    }
}

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

Давайте теперь упорствуем в Статья сущность:

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

Затем JPA выполнит следующее заявление S’L:

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]

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

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

5. Использование enums в JP’L

Давайте теперь посмотрим, как легко использовать enums в запросах JP’L.

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

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

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

Важно отметить, что в этом случае, мы должны использовать полностью квалифицированное имя enum.

Конечно, мы не ограничены статическими запросами. Совершенно законно использовать названные параметры:

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

TypedQuery
query = em.createQuery(jpql, Article.class); query.setParameter("category", Category.TECHNOLOGY); List
articles = query.getResultList();

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

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

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

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

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

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

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