JPA & HIBERNATE

Вопросы для собеседования

Воросы:

Что такое: ORM, JPA, Hibernate?

ORM

JPA

Hibernate


ORM

Object Relational Mapping (ORM) — это концепция/процесс преобразования данных из объектно-ориентированного языка в реляционные БД и наоборот.

Например, в Java это делается с помощью рефлексии и JDBC.

JDBC (Java DataBase Connectivity) — API для работы с реляционными (зависимыми) БД.

Платформенно независимый промышленный стандарт взаимодействия Java-приложений с различными СУБД, реализованный в виде пакета java.sql, входящего в состав Java SE.

Предоставляет методы для получения и обновления данных. Не зависит от конкретного типа базы. Библиотека, которая входит в стандартную библиотеу, содержит: набор классов и интерфейсов для работы с БД (для нас разработчиков api) + интерфейсы баз данных.

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

JDBC Архитектура

JDBC Архитектура

JDBC Сущности

  1. Connection (класс) объект которого отвечает за соединение с базой и режим работы с ней.

  2. Statement (объект для оператора JDBC) используется для отправки SQL-оператора на сервер баз данных.

    Объект для оператора связан с объектом Connection и является объектом, обрабатывающим взаимодействие между приложением и сервером баз данных.

    Можно:

    • что-то поменять Update statement (create, delete, insert) в базе;
    • что-то запросить Query statement (select) из базы;

    Виды Statement-ов:

    • Statement (обычный) передаем в него либо Update, либо Query;
    • PreparedStatement - возможность сделать некий шаблон запроса, подставлять в него к-то значения и использовать его;
    • CallableStatement - предоставляет возможность вызова хранимой процедуры, расположенной на сервере, из Java™-приложения.
  3. ResultSet - объект с результатом запроса, который вернула база. Внутри него таблица.

Рефлексия

это API, который позволяет:

  • получать информацию о переменных, методах внутри класса, о самом классе, его конструкторах, реализованных интерфейсах и т.д.;
  • получать новый экземпляр класса;
  • получать доступ ко всем переменным и методам, в том числе приватным;
  • преобразовывать классы одного типа в другой (cast);
  • делать все это во время исполнения программы (динамически, в Runtime).

В Java есть специальный класс по имени Class. Поэтому его и называют классом класса. С помощью него осуществляется работа с рефлексией.


JPA

Java Persistence API (JPA) - это спецификация (стандарт, технология), обеспечивающая объектно-реляционное отображение простых JAVA-объектов (Plain Old Java Object - POJO) и предоставляющая универсальный API для сохранения, получения и управления такими объектами.

Сам JPA не умеет ни сохранять, ни управлять объектами, JPA только определяет правила игры: как должен действовать каждый провайдер (Hibernate, EclipseLink, OJB, Torque и т.д.), реализующий стандарт JPA. Для этого JPA определяет интерфейсы, которые должны быть реализованы провайдерами. Также JPA определяет правила, как должны описываться метаданные отображения и как должны работать провайдеры. Каждый провайдер обязан реализовывать всё из JPA, определяя стандартное получение, сохранение и управление объектами. Помимо этого, провайдеры могут добавлять свои личные классы и интерфейсы, расширяя функционал JPA.

JPA

  • API в пакете javax.persistance (набор интерфейсов EntityManager, Query, EntityTransaction);
  • JPQL - объектный язык запросов (запросы выполняются к объектам);
  • Metadata (аннотации или xml)

HIBERNATE

Hibernate - это провайдер, реализующий спецификацию JPA. Hibernate полностью реализует JPA плюс добавляет функционал в виде своих классов и интерфейсов, расширяя свои возможности по работе с сущностями и БД.

Что такое EntityManager?
Какие функции он выполняет?

EntityManager

Это интерфейс JPA, используемый для взаимодействия с персистентным контекстом.

EntityManager описывает API для всех основных операций над Entity, а также для получения данных и других сущностей JPA.

По сути - главный API для работы с JPA.

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

Персистентный контекст является своего рода кэшем данных в рамках транзакции - это и есть кэш первого уровня.

Внутри контекста персистентности происходит управление экземплярами сущностей и их жизненным циклом.

EntityManager автоматически сохраняет в БД все изменения, сделанные в его персистентном контексте, в момент коммита транзакции, либо при явном вызове метода flush().

Один или несколько EntityManager образуют или могут образовать persistence context.

Если проводить аналогию с обычным JDBC, то EntityManagerFactory будет аналогом DataSource, а EntityManager аналогом Connection.

