JPA - DmitryGontarenko/usefultricks GitHub Wiki

About

JPA (Java Persistence API) – спецификация, которая предоставляет возможность сохранять в удобном виде Java-объекты в базе данных. Спецификация является лишь описание Java API. Грубо говоря, в ней указано, какими средставами мы должны быть обеспечены (какие интерйсы мы должны реализовать), что бы реализовать концепцию ORM.
Саму реализацию таких средств спецификация не описывает. Это дает возможность использовать для одной спецификации разные реализации. Существует несколько реализаций этой спецификации, одна из самых популярных это Hibernate.

В данной теме будет рассмотрена реализация JPA с помощью Hibernate, а в качестве базы данных будет использоваться PostgreSQL и H2.
Это может отразится на тех или иных примерах.

Configuration

Для работы с JPA нам нужна непосредственно ее реализация, а также база данных, с объектами которой мы будем работать.
Возьмем для этих целей Hibernate и PostgreSQL:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.4.9.Final</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.12</version>
        </dependency>

Создадим Java-сущность:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int age;
}

Теперь нам необходимо сконфигурировать JPA.
В нем мы должны описать хотя бы один Persistence Unit (объект состояния) - это некая логическая группа, которая включает в себя описание конфигурации сущностей, на основе которой будет построена фабрика объектов EntityManagerFactory и менеджер объектов EntityManager; а также методанные (в формате аннотаций или XML), которые определяют отображение Java-классов в базе данных.

Описание конфигурации Persistence Unit определяется в файле persistence.xml, он должен быть расположен в каталоге src/main/resources/META-INF/.
Создадим такой файл для нашего примера:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="JpaExample">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <class>com.home.jpaconfig.Person</class>
        <properties>
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost:5432/jpatest"/>
            <property name="javax.persistence.jdbc.user" value="postgres"/>
            <property name="javax.persistence.jdbc.password" value="123"/>
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

persistence-unit.name - имя объекта состояния, используется для последующей идентификации в фабрике объектов EntityManagerFactory.
provider - в качестве провайдера указывается тот, кто конкретно реализует JPA (провайдер должен быть указан до перечисления классов).
class - по всем классам, добавленым в этот тэг, будут производиться поиск на предмет аннотаций @Entity, @Embeddable, @MappedSuperclass или @Converter, и любые аннотации методанных для маппинга с БД, найденные в этих классах, будут обработаны.
property - содержит набор стандартных JPA конфигураций. Но также может принимать и провайдер-специфичные конфигурации.

Управление сущностями (работа с БД) начинается с создания фабрики EntityManagerFactory, которая отвечает за маппинг объектов с БД, конфигурацию соеденения и т.д. Создание фабрики довольно дорогая операция, поэтому желательно создавать ее один раз на все приложение:

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("JpaExample");

В качестве параметра, который передается в метод createEntityManagerFactory, используется имя Persistence Unit, указанное в файле persistence.xml.

С помошью фабрики можно создавать объекты управления EntityManager, благодоря которым можно управлять сущностями:

 EntityManager entityManager = entityManagerFactory.createEntityManager();

При работе с JPA можно сохранять и получать объекты из базы данных.
Рассмотрим пример сохранения сущности в базу данных:

        // создаем фабрику и менеджер сущности
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("JpaExample");
        EntityManager entityManager = entityManagerFactory.createEntityManager();

        // создаем экземпляр сущности
        Person person = new Person();
        person.setName("John");
        person.setAge(23);

        // сохраням сущность в открытой транзакции
        entityManager.getTransaction().begin(); // открываем транзакцию
        entityManager.persist(person); // передаем сущностью менеджеру для управления
        entityManager.getTransaction().commit(); // фиксируем изменения

        // закрывам менеджер сущности
        entityManager.close();

Попробуем получить объект из базы данных:

        entityManager.getTransaction().begin();
        
        Person personSix = entityManager.find(Person.class, 10L);
        System.out.println(personSix); // Person{id=10, name='Sarah', age=18}
        List<Person> fromPerson = entityManager.createQuery("from Person", Person.class).getResultList();
        System.out.println(fromPerson); // [Person{id=10, name='Sarah', age=18}, Person{id=12, name='John', age=23} ... ]
        
        entityManager.getTransaction().commit();
        entityManager.close();

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

