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) и методами обратного вызова.

Как мапятся даты (до Java 8 и после)?

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

Другой способ - настроить свойство hibernate.jdbc.time_zone в файле свойств Hibernate, который используется для создания фабрики сессий. Таким образом, мы можем указать часовой пояс один раз для всего приложения.

java.sql

java.util

java.time


java.sql

Hibernate позволяет отображать различные классы даты/времени из Java в таблицах баз данных.

Стандарт SQL определяет три типа даты/времени:

  1. DATE- Представляет календарную дату путем хранения лет, месяцев и дней.

    Эквивалентом JDBC является java.sql.Date.

  2. TIME - Представляет время дня и хранит часы, минуты и секунды.

    Эквивалентом JDBC является java.sql.Time.

  3. TIMESTAMP - Хранит как DATE, так и TIME плюс наносекунды.

    Эквивалентом JDBC является java.sql.Timestamp.

Поскольку эти типы соответствуют SQL, их сопоставление относительно простое. Мы можем использовать аннотацию @Basic или @Column:

@Entity

public class TemporalValues {

@Basic

private java.sql.Date sqlDate ;

@Basic

private java.sql.Time sqlTime ;

@Basic

private java.sql.Timestamp sqlTimestamp ;

}

Затем мы могли бы установить соответствующие значения следующим образом:

temporalValues.setSqlDate(java.sql.Date.valueOf("2017-11-15"));

temporalValues.setSqlTime(java.sql.Time.valueOf("15:30:14"));

temporalValues.setSqlTimestamp(java.sql.Timestamp.valueOf("2017-11-15 15:30:14.332"));

Обратите внимание, что использование типов java.sql для полей сущностей не всегда может быть хорошим выбором.

Эти классы специфичны для JDBC и содержат множество устаревших функций.

Чтобы избежать зависимостей от пакета java.sql, начали использовать классы даты/времени из пакета java.util вместо классов java.sql.Timestamp и java.sql.Time.


java.util

Точность представления времени составляет одна миллисекунда.

Для большинства практических задач этого более чем достаточно, но иногда хочется иметь точность повыше.

Поскольку классы в данном API изменяемые (не immutable), использовать их в многопоточной среде нужно с осторожностью.

В частности java.util.Date можно признать «эффективно» потоко-безопасным, если вы не вызываете у него устаревшие методы.

java.util.Date

Тип java.util.Date содержит информацию о дате и времени с точностью до миллисекунд.

Но так как классы из этого пакета не имели прямого соответствия типам данных SQL, приходилось использовать над полями java.util.Date аннотацию @Temporal, чтобы дать понять SQL, с каким конкретно типом данных она работает.

Для этого у аннотации @Temporal нужно было указать параметр TemporalType , который принимал одно из трёх значений:
  • DATE
  • TIME
  • TIMESTAMP
что позволяло указать базе данных с какими конкретными типами данных она работает.

@Basic

@Temporal(TemporalType.DATE)

private java.util.Date utilDate;

@Basic

@Temporal(TemporalType.TIME)

private java.util.Date utilTime;

@Basic

@Temporal(TemporalType.TIMESTAMP)

private java.util.Date utilTimestamp;

Тип java.util.Date имеет точность до миллисекунд, и недостаточно точен для обработки SQL-значения Timestamp, который имеет точность вплоть до наносекунд.

Поэтому, когда мы извлекаем сущность из базы данных, неудивительно, что в этом поле мы находим экземпляр java.sql.Timestamp, даже если изначально мы сохранили java.util.Date.

Но это не страшно, так как Timestamp наследуется от Date.

java.util.Calendar

Как и в случае java.util.Date, тип java.util.Calendar может быть сопоставлен с различными типами SQL, поэтому мы должны указать их с помощью @Temporal.

Разница лишь в том, что Hibernate не поддерживает отображение (маппинг) Calendar на TIME:

@Basic

@Temporal(TemporalType.DATE)

private java.util.Calendar calendarDate;

@Basic

@Temporal(TemporalType.TIMESTAMP)

private java.util.Calendar calendarTimestamp;


java.time

Начиная с Java 8, доступен новый API даты и времени для работы с временными значениями.

Этот API-интерфейс устраняет многие проблемы классов java.util.Date и java.util.Calendar.

Все классы в новом API неизменяемые (immutable) и, как следствие, потоко-безопасные.

Точность представления времени составляет одна наносекунда, что в миллион раз точнее чем в пакете java.util.