Создание EntityManagerFactory довольно дорогая операция, поэтому обычно её создают один раз и на всё приложение. А чаще всего не создают сами, а делегируют это фреймворку, такому как Spring, например.

Интерфейс Session из Hibernate представлен в JPA как раз интерфейсом EntityManager.

JPA JDBC (по аналогии) Hibernate
EntityManagerFactory DataSource SessionFactory
EntityManager Connection Session
JPQL HQL

Основные функции EntityManager

  1. Операции над Entity:

    • persist (добавление Entity под управление JPA)
    • merge (изменение)
    • remove (удаление)
    • refresh (обновление данных)
    • detach (удаление из-под управления контекста персистентности)
    • lock (блокирование Entity от изменений в других thread)
  2. Получение данных:

    • find (поиск и получение Entity)
    • createQuery
    • createNamedQuery
    • createNativeQuery
    • contains
    • createNamedStoredProcedureQuery
    • createStoredProcedureQuery
  3. Получение других сущностей JPA

    • getTransaction
    • getEntityManagerFactory,
    • getCriteriaBuilder
    • getMetamodel
    • getDelegate
  4. Работа с EntityGraph

    • createEntityGraph
    • getEntityGraph
  5. Общие операции

    • close
    • isOpen
    • getProperties
    • setProperty
    • clear

Объекты EntityManager не являются потокобезопасными.

Это означает, что каждый поток должен получить свой экземпляр EntityManager, поработать с ним и закрыть его в конце.

Каким условиям должен удовлетворять класс, чтобы являться Entity?

Entity

Требования к Entity классу в JPA

Требования к Entity классу в Hibernate


Entity

Сущность (entity) - это объект персистентной области.

Как правило, сущность представляет таблицу в реляционной базе данных, и каждый экземпляр сущности соответствует строке в этой таблице.

Основным программным представлением сущности является класс сущности.

Класс сущности может использовать другие классы, которые служат вспомогательными классами или используются для представления состояния сущности (например embedded).

Персистентное состояние сущности представлено персистентными полями или персистентными свойствами.

Персистентное поле - поле сущности, которое отражается в БД в виде столбца таблицы.

Персистентное свойство - это методы, которые аннотированы вместо полей для доступа провайдера к ним (полям).

Эти поля или свойства используют аннотации объектно-реляционного сопоставления (маппинга) для сопоставления сущностей и отношений между ними с реляционными данными в хранилище данных.

Примеры аннотаций: @OneToOne, @OneToMany, @ManyToOne, @ManyToMany.

Есть два вида доступа к состоянию сущности:

В JPA принято называть эти персистентные поля и свойства атрибутами класса-сущности.


Требования к Entity классу в JPA


Требования к Entity классу в Hibernate

Hibernate не так строг в своих требованиях. Вот отличия от требований JPA:

  1. Класс сущности должен иметь конструктор без аргументов, который может быть не только public или protected, но и package visibility (default).
  2. Класс сущности необязательно должен быть классом верхнего уровня.
  3. Технически Hibernate может сохранять финальные классы или классы с финальными методами (getter / setter).

    Однако, как правило, это не очень хорошая идея, так как это лишит Hibernate возможности генерировать прокси для отложенной загрузки сущности.

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

    Однако обоснованность такого подхода спорна.

Может ли абстрактный класс быть Entity?

Абстрактный класс может быть Entity классом.

Абстрактный Entity класс отличается от обычных Entity классов только тем, что нельзя создать объект этого класса. Имена абстрактных классов могут использоваться в запросах.

Абстрактные Entity классы используются в наследовании, когда их потомки наследуют поля абстрактного класса:

@Entity

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)

public abstract class Employee {

@Id

@GeneratedValue

private long id;

private String name;

...

}

@Entity

@Table(name = "FULL_TIME_EMP")

public class FullTimeEmployee extends Employee

private int salary;

...

}

@Entity

@Table(name = "PART_TIME_EMP")

public class PartTimeEmployee extends Employee {

private int hourlyRate;

...

}

Наследование Entity классов.

Таблица вариантов наследования Entity классов:

Родитель/Наследник Entity класс не Entity класс
Entity класс + +
Entity класс + обычное наследование в Java

Может ли Entity класс наследоваться от не Entity классов (non-entity classes)?

Да, сущности могут наследоваться от не Entity классов, которые, в свою очередь, могут быть как абстрактными, так и обычными.