Entity Lifecycle

Любая сущность под управлением EntityManager имеет строго определенное состояние и строго определенные правила перехода из одного состояния в другое. Экземпляр сущности можно охарактеризовать как new, managed, detached или removed.

Рассмотрим операции EntityManager для управления сущностями подробнее:

  1. Когда будущая сущность создается с помощью оператора new, про нее EntityManager еще не знает. Такая сущность называется new.
        Person person = new Person();
  1. В состоянии new сущность для нас бесполезна, поэтому мы отдаем её под управление EntityManager. Такая сущность называется managed.
        entityManager.persist(person);

Пока сущность управляема, значения полей будут автоматически записываться в базу данных. При этом сущность можно инициализировать данными УЖЕ после того, как она стала управляемой, данные все равно будут записаны, например:

        Person person = new Person();
        entityManager.persist(person);
        person.setName("John");
        person.setAge(23);
//      OR
        Person person = new Person("Sarah", 18);
        entityManager.persist(person);
  1. Сущность находится в состоянии detached, когда теряет связь с EntityManager. Следовательно пропадет и синхронизация с БД.
    Переход в это состояние может произойти: при явном вызове метода - entityManager.detach(person), при закрытии объекта управления - entityManager.close(), сброса контекста управления entityManager.clear().
  2. Сущность может быть удалена и получить состояние removed. Что бы удалить обект из базы данных, он должен быть с начало получен, а затем удален в активной транзакции:
        Person personSeven = entityManager.find(Person.class, 7L);

        entityManager.getTransaction().begin();
        entityManager.remove(personSeven);
        entityManager.getTransaction().commit();

EntityManager

Identifier

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

Значение естественных ключей создаются естественным образом, при создании записи.
Например, если у нас будет таблица пользователей, которые идентифицируются по логину, этот логин и будет естественным первичным ключом.
Создаем таблицу:

create table tb_user (
	username varchar(255) primary key,
	password varchar(255) not null
);

Создаем сущность и устанавливаем идентификатор:

@Entity
@Table(name = "tb_user")
public class User {
    @Id
    private String username;
    @Column(nullable = false)
    private String password;
    ...

Суррогатные ключи необходимо заполнять уникальными значениями. Для этого в JPA предусмотрен механизм автоматической генерации сурргатных ключей, он определяется с помощью аннотации @GeneratedValue.
JPA поддерживает несколько стратегий генерации значений для суррогтаных ключей:

  • GenerationType.AUTO;
  • GenerationType.IDENTITY;
  • GenerationType.SEQUENCE;
  • GenerationType.TABLE;

Рассмотрим все стратегии по порядку, предварительно создав таблицу для тестирования:

create table Person (
	id serial primary key,
	name varchar(255)
);

При этом PostgreSQL создал для первичного ключа последовательность person_id_seq.

GenerationType.AUTO - стратегия AUTO подразумевая генерацию значения по умолчанию, позволяя JPA самостоятельно выбирать стретегию заполнения идетификатора. В случе использования Hibernate, будет выбрана стратегия на основе диалекта БД.
Данная стратегия используется в аннотации @GeneratedValue по умолчанию (указывать необязательно).

Пример:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    ...

GenerationType.IDENTITY - при использовании стратегии IDENTITY, база данных автоматически будет проставлять для идентификатора следующее значение, т.е. будет использован автоинкремент (при условии поддержки в БД).
Идентификатор в этом случае будет генерироваться не на стороне Java, а на стороне БД. Это ведет к тому, что пока строка не сохранена, идентификатор записи нельзя будет получить.

Пример:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    ...

В случае использования типа serial в PostgreSQL фактически создается последовательность (sequence) и если идентификатор строки заполнялся вручную - последовательность не будет это учитывать, и при попытке вставить новую запись в БД, используя стратегию IDENTITY, можно столкнуться с ошибкой:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "person_pkey" Detail: Key (id)=(5) already exists.
Следующее число в последовательности равно 5, но таблица уже имеет запись с таким идентификатором.

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

alter sequence person_id_seq restart with 10;

GenerationType.SEQUENCE - стратегия SEQUENCE использует объект последовательности (SEQUENCE) из базы данных для генерации идетификаторов.
При использовании данной стратегии будет задействована последовательность Hibernate по умолчанию - hibernate_sequence.
Пример:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    ...

Или же можно создать свою последовательность, указав ее с помощью аннотации @SequenceGenerator.
Создадим последовательность, которая начинается со 100 с шагом 5:

create sequence person_seq start 100 increment by 5; 

Применим ее для нашего идентификатора:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "person_generator")
    @SequenceGenerator(name = "person_generator", sequenceName = "person_seq", allocationSize = 5)
    private Long id;
    ...