Типы данных из пакета java.time напрямую отображаются (маппятся) на соответствующие типы SQL.

Поэтому нет необходимости явно указывать аннотацию @Temporal:

  1. LocalDate соответствует DATE.
  2. LocalTime и OffsetTime соответствуют TIME.
  3. Instant, LocalDateTime, OffsetDateTime и ZonedDateTime соответствуют TIMESTAMP.

Это означает, что мы можем пометить эти поля только аннотацией @Basic (или @Column), например:

@Basic

private java.time.LocalDate localDate;

@Basic

private java.time.LocalTime localTime;

@Basic

private java.time.OffsetTime offsetTime;

@Basic

private java.time.Instant instant;

@Basic

private java.time.LocalDateTime localDateTime;

@Basic

private java.time.OffsetDateTime offsetDateTime;

@Basic

private java.time.ZonedDateTime zonedDateTime;

Каждый временной класс в пакете java.time имеет статический метод parse() для анализа предоставленного значения типа String с использованием соответствующего формата.

Вот как мы можем установить значения полей сущности:

temporalValues.setLocalDate(LocalDate.parse("2017-11-15"));

temporalValues.setLocalTime(LocalTime.parse("15:30:18"));

temporalValues.setOffsetTime(OffsetTime.parse("08:22:12+01:00"));

temporalValues.setInstant(Instant.parse("2017-11-15T08:22:12Z"));

temporalValues.setLocalDateTime(LocalDateTime.parse("2017-11-15T08:22:12"));

temporalValues.setOffsetDateTime(OffsetDateTime.parse("2017-11-15T08:22:12+01:00"));

temporalValues.setZonedDateTime( ZonedDateTime.parse("2017-11-15T08:22:12+01:00[Europe/Paris]"));

Как сохранять в базе данных коллекции базовых типов?

Если у нашей сущности есть поле с коллекцией, то мы привыкли ставить над ним аннотации @OneToMany либо @ManyToMany. Но данные аннотации применяются в случае, когда это коллекция других сущностей (entities).

Но что, если у нашей сущности коллекция не других сущностей, а базовых или встраиваемых (embeddable) типов, то есть коллекция элементов?

Для этих случаев в JPA имеется специальная аннотация @ElementCollection, которая указывается в классе сущности над полем коллекции базовых или встраиваемых типов.

Все записи коллекции хранятся в отдельной таблице, то есть в итоге получаем две таблицы:
  1. одну для сущности,
  2. вторую для коллекции элементов.

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

Base type collection mapping

@Entity

public class Customer {

@Id

@GeneratedValue

private int id;

private String name;

@ElementCollection

private List <String> phoneNumbers;

.............

}

Аннотация @ElementCollection похожа на отношение @OneToMany, за исключением того, что целью являются базовые и встраиваемые типы, а не сущности.

Можно использовать аннотации @AttributeOverrides и @AttributeOverride для настройки отображения в таблице полей базовых или встраиваемых типов.

Коллекции могут иметь тип java.util.Map, которые состоят из ключа и значения.

Для этого типа коллекций применяются следующие правила:

  1. Ключ или значение Map может быть базовым типом языка программирования Java, встраиваемым классом или сущностью.

  2. Если значение Map является встраиваемым классом или базовым типом, используйте аннотацию @ElementCollection.

    пример

  3. Если значение Map является сущностью, используйте аннотацию @OneToMany или @ManyToMany.

    пример

  4. Использовать тип Map только на одной стороне двунаправленной связи.

Аннотация @MapKeyColumn позволяет настроить столбец «ключ» в таблице Map.

Аннотация @Column позволяет настроить столбец «значение» в таблице Map.

пример

Использование коллекций элементов имеет один большой недостаток:

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

То есть при добавлении одного элемента в коллекцию Hibernate не добавит одну строку в таблицу коллекции, а очистит её и заполнит по новой всеми элементами.

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

Во всех других случаях рекомендуется использовать коллекции сущностей с @OneToMany

Какие существуют виды связей?

Множественность в отношениях сущностей

Направление в отношениях сущностей

Запросы и направление отношений


Множественность в отношениях сущностей

Существуют следующие четыре типа связей между сущностями:

  1. OneToOne - когда один экземпляр Entity может быть связан не больше чем с одним экземпляром другого Entity.

  2. OneToMany - когда один экземпляр Entity может быть связан с несколькими экземплярами других Entity.

  3. ManyToOne - обратная связь для OneToMany. Несколько экземпляров Entity могут быть связаны с одним экземпляром другого Entity.

  4. ManyToMany - экземпляры Entity могут быть связаны с несколькими экземплярами друг друга.