Состояние (поля) не Entity суперкласса не является персистентным, то есть не хранится в БД и не обрабатывается провайдером (Hibernate), поэтому любое такое состояние (поля), унаследованное Entity классом, также не будет отображаться в БД.

Не Entity суперклассы не могут участвовать в операциях EntityManager или Query. Любые маппинги или аннотации отношений в не Entity суперклассах игнорируются.


Может ли Entity класс наследоваться от Entity классов?

Да, может.


Может ли не Entity класс наследоваться от Entity класса?

Да, может.

Что такое встраиваемый (Embeddable) класс?
Какие требования JPA предъявляет к встраиваемым (Embeddable) классам?

Описание

Особенности

Требования


Встраиваемый (Embeddable) класс

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

Hibernate называет эти классы компонентами.

JPA называет их встраиваемыми.

В любом случае, концепция одна и та же: композиция значений.

Встраиваемый класс помечается аннотацией @Embeddable.

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

То есть, если класс Person с полями name и age встроен и в класс Driver, и в класс Baker, то у обоих последних классов появятся оба поля из класса Person. Но если у объекта Driver эти поля будут иметь значения "Иван" и "35", то эти же поля у объекта Baker могут иметь совершенно иные значения, никак не связанные с объектом Driver.

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


Особенности встраиваемых классов


Требования к встраиваемым классам

Например, у нас может быть встраиваемый класс ClassA, который представляет собой композицию строкового и числового значений, и эти два поля будут добавлены в класс EntityA:

@Entity

public class EntityA {

@Id

@GeneratedValue

private int id;

...

@Embedded

private ClassA classARef;

...

}

@Embeddable

public class ClassA {

private String myStr;

private int myInt;

...

}

Embeddable

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

Например, у класса Driver поля из встраиваемого класса Person будут изменены с name на driver_name и с age на driver_age:

@Embeddable

public class Person {

private String name;

name; private int age;

}

@Entity

public class Driver {

@Embedded

@AttributeOverrides({

@AttributeOverride(name = "name", column = @Column(name = "driver_name")),

@AttributeOverride(name = "age", column = @Column(name = "driver_age")),

})

private Person person;

...

}

Сущности, которые имеют встраиваемые классы, могут аннотировать поле или свойство аннотацией @Embedded, но не обязаны это делать.

Что такое Mapped Superclass?

Определение

Особенности

Mapped Superclass vs. Embeddable class


Mapped Superclass

Mapped Superclass (сопоставленный суперкласс) - это класс, от которого наследуются Entity, он может содержать аннотации JPA, однако сам такой класс не является Entity, ему не обязательно выполнять все требования, установленные для Entity (например, он может не содержать первичного ключа).

Эти суперклассы чаще всего используются, когда у нас есть общая для нескольких классов сущностей информация о состоянии и отображении, которую можно вынести в Mapped Superclass.


Особенности Mapped Superclass

Для того, чтобы использовать Mapped Superclass, достаточно унаследовать его в классах-потомках:

@MappedSuperclass

public class Employee {

@Id

@GeneratedValue

private long id;

private String name;

...

}

@Entity

@Table(name = "FULL_TIME_EMP")

public class FullTimeEmployee extends Employee {

private int salary;

...

}

@Entity

@Table(name = "PART_TIME_EMP")

public class PartTimeEmployee extends Employee {

private int hourlyRate;

...

}

В указанном примере кода в БД будут таблицы FULL_TIME_EMPLOYEE и PART_TIME_EMPLOYEE, но таблицы EMPLOYEE не будет:

MappedSuperclass

Это похоже на стратегию наследования “Таблица для каждого конкретного класса сущностей”, но в модели данных нет объединения таблиц или наследования. Также тут нет таблицы для Mapped Superclass. Наследование существует только в объектной модели.

Основным недостатком использования сопоставленного суперкласса является то, что полиморфные запросы невозможны, то есть мы не можем загрузить всех наследников Mapped Superclass.


Mapped Superclass vs. Embeddable class

Сходства:

Различия:

Какие три стратегии маппинга при наследовании сущностей описаны в JPA?

SINGLE_TABLE

JOINED

TABLE_PER_CLASS


Entity Inheritance Mapping Strategies

Стратегии наследования нужны для того, чтобы дать понять провайдеру (Hibernate) как ему отображать в БД сущности-наследники.

Для этого нам нужно декорировать родительский класс аннотацией @Inheritance и указать один из типов отображения.