Аннотация @SequenceGenerator позволяет определить имя генератора, имя и схему последовательности, а также начальне значение и диапазон присваемого значения.

GenerationType.TABLE - стратегия TABLE использует таблицу БД для генерации уникальных идентификаторов. В такой таблице должно быть два столбца - в одном хранится имя последовательности, в другом - последнее присвоенное значение идентификатора.
Для каждого объекта последовательности должна быть отдельная строка в таблице.
Последовательность таблиц является наболее "переносимымы" решением, поскольку использует обычную таблицу БД, и в отличии от стратегий SEQUENCE и IDENTITY, ее можно использовать в любой базе данных.

Рассмотрим на примере, для начала создадим таблицу в БД и проинициализируем ее объектом последовательности:

create table person_seq_store (
	seq_name varchar(255) primary key,
	seq_value bigint not null
);

insert into person_seq_store values ('PERSON.ID.PK', 0);

Затем применим ее для нашего идентификатора:

@Entity
public class Person {
    @Id
    @TableGenerator(name = "person_table_gen", table = "person_seq_store",
            pkColumnName = "seq_name", pkColumnValue = "PERSON.ID.PK",
            valueColumnName = "seq_value")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "person_table_gen")
    private Long id;
    ...

Аннотация @TableGenerator позволяет определить следующие параметры:

  • name - имя конкретного генератора;
  • table - имя таблицы, в которой хранятся значение последовательностей;
  • pkColumnName - имя столбца, в котором хранятся имена последовательностей;
  • pkColumnValue - имя конкретной последовательности;
  • valueColumnName - имя столбца, в котором хранятся значения последовательности.

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

Создадим таблицу с составным первичным ключем:

create table passport (
	series varchar(50),
	number varchar(50),
	name varchar(255),
	primary key(serial, number)
);

Далее в коде программы необходимо создать класс-ключа:

@EqualsAndHashCode
public class PassportKey implements Serializable {
    static final long serialVersionUID = 1L;
    private String series;
    private String number;
}

Такой класс должен отвечать требованиям JPA:

  • быть public;
  • иметь конструктор по умолчанию;
  • иметь перегруженные equals() и hashCode();
  • реализовывать интерфейс Serializable.

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