Направление в отношениях сущностей

Направление отношений может быть как двунаправленным, так и однонаправленным.

Сторона-владелец отношения определяет, как среда выполнения Persistence обновляет отношение в базе данных.

Двунаправленные отношения имеют как сторону-владельца, так и владеемую сторону.

Однонаправленные отношения имеют только сторону-владельца.

Двунаправленные отношения

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

Двунаправленные отношения должны следовать следующим правилам:

  1. Владеемая сторона в двунаправленных отношениях должна ссылаться на владеющую сторону используя элемент mappedBy аннотаций @OneToOne, @OneToMany, или @ManyToMany.

    Если применить атрибут mappedBy на одной стороне связи, то Hibernate не станет создавать сводную таблицу:

    @Entity

    @Table(name="CART")

    public class Cart {

    //...

    @OneToMany(mappedBy="cart")

    private Set<Items> items;

    // getters and setters

    }

    @Entity

    @Table(name="ITEMS")

    public class Items {

    //...

    @ManyToOne

    @JoinColumn(name="cart_id", nullable=false)

    private Cart cart;

    public Items() {}

    // getters and setters

    }

    В данном примере таблица класса Items является владеющей стороной и будет иметь колонку с внешними ключами на таблицу Cart. Таблица класса Cart будет владеемой.

  2. Сторона many в отношениях many-to-one всегда является владельцем отношений и не может определять элемент mappedBy (такого параметра у аннотации @ManyToOne просто нет).
  3. Для двунаправленных отношений one-to-one, сторона-владелец это та сторона, чья таблица имеет столбец с внешним ключом на другую таблицу.

    Если не указан параметр mappedBy, то колонки с айдишниками появляются у каждой таблицы.

  4. Для двунаправленных отношений many-to-many, любая сторона может быть стороной-владельцем.

Однонаправленные отношения

В однонаправленных отношениях только одна сущность имеет поле, которое ссылается на вторую сущность.

Вторая сущность (сторона) не имеет поля первой сущности и не знает об отношениях.


Запросы и направление отношений

Язык запросов Java Persistence и запросы API Criteria часто перемещаются между отношениями.

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

Например:

Что такое "владелец связи"?

Владелец связи и владеемый

Родительская сущность (таблица)

Дочерняя сущность (таблица)

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


Владелец связи и владеемый

По сути, у кого есть внешний ключ на другую сущность - тот и владелец связи. То есть, если в таблице одной сущности есть колонка, содержащая внешние ключи от другой сущности, то первая сущность признаётся владельцем связи, вторая сущность - владеемой.

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

@Entity
public class LineItem {
    @Id
    private Long id
    @OneToOne
    private Product product;
}

@Entity
public class Product {
    @Id
    private Long id;
    private String name;
    private Double price;
}

Родительская сущность (таблица)

Это сущность (таблица), на которую ссылается внешний ключ из дочерней сущности (таблицы).


Дочерняя сущность (таблица)

Это сущность (таблица), в которой есть колонка с внешним ключом, ссылающимся на родительскую сущность (таблицу).

Что такое каскадные операции?

Каскадные операции и отношения

Удаление сирот в отношениях (Orphan Removal)


Каскадные операции и отношения

Сущности, между которыми есть отношения, часто зависят от существования друг друга.

Например, позиции (LineItem) являются частью заказа (CustomerOrder), и если заказ удален, все позиции также должны быть удалены.

Это называется - каскадным удалением.

JPA позволяет распространять операции с сущностями (например, persist или remove) на связанные сущности. Это означает, что при включенном каскадировании, если сущность A сохраняется или удаляется, тогда сущность B (связанная с A отношением, например через ManyToOne) также будет сохраняться или удаляться без явных команд сохранения или удаления.

Каскадирования можно добиться, указав у любой из аннотаций @OneToOne, @ManyToOne, @OneToMany, @ManyToMany элемент cascade и присвоив ему одно или несколько значений из перечисления CascadeType (ALL, DETACH, MERGE, PERSIST, REFRESH, REMOVE).

Как правило каскадные операции применяются от родительской сущности к дочерним, но они могут распространяться и в обратном направлении - от дочерней к родительской.

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


Удаление сирот в отношениях (Orphan Removal)

Представим, что у нас есть класс Customer, у которого есть коллекция Order:

@Entity
public class Customer {
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();
    // other mappings, getters and setters
}