Следующие три стратегии используются для отображения данных сущности-наследника и родительской сущности:

  1. SINGLE_TABLE

    Одна таблица на всю иерархию классов.

  2. JOINED

    Стратегия "соединения", при которой поля или свойства, специфичные для подклассов, отображаются в таблицах этих подклассов, а поля или свойства родительского класса отображаются в таблице родительского класса.

  3. TABLE_PER_CLASS

    Таблица для каждого конкретного класса сущностей.


SINGLE TABLE

Одна таблица на всю иерархию классов

Является стратегией по умолчанию и используется, когда аннотация @Inheritance не указана в родительском классе или когда она указана без конкретной стратегии.

SINGLE TABLE

Все entity, со всеми наследниками записываются в одну таблицу.

Для идентификации типа entity (наследника) определяется специальная колонка "discriminator column".

Минусом стратегии является невозможность применения ограничения NOT NULL для тех колонок таблицы, которые характерны только для классов-наследников.

Например, если есть entity Employee c классами-потомками FullTimeEmployee и PartTimeEmployee, то при такой стратегии все FullTimeEmployee и PartTimeEmployee записываются в таблицу Employee, и при этом в таблице появляется дополнительная колонка с именем DTYPE, в которой будут записаны значения, определяющие принадлежность к классу.

По умолчанию эти значения формируются из имён классов, в нашем случае - либо «FullTimeEmployee» либо «PartTimeEmployee».

Но мы можем их поменять в аннотации у каждого класса-наследника: @DiscriminatorValue("F").

Если мы хотим поменять имя колонки, то мы должны указать её новое имя в параметре аннотации у класса-родителя: @DiscriminatorColumn(name=EMP_TYPE) .

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

@Entity

@DiscriminatorColumn(name = "EMP_TYPE")

public class Employee {

@Id

@GeneratedValue

private long id;

private String name;

}

@Entity

@DiscriminatorValue("F")

public class FullTimeEmployee extends Employee {

private int salary;

}

@Entity

@DiscriminatorValue("P")

public class PartTimeEmployee extends Employee {

private int hourlyRate;

}

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

-- Persisting entities --

FullTimeEmployee{id=0, name='Sara', salary=100000}

PartTimeEmployee{id=0, name='Tom', hourlyRate='60'}

-- Native queries --

'Select * from Employee'

[F, 1, Sara, null, 100000]

[P, 2, Tom, 60, null]

-- Loading entities --

FullTimeEmployee{id=1, name='Sara', salary=100000}

PartTimeEmployee{id=2, name='Tom', hourlyRate='60'}


JOINED

Стратегия "соединения"

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

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

Столбец первичного ключа в таблице подкласса служит внешним ключом первичного ключа таблицы суперкласса.

Также в таблице родительского класса добавляется столбец DiscriminatorColumn с DiscriminatorValue для определения типа наследника.

JOINED

@Inheritance(strategy = InheritanceType.JOINED)

@Entity

@DiscriminatorColumn(name = "EMP_TYPE") //определение типа наследника

public class Employee {

@Id

@GeneratedValue

private long id;

private String name;

...

}

@Entity

@DiscriminatorValue("F")

@Table(name = "FULL_TIME_EMP")

public class FullTimeEmployee extends Employee {

private int salary;

...

}

@Entity

@DiscriminatorValue("P")

@Table(name = "PART_TIME_EMP")

public class PartTimeEmployee extends Employee {

private int hourlyRate;

...

}

-- Persisting entities --

FullTimeEmployee{id=0, name='Sara', salary=100000}

PartTimeEmployee{id=0, name='Robert', hourlyRate='60'}

-- Native queries --

'Select * from Employee'

[F, 1, Sara]

[P, 2, Robert]

'Select * from FULL_TIME_EMP'

[100000, 1]

'Select * from PART_TIME_EMP'

[60, 2]

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

В глубоких иерархиях классов это может привести к недопустимому снижению производительности.

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

-- Loading entities --

List<Employee> entityAList = em

.createQuery("Select t from Employee t")

.getResultList();

// Hibernate создает соединения для сборки объектов

-- Result --

FullTimeEmployee{id=1, name='Sara', salary=100000}

PartTimeEmployee{id=2, name='Robert', hourlyRate='60'}


TABLE PER CLASS

Таблица для каждого класса сущностей

Каждый класс-наследник имеет свою таблицу. Во всех таблицах подклассов хранятся все поля этого класса плюс те, которые унаследованы от суперкласса.