@Entity
@IdClass(PassportKey.class)
public class Passport {
    @Id
    private String series;
    @Id
    private String number;
    private String name;
    ...

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

Relationship

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

create table tb_address (
	id serial primary key,
	country varchar(255),
	city varchar(255)
);
create table tb_hobby (
	id serial primary key,
	name varchar(255)
);
create table tb_passport (
	id serial primary key,
	serial varchar (255),
	number varchar(255)
);
create table tb_person (
	id serial primary key,
	name varchar(255),
	address_id bigint,
	passport_id bigint,

	constraint fk_person_address foreign key (address_id) references tb_address (id),
	constraint fk_person_passport foreign key (passport_id) references tb_passport (id),
	constraint uk_person_passport unique (passport_id)
);
create table tb_crs_person_hobby (
	id serial primary key,
	person_id bigint,
	hobby_id bigint,
	
	constraint fk_crs_person foreign key (person_id) references tb_person (id),
	constraint fk_crs_hobby foreign key (hobby_id) references tb_hobby (id)
);

В этом примере главной таблицей является tb_person.
Таблица tb_person имеет связь 1:1 с таблицей tb_passport. Внешним ключом является поле tb_person.passport_id - оно уникально, потому что два человека не могут иметь одинаковый паспорт, такое условие и обеспечивает реализацию отношения 1:1.
Таблица tb_person имеет связь М:1 с таблицей address_id. Потому что люди могут иметь одинаковые адреса.
Таблица tb_crs_person_hobby является кросс-таблицей и обеспечивает связь М:М между таблицами tb_person и tb_hobby. Поле tb_crs_person_hobby.person_id связана М:1 с таблицей tb_person, а поле tb_crs_person_hobby.hobby_id связано М:1 с таблицей tb_hobby.

OneToOne

Для реализации связи один к одному используется аннотация @OneToOne.
Для начала создадим сущность Person:

@Entity
@Table(name = "tb_person")
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    @OneToOne(optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "passport_id")
    private Passport passport;
    ...

В данном случае владелец связи 1:1 - это поле passport объекта Person, к нему, помимо аннотации @OneToOne, добавляется аннотация @JoinColumn, в которой задается имя столбца.

Сущность Passport:

@Entity
@Table(name = "tb_passport")
public class Passport {
    @Id
    @GeneratedValue
    private Long id;
    @OneToOne(optional = false, mappedBy = "passport")
    private Person person;
    ...

Со стороны владеемого объекта (паспорта) не требуется указания имени столбца, но в аннотации @OneToOne требуется задать параметр mappedBy, в котором указывается имя поля в объекте-владельце (классе Person), которое ссылается на владеемый объект (то есть поле passport).

Делая запрос по идентификатору таблицы Person, получаем следующий результат:

        Person person = entityManager.find(Person.class, 1L);
        System.out.println(person); 

//        Person{id=1, name='John', 
//        passport=Passport{id=1, serial='342', number='2424'}}

ManyToOne

Для реализации связи многие к одному используется аннотация @ManyToOne.
Добавим ссылку на адрес к существующей сущности Person:

@Entity
@Table(name = "tb_person")
public class Person {
    ...
    @ManyToOne
    @JoinColumn(name = "address_id")
    private Address address;
    ...

В данном случае владелец связи М:1 - это поле address объекта Person, к нему, помимо аннотации @ManyToOne, добавляется аннотация @JoinColumn, в которой задается имя столбца.

Сущность Address:

@Entity
@Table(name = "tb_address")
public class Address {
    @Id
    @GeneratedValue
    private Long id;
    @OneToMany(mappedBy = "address", fetch = FetchType.EAGER)
    private List<Person> persons;
    ...

Со стороны владеемого объекта Address требуется указать аннотацию @OneToMany и задать параметр mappedBy, в котором указывается имя поля в объекте-владельце (классе Person), которое ссылается на владеемый объект.
Аннотация @OneToMany в этом случае применяется не к одному объекту (как было реализовано со связью 1:1), а к коллекции типа Person (владеющего объекта). Так как несколько людей могут иметь один и тот же адрес, следовательно для одного адреса может существовать коллекция/список из разных людей.

Сделаем запрос по идентификатору Person и Address:

        Person person = entityManager.find(Person.class, 1L);
        System.out.println(person);

//        Person{id=1, name='John', 
//        passport=Passport{id=1, serial='342', number='2424'}, 
//        address=Address{id=10, country='Russia', city='st.Petersburg'}}