Пусть у нас есть один объект Customer - родитель, в коллекции которого есть 4 объекта Order - дети, и мы установили атрибут orphanRemoval = true над этой коллекцией. В нашей базе данных в таблице Customer будет одна строка, а в таблице Order будет четыре строки. Также в таблице Order будет колонка с внешними ключами на таблицу Customer. В каждой из четырех ячеек этой колонки будут ссылки на один и тот же первичный ключ объекта Customer.

Например, мы удалим из коллекции orders один объект Order - любой из четырех, в результате чего у объекта Customer останется три объекта Order:

Customer customer = entityManager.find(Customer.class, 1L);
Order order = customer.getOrders().get(0);
customer.getOrders().remove(order);
flushAndClear();

После запуска метода flushAndClear() - обновления объекта Customer отправятся в базу данных, и произойдет следующее:

  1. Hibernate заметит, что у объекта Customer уже не 4, а 3 связанных дочерних объекта Order;
  2. в связи с этим Hibernate найдёт в таблице Order строку с удаленным объектом из коллекции Order;
  3. очистит в этой строке ячейку с внешним ключом на Customer;
  4. после чего удалит саму эту строку, как осиротевшую (более не ссылающуюся на родителя).

Если не будет атрибута orphanRemoval = true, то пункт 4 не выполнится, и в таблице Order останется сущность Order, не связанная ни с одной сущностью Customer, то есть её ячейка с внешним ключом будет пустой.

Такая сущность будет считаться осиротевшей.

Какие типы fetch-стратегии в JPA вы знаете?

В JPA описаны два типа fetch-стратегии:

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

EAGER — данные поля будут загружены немедленно вместе с сущностью.

В Hibernate:

FetchType.EAGER
Hibernate должен сразу загрузить соответствующее аннотированное поле или свойство.
Это поведение по умолчанию для полей, аннотированных @Basic, @ManyToOne и @OneToOne (все что быстро).

FetchType.LAZY
Hibernate может загружать данные не сразу, а при первом обращении к ним, но так как это необязательное требование, то Hibernate имеет право изменить это поведение и загружать их сразу.
Это поведение по умолчанию для полей, аннотированных @OneToMany, @ManyToMany и @ElementCollection (все что медленно) .

Раньше у Hibernate все поля были LAZY, но в последних версиях - всё как в JPA 2.0.

Какие типы fetch-стратегии в JPA вы знаете?

Согласно JPA объект сущности может иметь один из четырех статусов жизненного цикла:

  1. new - объект создан, не имеет primary key, не является частью контекста персистентности (не управляется JPA);

  2. managed - объект создан, имеет primary key, является частью контекста персистентности (управляется JPA);

  3. detached - объект создан, имеет primary key, не является (или больше не является) частью контекста персистентности (не управляется JPA);

  4. removed - объект создан, является частью контекста персистентности (управляется JPA), будет удален при commit-е транзакции.

Как влияет операция на объекты Entity?

Операции:

persist()

remove()

merge()

refresh()

detach()


persist()

new → managed
объект будет сохранен в базу при commit-е транзакции или в результате flush-операции.

managed → {ignored}
операция игнорируется, однако связанные entity могут поменять статус на managed, если у них есть аннотации каскадных изменений.

removed → managed

detached → {exception}
исключение сразу или на этапе commit-а транзакции (так как у detached уже есть первичный ключ).


remove()

new → {ignored}
операция игнорируется, однако связанные entity могут поменять статус на removed, если у них есть аннотации каскадных изменений и они имели статус managed

managed → removed
запись в базе данных будет удалена при commit-е транзакции (также произойдут операции remove для всех каскадно зависимых объектов)

removed → {ignored}
операция игнорируется

detached → {exception}
исключение сразу или на этапе commit-а транзакции


merge()

new → ...
будет создана новая managed entity, в которую будут скопированы данные объекта.

managed → {ignored}
операция игнорируется, однако операция merge сработает на каскадно зависимых entity, если их статус не managed.

removed → {exception}
исключение сразу или на этапе commit-а транзакции.

detached → ...
либо данные будут скопированы в существующую БД managed entity с тем же первичным ключом,
либо создана новая managed entity, в которую скопируются данные.


refresh()

managed → ...
будут восстановлены все изменения из базы данных данного entity, также произойдет refresh всех каскадно зависимых объектов

new, removed, detached → {exception}
будет вызвано исключение.


detach()

managed, removed → detached

new, detached → {ignored}
операция игнорируется.