TABLE PER CLASS

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)

@Entity

public class Employee {

@Id

@GeneratedValue

private long id;

private String name;

...

}

@Entity

@Table(name = "FULL_TIME_EMP")

public class FullTimeEmployee extends Employee {

private int salary;

...

}

@Entity

@Table(name = "PART_TIME_EMP")

public class PartTimeEmployee extends Employee {

private int hourlyRate;

...

}

-- Persisting entities --

FullTimeEmployee{id=0, name='Sara', salary=100000}

PartTimeEmployee{id=0, name='Robert', hourlyRate='60'}

-- Native queries --

'Select * from Employee'

no data

'Select * from FULL_TIME_EMP'

[1, Sara, 100000]

'Select * from PART_TIME_EMP'

[2, Robert, 60]

-- Loading entities --

List<Employee> entityAList = em

.createQuery("Select t from Employee t")

.getResultList();

// Hibernate выполнит дополнительные sql-запросы

// или запросы объединения для получения сущностей

-- Result ---

FullTimeEmployee{id=1, name='Sara', salary=100000}

PartTimeEmployee{id=2, name='Robert', hourlyRate='60'}

Минусом является плохая поддержка полиморфизма (polymorphic relationships) и то, что для выборки всех классов иерархии потребуется большое количество отдельных sql-запросов для каждой таблицы-наследника или использование UNION-запроса для соединения таблиц всех наследников в одну таблицу.

Также недостатком этой стратегии является повторение одних и тех же атрибутов в таблицах.

При TABLE PER CLASS не работает стратегия генератора первичных ключей IDENTITY, поскольку может быть несколько объектов подкласса, имеющих один и тот же идентификатор, и запрос базового класса приведет к получению объектов с одним и тем же идентификатором (даже если они принадлежат разным типам).

Как мапятся Enum'ы?

По порядковым номерам

По именам

Методы обратного вызова

Converter


По порядковым номерам

Если мы сохраняем в БД сущность, у которой есть поле-перечисление (Enum), то в таблице этой сущности создаётся колонка для значений этого перечисления и по умолчанию в ячейки сохраняется порядковый номер этого перечисления (ordinal).

public enum MyEnum {

ConstA,

ConstB,

ConstC;

}

@Entity

public class MyEntity {

@Id

private long myId;

private MyEnum myEnum;

public MyEntity() {

}

public MyEntity(long myId, MyEnum myEnum) {

this.myId = myId;

this.myEnum = myEnum;

}

...

}

В JPA типы Enum могут быть помечены аннотацией @Enumerated, которая может принимать в качестве атрибута EnumType.ORDINAL или EnumType.STRING, определяющий, отображается ли перечисление (enum) на столбец с типом Integer или String соответственно.

@Enumerated(EnumType.ORDINAL) - значение по умолчанию, говорит о том, что в базе будут храниться порядковые номера Enum (0, 1, 2…).

Проблема с этим типом отображения возникает, когда нам нужно изменить наш Enum.

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

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


По именам

@Enumerated(EnumType.STRING) - означает, что в базе будут храниться имена Enum.

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

Однако переименование значения enum все равно нарушит работу базы данных.

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

Это может оказаться серьезной проблемой, когда нам нужно иметь дело с большим объемом данных.


@PostLoad и @PrePersist

Другой вариант - использование стандартных методов обратного вызова из JPA.

Мы можем смапить наши перечисления в БД и обратно в методах с аннотациями @PostLoad и @PrePersist.

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

Создадим Enum с полем priority содержащем числовое значение приоритета:

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();

}

}

}

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

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


Converter

В JPA с версии 2.1 можно использовать Converter для конвертации Enum’а в некое его значение для сохранения в БД и получения из БД.

Все, что нам нужно сделать, это создать новый класс, который реализует 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;

}

}

@Entity

public class Article {

@Id

private int id;

private String title;

@Basic

private int priorityValue;

@Transient

private Priority priority;

private Category category;

}

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

}

}

Мы установили @Converter(autoApply=true), чтобы JPA автоматически применял логику преобразования ко всем сопоставленным атрибутам типа Category.

В противном случае нам пришлось бы поместить аннотацию @Converter непосредственно над полем Category у каждой сущности, где оно имеется.

В результате в столбце таблицы будут храниться значения: "S", "M" или "T".

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

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

Это решение просто в реализации и устраняет все недостатки с @Enumerated(EnumType.ORDINAL), @Enumerated(EnumType.STRING) и методами обратного вызова.