        Address address = entityManager.find(Address.class, 10L);
        List<Person> persons = address.getPersons();
        System.out.println(persons);

//      [Person{id=1, name='John', ... }]},
//      Person{id=2, name='Kate', ... }]}]

Как видно на результате, сущность Person содержит только один адрес, а сущность Address - коллекцию из нескольких значений типа Person.

ManyToMany

Для реализации связи многие ко многим используется аннотация @ManyToMany.
Добавим ссылку на хобби к существующей сущности Person:

@Entity
@Table(name = "tb_person")
public class Person {
    ...
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "tb_crs_person_hobby",
        joinColumns = @JoinColumn(name = "person_id"),
        inverseJoinColumns = @JoinColumn(name = "hobby_id"))
    private List<Hobby> hobbies;
    ...

Связь М:М с обоих сторон представлена коллекцией объектов. Так как напрямую в реляционных базах данных такая связь не поддерживается, JPA реализует её с помощью кросс-таблицы, которая описывается аннотацией @JoinTable у объекта владельца. Параметр name задаёт имя кросс-таблицы, joinColumns — имя столбца, связанного с классом владельцем (Person), inverseJoinColumns — имя столбца, связанного с владеемым классом (Hobby).

Сущность Hobby:

@Entity
@Table(name = "tb_hobby")
public class Hobby {
    @Id
    @GeneratedValue
    @ManyToMany(mappedBy = "hobbies")
    private List<Person> persons;
    ...

Во владеемом классе аннотацией @ManyToMany должно отмечаться поле с коллекцией объектов класса владельца. Параметр mappedBy указывает на имя поля в классе владельце (Person), которое ссылается на владеемый объект.

Делая запрос по идентификатору таблицы Person, получаем следующий результат:

        Person person = entityManager.find(Person.class, 1L);
        System.out.println(person);

//        Person{id=1, name='John', passport=Passport{id=1, serial='342', number='2424'}, 
//        address=Address{id=10, country='Russia', city='st.Petersburg'}, 
//        hobbies=[Hobby{id=1, name='guitar'}, Hobby{id=2, name='chess'}, Hobby{id=3, name='languages'}]} 

Результат этого запроса полностью возвращает все данные из таблицы tb_person, а также из всех связанных с нею таблиц.

JPQL

JPQL (Java Persistence Query Language) - язык запросов, который позволяет создавать запросы к базе данных на основе модели сущностей. Его структура и синтаксис похожи на SQL.
При работе с БД JPQL преобразуется с помощью Hibernate или другой ORM в обычный SQL.

Сущность, используемая в примерах:

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;
    @OneToMany(mappedBy = "person", fetch = FetchType.EAGER)
    private List<Phone> phoneNumbers;
}

FROM
Что бы получить значение всех сущностей, можно воспользоваться сокращеный вариантом FROM - FROM Person.
Вариант целиком будет выглядеть так - SELECT p FROM Person p.
В запросе мы ссылаемся на сущность Person, а не на таблицу.

Пример:

        Query fromPerson = entityManager.createQuery("SELECT p FROM Person p");

WHERE
Для ограничения выбранных сущностей используется оператор WHERE.
Список функций, поддерживаемых WHERE на языке JPQL:

  • Операции сравнения - =, >, <, >=, <=, не равно <>;
  • BETWEEN - SELECT p FROM Person p WHERE p.age BETWEEN 18 AND 32;
  • LIKE - SELECT p FROM Person p WHERE p.name LIKE 'Jo%';
  • (NOT) IN - SELECT p FROM Person p WHERE p.name IN('Kate', 'John');
  • IS (NOT) NULL - проверяет поля на null SELECT p FROM Person p WHERE p.name IS NULL;
  • SIZE - проверят количество элементов в коллекции SELECT p FROM Person p WHERE SIZE(p.phoneNumbers) = 2;
  • IS (NOT) EMPTY - проверяет коллекцию на наличие значений SELECT p FROM Person p WHERE p.phoneNumbers IS NOT EMPTY;
  • (NOT) MEMBER OF - определяет, является ли значение частью коллекции SELECT p FROM Person p WHERE '111-111' MEMBER OF p.phoneNumbers;

SELECT
Для выборки полей сущности существует оператор SELECT, его работа схожа с аналогичным оператором в SQL.
Особенности SELECT на языке JPQL:

  • В JPQL запросе мы ссылаемся не на таблицу, а на сущность - SELECT p FROM Person p.
    И соответственно мы используем наименование полей сущности, а не столбцов таблицы - SELECT p.phoneNumbers FROM Person p.
  • В JPQL есть возможность использовать ссылку на конструктор - SELECT new com.home.jpa.model.Person(p.name, p.age) FROM Person p.
    В таком случае, вернутся только указанные в конструкторе поля.
  • С помощью оператора DISTINCT можно получить только уникальные значения полей - SELECT DISTINCT a.lastName FROM Author a.

ORDER BY
Оператор ORDER BY работает аналогично SQL оператору.
С помощью него можно проводить сортировку по возрастанию (ASC) или убыванию (DESC).
SELECT p FROM Person p ORDER BY p.name DESC.

GROUP BY, HAVING
Операторы GROUP BY и HAVING работают аналогично SQL операторам.
Подробнее об их возможностях можно почитать здесь.
SELECT p.name, MAX(p.age) FROM Person p GROUP BY p.name HAVING p.name LIKE 'Ka%'.

Others
Также JPQL поддерживает (LEFT) JOIN, Полиморфизм (ссылка на сущность будет содержать все ее подклассы) и Downcasting (с помощью оператора TREAT), Множество функций (математические функции, получение даты/времени, функции работы со строкой), Подзапросы (только с оператором WHERE) и Параметризацию параметров.

Annotations

Рассмотрим различные JPA аннотации.
Создадим таблицы в БД для тестовых примеров с помощью данного SQL скрипта:

create table tb_user (
  id serial primary key,
  username varchar(255),
  role varchar(255),
  last_updated timestamp,
  first_name varchar(50),
  last_name varchar(50)
);

create table tb_phone (
  id serial primary key,
  phone_number varchar(50),
  user_id bigint,

  constraint fk_phone_user foreign key (user_id) references tb_user (id)
);

@Temporal
С помощью аннотации @Temporal можно описать ожидаемую временную точность в базе данных. Временная точность представлена в трех типах - TIME, DATE и TIMESTAMP (т.е. время, дата или и то, и другое).
Данная аннотация может быть указана для полей типа Date или Calendar.

Создадим в сущности User поле с временным типом:

    @Column(name = "last_updated")
    @Temporal(TemporalType.DATE)
    private Date lastUpdated;

Теперь сохраним сущность в БД:

        User user = new User();
        user.setName("sarah.mc");
        user.setLastUpdated(new Date());
        entityManager.persist(user);

Так как в качестве аргумента мы указали тип TemporalType.DATE, в БД будет сохранена только дата, без времени - "2020-08-18 00:00:00".

@Enumerated
С помощью аннотации @Enumerated можно сохранять значения перечислений в базе данных.
Данная аннотация имеет обязательный параметр типа - EnumType.ORDINAL (по умолчанию) или EnumType.STRING.
В случае типа ORDINAL - в БД будет записан номер элемента из Enum (начиная с 0), а в случе STRING - будет записано само значение элемента.

Рассмотрим на примере. Создадим перечисление:

public enum Role {
    ADMIN, MOREDATOR, GUEST
}

Создадим в сущности User поле с типом созданного перечисления:

    @Enumerated(EnumType.STRING)
    private Role role;

Теперь сохраним сущность в БД:

        User user = new User();
        user.setName("sarah.mc");
        user.setRole(Role.ADMIN);
        entityManager.persist(user);

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

@ElementCollection, @CollectionTable
Аннотация @ElementCollection позволяет отображать в сущности несколько нестандартных коллекций. Ее можно использовать для определения отношения 1:М с коллекцией Embeddable-объектов или коллекцией базовых значений (String, Integer и т.д.)
Значения ElementCollection всегда хранятся в отдельной таблице, которая определяетсяс помощью аннотации @CollectionTable.

Описание встроенных объектов для сущности похоже на описание связи в аннотации @OneToMany, за исключением того, что целевая таблица является Embeddable-объектом, а не сущностью. Это позволяет проще определять коллекцию базовых типов, не требуя от них определяния обратного сопоставления (как в аннотации @ManyToOne).
Еще одним отличием ElementCollection от OneToMany является то, что целевые объекты нельзя получать или сохранять напрямую независимо от их родительского объекта. Отсутствует политика каскадирования, целевые объекты всегда сохраняются, изменяются и удаляются вместе со своимм родителями.

Рассмотрим на примере.
Создаем класс Phone и помечаем его аннотацией @Embeddable:

@Embeddable
public class Phone {
    @Column(name = "phone_number")
    private String phoneNumber;
}

Добавляем в нашу сущность User коллекцию типа Phone:

    @ElementCollection
    @CollectionTable(name = "tb_phone", joinColumns = @JoinColumn(name = "user_id"))
    private List<Phone> phones;

Для коллекции можно указать и базовый тип, например:

    @ElementCollection
    @CollectionTable(name = "tb_phone", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "phone_number")
    private List<String> phones;

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

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

        User user = new User();
        user.setName("Sarah");
        user.setPhones(Collections.singletonList(new Phone("88002004322")));
        entityManager.persist(user);

/* Логи Hibernate:
Hibernate: insert into tb_user (last_updated, username, role) values (?, ?, ?)
Hibernate: insert into tb_phone (user_id, phone_number) values (?, ?)
*/

Попробуем удалить сущность:

        User user = entityManager.find(User.class, 3L);
        entityManager.remove(user);

/* Логи Hibernate:
Hibernate: select user0_.id as id1_2_0_, user0_.last_updated as last_upd2_2_0_, user0_.username as username3_2_0_, user0_.role as role4_2_0_ from tb_user user0_ where user0_.id=?
Hibernate: delete from tb_phone where user_id=?
Hibernate: delete from tb_user where id=? 
*/

Как видно по логам Hibernate, при любых изменениях сущности (родительского объекта) User, меняется и ее встроенный объект Phone.

@Embeddable, @Embedded
С помощью аннотации @Embeddable класс можно объявить встроенным и использовать в какой-либо сущности.
Для указания в сущности поля, которое имеет встроенный тип, применяется аннотация @Embedded.
Встраиваемые объекты имеют другие требования по сравнению с объектами Entity. Встраиваемый объект нельзя получить или сохранить напрямую независимо от родительского объекта. Встраиваемый объект не имеет идентификатора или таблицы.
Встраиваемые объекты, имеющие наследования, поддерживаются в зависимости от версии JPA и ORM.

Рассмотрим на примере.
У нас в таблице содержатся столбцы - first_name и last_name, а в сущности User поля - fistName и lastName. Давайте создадим отдельный объект UserInfo и вынесем туда поля, которые относятся к персональной информации пользователя:

@Embeddable
public class UserInfo {
    @Column(name = "first_name")
    private String firstName;
    @Column(name = "last_name")
    private String lastName;
}

Аннотация @Embeddable используется, что бы указать, что класс является/может быть встроенным.

Теперь добавим в нашу сущность User объект типа UserInfo:

    @Embedded
    private UserInfo userInfo;

Аннотация @Embedded используется, чтобы указать, что поле сущности является встроенным типом.

Сохраним сущность:

        User user = new User();
        user.setName("sarah.mc");
        user.setUserInfo(new UserInfo("Sarah", "Mcdaniel"));
        entityManager.persist(user);

/* Логи Hibernate:
Hibernate: insert into tb_user (last_updated, username, role, first_name, last_name) values (?, ?, ?, ?, ?)
*/

Как видно по логам Hibernate, данные сущности User, а также встроенного объекта UserInfo, успешно сохранились в одной записи.

Criteria API

Criteria API - JPA инструмент для динамического построения запросов. Эти запросы являются типобезопасными.

Рассмотрим на примерах, предварительно настроев приложение.
В качестве БД подключим H2, а ORM - Hibernate:

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.191</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.4.9.Final</version>
        </dependency>

Создадим сущность:

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int age;

Для настройки БД и сущности в resources/META-INF создадим persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="JpaExample">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <class>com.home.jpa.model.Person</class>
        <properties>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:testdb"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
        </properties>
    </persistence-unit>
</persistence>

Cоздадим EntityManager, CriteriaBuilder, откроем транзакцию и заполним сущность Person значениями.

        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("JpaExample");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        entityManager.getTransaction().begin();

        Person kate = new Person("Kate", 23);
        entityManager.persist(kate);

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

Создадим запрос, который будет возвращать все сущности заданного класса из базы данных.
Он будет эквивалентен JPQL запросу from Person.

        CriteriaQuery<Person> query = criteriaBuilder.createQuery(Person.class);
        Root<Person> fromPerson = query.from(Person.class);
        query.select(fromPerson);
        List<Person> resultList = entityManager.createQuery(query).getResultList(); // all results...

Создаем CriteriaQuery, который параметризируется типом, который этот запрос возвращает.
Для установки корня запроса вызывается метод CriteriaQuery.from, корневой объект указывает - откуда будут браться данные.
Вызываем CriteriaQuery.select для установки типа списка результатов.
Запрос выполняется с помощью TypedQuery.getResultList, этот метод возвращает коллекцию сущностей.

Пагинация
К запросам, которые возвращает метод createQuery() можно применить два метода фильтрации:
setFirstResult() - указывает, сколько результатов нужно игнорировать от начала списка результатов.
setMaxResults() - ограничивает количество возвращаемых объектов.

Рассмотрим на примере:

        CriteriaQuery<Person> query = criteriaBuilder.createQuery(Person.class);
        Root<Person> fromPerson = query.from(Person.class);
        query.select(fromPerson);
        List<Person> resultList = entityManager.createQuery(query)
                .setFirstResult(0) // значение по умолчанию
                .setMaxResults(10) // коллекция будет содержать максимум 10 результатов
                .getResultList();

Установка ограничений
Ограничения устанавливаются с помощью вызова СriteriaBuilder.equal или СriteriaBuilder.like.

Рассмотрим на примере запрос с equal.
Он будет эквивалентен JPQL запросу from Person as p where p.name='Kate'.

        CriteriaQuery<Person> query = criteriaBuilder.createQuery(Person.class);
        Root<Person> fromPerson = query.from(Person.class);
        query.select(fromPerson);
        query.where(criteriaBuilder.equal(fromPerson.get("name"), "Kate"));
        List<Person> resultList = entityManager.createQuery(query).getResultList();  // all results...

В данном случае вернутся все сущности, у которых поле name содержит значение Kate.

Рассмотрим на примере запрос с like.
Он будет эквивалентен JPQL запросу from Person as p where p.name like 'Jo%'.

        query.where(criteriaBuilder.like(fromPerson.get("name"), "Jo%"));

В данном случае вернутся все сущности, у которых поле name содержит значения, начинающиеся с подстроки Jo.

В предыдущих примерах мы прописывали наименование сущностей вручную, это не является типобезопасным. Например, если в сущности Person поле name как то измениться, запрос при выполнении вернет ошибку.
Эту проблему может решить Metamodel.

Metamodel

Metamodel создает специальные описательные классы, которые могут использоваться в Criteria API (похоже на генерацию сущностей в JOOQ).

Воспользуемся генератором от Hibernate.

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-jpamodelgen</artifactId>
            <version>5.4.2.Final</version>
            <scope>provided</scope>
        </dependency>

Вот так выглядит сгенерированная мето-модель класса Person:

@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Person.class)
public abstract class Person_ {
	public static volatile SingularAttribute<Person, String> name;
	public static volatile SingularAttribute<Person, Long> id;
	public static volatile SingularAttribute<Person, Integer> age;
	public static final String NAME = "name";
	public static final String ID = "id";
	public static final String AGE = "age";
}

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

Теперь, при составлении Criteria запросов, мы можем использовать сгенерированные сущности и в случае каких-либо изменений полей сущности, получим ошибку во время компиляции.
Рассмотрим на примере:

query.where(criteriaBuilder.equal(fromPerson.get(Person_.NAME), "Kate"));

Sources

EasyJava. Java JPA
Документация. JSR 338: Java Persistence API, Version 2.2
Wikibooks. Java Persistence

⚠️ **GitHub.com Fallback** ⚠️