Статьи

JPA Tutorial — ULTIMATE Guide (PDF Download)

ПРИМЕЧАНИЕ РЕДАКЦИИ: В этом посте мы представляем всеобъемлющее руководство по JPA. Java Persistence API (JPA) — это спецификация интерфейса прикладного программирования на языке программирования Java, которая описывает управление реляционными данными в приложениях с использованием Java Platform, Standard Edition и Java Platform, Enterprise Edition.

JPA стал стандартом де-факто для написания кода приложения, взаимодействующего с базами данных. По этой причине мы предоставили множество учебников здесь, на Java Code Geeks, большинство из которых можно найти здесь . Кроме того, мы создали мини-книгу JPA, которая познакомит вас с JPA и плавно переведет читателя на более продвинутые концепции (получите его бесплатно, присоединившись к нашей рассылке ).

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

А. Введение

Java Persistence API (JPA) — это независимая от производителя спецификация для отображения объектов Java в таблицы реляционных баз данных. Реализации этой спецификации позволяют разработчикам приложений абстрагироваться от конкретного продукта базы данных, с которым они работают, и позволяют им реализовывать операции CRUD (создание, чтение, обновление и удаление) таким образом, чтобы один и тот же код работал на разных продуктах баз данных. Эти структуры не только обрабатывают код, который взаимодействует с базой данных (код JDBC), но также отображают данные в структуры объектов, используемые приложением.

JPA состоит из трех различных компонентов:

  • Entities : в текущих версиях сущности JPA являются простыми старыми объектами Java (POJO). В старых версиях JPA требовалось создавать подклассы сущностей из классов, предоставляемых JPA, но поскольку эти подходы труднее тестировать из-за их жесткой зависимости от платформы, более новые версии JPA не требуют, чтобы сущности делили подклассы какого-либо класса инфраструктуры.
  • Object-relational metadata . Разработчик приложения должен обеспечить соответствие между классами Java и их атрибутами таблицам и столбцам базы данных. Это можно сделать либо с помощью выделенных файлов конфигурации, либо в более новой версии платформы, также с помощью аннотаций.
  • Java Persistence Query Language (JPQL) : поскольку JPA стремится абстрагироваться от конкретного продукта базы данных, платформа также предоставляет выделенный язык запросов, который можно использовать вместо SQL. Этот дополнительный перевод с JPQL на SQL позволяет реализациям платформы поддерживать различные диалекты базы данных и позволяет разработчику приложения выполнять запросы независимо от базы данных.

В этом руководстве мы рассмотрим различные аспекты инфраструктуры и разработаем простое приложение Java SE, которое сохраняет и извлекает данные в / из реляционной базы данных. Мы будем использовать следующие библиотеки / среды:

  • maven> = 3.0 в качестве среды сборки
  • JPA 2.1, содержащийся в Java Enterprise Edition (JEE) 7.0
  • Hibernate как реализация JPA (4.3.8.Final)
  • H2 как реляционная база данных в версии 1.3.176

B. Настройка проекта

В качестве первого шага мы создадим простой проект maven в командной строке:

1
mvn archetype:create -DgroupId=com.javacodegeeks.ultimate -DartifactId=jpa

Эта команда создаст следующую структуру в файловой системе:

01
02
03
04
05
06
07
08
09
10
11
12
|-- src
|   |-- main
|   |   `-- java
|   |       `-- com
|   |           `-- javacodegeeks
|   |                `-- ultimate
|   `-- test
|   |   `-- java
|   |       `-- com
|   |           `-- javacodegeeks
|   |                `-- ultimate
`-- pom.xml

Библиотеки, от которых зависит наша реализация, добавляются в раздел зависимостей файла pom.xml следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<properties>
    <jee.version>7.0</jee.version>
    <h2.version>1.3.176</h2.version>
    <hibernate.version>4.3.8.Final</hibernate.version>
</properties>
 
<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>${jee.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>${h2.version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>${hibernate.version}</version>
    </dependency>
</dependencies>

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

C. Основы

C.1. EntityManager и модуль сохранения

Теперь мы можем приступить к реализации нашей первой функциональности JPA. Давайте начнем с простого класса, который предоставляет метод run() который вызывается в main методе приложения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Main {
    private static final Logger LOGGER = Logger.getLogger("JPA");
 
    public static void main(String[] args) {
        Main main = new Main();
        main.run();
    }
 
    public void run() {
        EntityManagerFactory factory = null;
        EntityManager entityManager = null;
        try {
            factory = Persistence.createEntityManagerFactory("PersistenceUnit");
            entityManager = factory.createEntityManager();
            persistPerson(entityManager);
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            e.printStackTrace();
        } finally {
            if (entityManager != null) {
                entityManager.close();
            }
            if (factory != null) {
                factory.close();
            }
        }
    }
    ...

Почти все взаимодействие с JPA осуществляется через EntityManager . Чтобы получить экземпляр EntityManager , мы должны создать экземпляр EntityManagerFactory . Обычно нам нужен только один EntityManagerFactory для одного «модуля персистентности» на приложение. Модуль персистентности — это набор классов JPA, который управляется вместе с конфигурацией базы данных в файле с именем persistence.xml :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8" ?>
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
 
    <persistence-unit name="PersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="connection.driver_class" value="org.h2.Driver"/>
            <property name="hibernate.connection.url" value="jdbc:h2:~/jpa"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Этот файл создается в папке src/main/resource/META-INF проекта maven. Как видите, мы определяем один persistence-unit с именем PersistenceUnit который имеет transaction-type RESOURCE_LOCAL . Тип транзакции определяет, как транзакции обрабатываются в приложении.

В нашем примере приложения мы хотим обрабатывать их самостоятельно, поэтому мы указываем здесь RESOURCE_LOCAL . Когда вы используете JEE-контейнер, этот контейнер отвечает за настройку EntityManagerFactory и предоставляет вам только EntityManager . Затем контейнер также обрабатывает начало и конец каждой транзакции. В этом случае вы бы JTA значение JTA .

Устанавливая provider org.hibernate.ejb.HibernatePersistence мы выбираем реализацию JPA, которую мы хотим использовать. Поскольку мы включили Hibernate в качестве провайдера JPA в путь к классу, определив зависимость от него, мы можем ссылаться на реализацию провайдера здесь по имени класса.

Следующим шагом в persistence.xml является информирование поставщика JPA о базе данных, которую мы хотим использовать. Это делается путем указания драйвера JDBC, который должен использовать Hibernate. Поскольку мы хотим использовать базу данных H2 (www.h2database.com), для свойства connection.driver_class установлено значение org.h2.Driver .

Чтобы Hibernate мог создавать подключения к базе данных, мы также должны предоставить URL-адрес подключения. H2 предоставляет возможность создать базу данных в одном файле, путь которого указан в URL JDBC. Следовательно, JDBC URL jdbc:h2:~/jpa указывает H2 создать в домашнем каталоге пользователя файл с именем jpa.h2.db

H2 — это быстрая база данных с открытым исходным кодом, которая может быть легко встроена в приложения Java, так как поставляется в виде одного файла JAR размером около 1,5 МБ. Это делает его подходящим для нашей цели создания простого примера приложения для демонстрации использования JPA. В проектах, которые требуют решений, которые лучше масштабируются для огромных объемов данных, вы, вероятно, выбрали бы другой продукт базы данных, но для небольшого объема данных, которые имеют прочные отношения, H2 — хороший выбор.

Следующее, что мы должны сказать Hibernate — это диалект JDBC, который он должен использовать. Так как Hibernate предоставляет отдельную реализацию диалекта для H2, мы выбираем ее с помощью свойства hibernate.dialect . С помощью этого диалекта Hibernate способен создавать соответствующие операторы SQL для базы данных H2.

И последнее, но не менее важное: мы предлагаем три варианта, которые пригодятся при разработке нового приложения, но не будут использоваться в производственных средах. Первым из них является свойство hibernate.hbm2ddl.auto которое указывает Hibernate создавать все таблицы с нуля при запуске. Если таблица уже существует, она будет удалена. В нашем примере приложения это хорошая функция, так как мы можем положиться на тот факт, что база данных пуста в начале и что все изменения, которые мы внесли в схему с момента нашего последнего запуска приложения, отражаются в схеме.

Второй вариант — hibernate.show_sql который указывает Hibernate печатать каждый оператор SQL, который он выдает в базу данных в командной строке. С этой опцией мы можем легко отследить все операторы и посмотреть, все ли работает как положено. И наконец, мы говорим Hibernate довольно печатать SQL для лучшей читабельности, устанавливая свойство hibernate.format_sql в true .

Теперь, когда мы настроили файл persistence.xml , мы возвращаемся к нашему Java-коду выше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
EntityManagerFactory factory = null;
EntityManager entityManager = null;
try {
    factory = Persistence.createEntityManagerFactory("PersistenceUnit");
    entityManager = factory.createEntityManager();
    persistPerson(entityManager);
} catch (Exception e) {
    LOGGER.log(Level.SEVERE, e.getMessage(), e);
    e.printStackTrace();
} finally {
    if (entityManager != null) {
        entityManager.close();
    }
    if (factory != null) {
        factory.close();
    }
}

После получения экземпляра EntityManagerFactory и от него экземпляра EntityManager мы можем использовать их в методе persistPerson для сохранения некоторых данных в базе данных. Обратите внимание, что после того, как мы выполнили нашу работу, мы должны закрыть как EntityManager так и EntityManagerFactory .

С.2. операции

EntityManager представляет постоянный модуль, и поэтому нам потребуется в приложениях RESOURCE_LOCAL только один экземпляр EntityManager . Постоянный блок — это кэш для сущностей, которые представляют части состояния, хранящиеся в базе данных, а также соединение с базой данных. Поэтому, чтобы сохранить данные в базе данных, мы должны передать их в EntityManager и вместе с тем в основной кеш. Если вы хотите создать новую строку в базе данных, это делается путем вызова метода persist() в EntityManager как показано в следующем коде:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private void persistPerson(EntityManager entityManager) {
    EntityTransaction transaction = entityManager.getTransaction();
    try {
        transaction.begin();
        Person person = new Person();
        person.setFirstName("Homer");
        person.setLastName("Simpson");
        entityManager.persist(person);
        transaction.commit();
    } catch (Exception e) {
        if (transaction.isActive()) {
            transaction.rollback();
        }
    }
}

Но прежде чем мы сможем вызвать функцию persist() нам нужно открыть новую транзакцию, вызвав transaction.begin() для нового объекта транзакции, который мы получили из EntityManager . Если мы пропустим этот вызов, Hibernate выдаст IllegalStateException которое сообщает нам, что мы забыли запустить persist() внутри транзакции:

1
2
3
java.lang.IllegalStateException: Transaction not active
    at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:70)
    at jpa.Main.persistPerson(Main.java:87)

После вызова persist() мы должны зафиксировать транзакцию, т.е. отправить данные в базу данных и сохранить их там. В случае возникновения исключения в блоке try, мы должны откатить транзакцию, которую мы начали ранее. Но так как мы можем только откатить активные транзакции, мы должны прежде проверить, выполняется ли текущая транзакция, так как может случиться так, что исключение выдается в вызове transaction.begin() .

С.3. таблицы

Класс Person отображается в таблицу базы данных T_PERSON путем добавления аннотации @Entity :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entity
@Table(name = "T_PERSON")
public class Person {
    private Long id;
    private String firstName;
    private String lastName;
 
    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    @Column(name = "FIRST_NAME")
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
    @Column(name = "LAST_NAME")
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

Дополнительная аннотация @Table является необязательной, но вы можете использовать ее для указания конкретного имени таблицы. В нашем примере мы хотим, чтобы все таблицы имели префикс T_, поэтому мы говорим Hibernate создать таблицу T_PERSON . Таблица T_PERSON имеет три столбца: ID , FIRST_NAME , LAST_NAME .

Эта информация предоставляется поставщику JPA с аннотациями @Column и name их атрибута. Даже эта аннотация не является обязательной. JPA будет использовать все свойства класса Java, которые имеют метод setter и getter, и создаст для них столбцы, если вы не исключите их, @Transient их @Transient . С другой стороны, вы можете указать больше информации для каждого столбца, используя другие атрибуты, которые @Column аннотация @Column :

1
@Column(name = "FIRST_NAME", length = 100, nullable = false, unique = false)

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

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

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

01
02
03
04
05
06
07
08
09
10
11
@Entity
@Table(name = "T_PERSON")
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "FIRST_NAME")
    private String firstName;
    @Column(name = "LAST_NAME")
    private String lastName;
    ...

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

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

1
2
3
4
5
@Entity
@Table(name = "T_GEEK")
@Access(AccessType.PROPERTY)
public class Geek extends Person {
...

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

Когда мы запустим приведенный выше код, Hibernate выдаст следующие запросы к нашей локальной базе данных H2:

1
2
3
Hibernate: drop table T_PERSON if exists
Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id))
Hibernate: insert into T_PERSON (id, FIRST_NAME, LAST_NAME) values (null, ?, ?)

Как мы видим, Hibernate сначала удаляет таблицу T_PERSON если она существует, и создает ее заново. If создает таблицу с двумя столбцами типа varchar(255) ( FIRST_NAME , LAST_NAME ) и одним столбцом с именем id типа bigint . Последний столбец определяется как первичный ключ, и его значения автоматически генерируются базой данных, когда мы вставляем новое значение.

Мы можем проверить, что все правильно, используя Shell, поставляемый с H2. Чтобы использовать эту оболочку, нам просто нужен jar-архив h2-1.3.176.jar :

1
2
3
4
5
6
7
8
>java -cp h2-1.3.176.jar org.h2.tools.Shell -url jdbc:h2:~/jpa
 
...
 
sql> select * from T_PERSON;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson
(4 rows, 4 ms)

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

D. Наследование

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Entity
@Table(name = "T_GEEK")
public class Geek extends Person {
    private String favouriteProgrammingLanguage;
    private List<Project> projects = new ArrayList<Project>();
 
    @Column(name = "FAV_PROG_LANG")
    public String getFavouriteProgrammingLanguage() {
            return favouriteProgrammingLanguage;
    }
 
    public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
        this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
    }
    ...
}

Добавление аннотаций @Entity и @Table к классу позволяет Hibernate создать новую таблицу T_GEEK :

1
Hibernate: create table T_PERSON (DTYPE varchar(31) not null, id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (id))

Мы видим, что Hibernate создает одну таблицу для обеих сущностей и помещает информацию о том, сохранили ли мы Person или Geek в новый столбец с именем DTYPE . Давайте сохраним некоторых фанатов в нашей базе данных (для лучшей читабельности я пропустил блок, который перехватывает любое исключение и откатывает транзакцию):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
private void persistGeek(EntityManager entityManager) {
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    Geek geek = new Geek();
    geek.setFirstName("Gavin");
    geek.setLastName("Coffee");
    geek.setFavouriteProgrammingLanguage("Java");
    entityManager.persist(geek);
    geek = new Geek();
    geek.setFirstName("Thomas");
    geek.setLastName("Micro");
    geek.setFavouriteProgrammingLanguage("C#");
    entityManager.persist(geek);
    geek = new Geek();
    geek.setFirstName("Christian");
    geek.setLastName("Cup");
    geek.setFavouriteProgrammingLanguage("Java");
    entityManager.persist(geek);
    transaction.commit();
}

После выполнения этого метода таблица базы данных T_PERSON содержит следующие строки (вместе с человеком, которого мы уже вставили):

1
2
3
4
5
6
sql> select * from t_person;
DTYPE  | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
Person | 1  | Homer      | Simpson   | null
Geek   | 2  | Gavin      | Coffee    | Java
Geek   | 3  | Thomas     | Micro     | C#
Geek   | 4  | Christian  | Cup       | Java

Как и ожидалось, новый столбец DTYPE определяет, какой тип человека у нас есть. Столбец FAV_PROG_LANG имеет значение null для людей, которые не являются фанатами.

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

1
@DiscriminatorColumn(name="PERSON_TYPE", discriminatorType = DiscriminatorType.INTEGER)

Это приводит к следующему результату:

1
2
3
4
5
6
sql> select * from t_person;
PERSON_TYPE | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
-1907849355 | 1  | Homer      | Simpson   | null
2215460     | 2  | Gavin      | Coffee    | Java
2215460     | 3  | Thomas     | Micro     | C#
2215460     | 4  | Christian  | Cup       | Java

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

  • SINGLE_TABLE : эта стратегия отображает все классы в одну таблицу. Как следствие, в каждой строке есть все столбцы для всех типов, база данных нуждается в дополнительном хранилище для пустых столбцов. С другой стороны, эта стратегия дает преимущество, заключающееся в том, что в запросе никогда не используется соединение, и поэтому оно может быть намного быстрее
  • JOINED : эта стратегия создает для каждого типа отдельную таблицу. Поэтому каждая таблица содержит только состояние сопоставленной сущности. Чтобы загрузить один объект, поставщик JPA должен загрузить данные для одного объекта из всех таблиц, в которые отображается объект. Этот подход уменьшает объем памяти, но с другой стороны вводит запросы на соединение, которые могут значительно снизить скорость запросов.
  • TABLE_PER_CLASS : Как и стратегия JOINED , эта стратегия создает отдельную таблицу для каждого типа сущности. Но в отличие от стратегии JOINED эти таблицы содержат всю информацию, необходимую для загрузки этой сущности. Следовательно, для загрузки сущностей не нужны никакие запросы соединения, но он вводит в ситуациях, когда конкретному подклассу неизвестны дополнительные запросы SQL для его определения.

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

1
@Inheritance(strategy = InheritanceType.JOINED)

Теперь Hibernate создает две таблицы для людей и гиков:

1
2
Hibernate: create table T_GEEK (FAV_PROG_LANG varchar(255), id bigint not null, primary key (id))
Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id))

Добавив человека и гиков, мы получим следующий результат:

01
02
03
04
05
06
07
08
09
10
11
12
13
sql> select * from t_person;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson
2  | Gavin      | Coffee
3  | Thomas     | Micro
4  | Christian  | Cup
(4 rows, 12 ms)
sql> select * from t_geek;
FAV_PROG_LANG | ID
Java          | 2
C#            | 3
Java          | 4
(3 rows, 7 ms)

Как и ожидалось, данные распределены по двум таблицам. Базовая таблица T_PERSON содержит все общие атрибуты, тогда как таблица T_GEEK содержит только строки для каждого гика. Каждая строка ссылается на человека по значению ID столбца.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
select
    person0_.id as id1_2_,
    person0_.FIRST_NAME as FIRST_NA2_2_,
    person0_.LAST_NAME as LAST_NAM3_2_,
    person0_1_.FAV_PROG_LANG as FAV_PROG1_1_,
    case
        when person0_1_.id is not null then 1
        when person0_.id is not null then 0
    end as clazz_
from
    T_PERSON person0_
left outer join
    T_GEEK person0_1_
        on person0_.id=person0_1_.id

Мы видим, что запрос соединения необходим для включения данных из таблицы T_GEEK и что Hibernate кодирует информацию, если одна строка является гиком или возвращает целое число (см. Оператор case).

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

01
02
03
04
05
06
07
08
09
10
11
TypedQuery<Person> query = entityManager.createQuery("from Person", Person.class);
List<Person> resultList = query.getResultList();
for (Person person : resultList) {
    StringBuilder sb = new StringBuilder();
    sb.append(person.getFirstName()).append(" ").append(person.getLastName());
    if (person instanceof Geek) {
        Geek geek = (Geek)person;
        sb.append(" ").append(geek.getFavouriteProgrammingLanguage());
    }
    LOGGER.info(sb.toString());
}

Прежде всего мы создаем объект Query , вызывая метод createQuery() EntityManager. В предложении запроса может отсутствовать ключевое слово select. Второй параметр помогает параметризовать метод так, чтобы Query тип Person . Выдача запроса выполняется просто путем вызова query.getResultList() . Возвращаемый список является итеративным, поэтому мы можем просто перебирать объекты Person . Если мы хотим знать, есть ли у нас Person или Geek , мы можем просто использовать оператор instanceof Java.

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

1
2
3
4
Homer Simpson
Gavin Coffee Java
Thomas Micro C#
Christian Cup Java

Е. Отношения

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

  • OneToOne : в этом отношении каждая сущность имеет ровно одну ссылку на другую сущность и наоборот.
  • OneToMany / ManyToOne : в этом отношении один объект может иметь несколько дочерних объектов, и каждый дочерний объект принадлежит одному родительскому объекту.
  • ManyToMany : в этом отношении несколько объектов одного типа могут иметь несколько ссылок на объекты другого типа.
  • Embedded : в этом отношении другая сущность хранится в той же таблице, что и родительская сущность (то есть у нас есть две сущности для одной таблицы).
  • ElementCollection : это отношение похоже на отношение OneToMany но в отличие от него указанный объект является Embedded объектом. Это позволяет определять отношения OneToMany для простых объектов, которые хранятся в отличие от «нормальных» Embedded отношений в другой таблице.

Д.1. Один к одному

Давайте начнем с отношения OneToOne , добавив новую сущность IdCard :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Entity
@Table(name = "T_ID_CARD")
public class IdCard {
    private Long id;
    private String idNumber;
    private Date issueDate;
 
    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    @Column(name = "ID_NUMBER")
    public String getIdNumber() {
        return idNumber;
    }
 
    public void setIdNumber(String idNumber) {
        this.idNumber = idNumber;
    }
 
    @Column(name = "ISSUE_DATE")
    @Temporal(TemporalType.TIMESTAMP)
    public Date getIssueDate() {
        return issueDate;
    }
 
    public void setIssueDate(Date issueDate) {
        this.issueDate = issueDate;
    }
}

Обратите внимание, что мы использовали общий java.util.Date для моделирования даты выпуска удостоверения личности. Мы можем использовать аннотацию @Temporal чтобы сообщить JPA, как мы хотим, чтобы Date была сериализована в базу данных. В зависимости от базового продукта базы данных этот столбец сопоставляется с соответствующим типом даты / времени. Возможные значения для этой аннотации следующие: TIMESTAMP : TIME и DATE .

Мы сообщаем JPA, что у каждого человека есть ровно одна идентификационная карта:

01
02
03
04
05
06
07
08
09
10
11
12
@Entity
@Table(name = "T_PERSON")
public class Person {
    ...
    private IdCard idCard;
    ...
 
    @OneToOne
    @JoinColumn(name = "ID_CARD_ID")
    public IdCard getIdCard() {
        return idCard;
    }

Столбец в таблице T_PERSON который содержит внешний ключ к таблице T_ID_CARD , хранится в дополнительном столбце ID_CARD_ID . Теперь Hibernate генерирует эти две таблицы следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
create table T_ID_CARD (
    id bigint generated by default as identity,
    ID_NUMBER varchar(255),
    ISSUE_DATE timestamp,
    primary key (id)
)
 
create table T_PERSON (
    id bigint generated by default as identity,
    FIRST_NAME varchar(255),
    LAST_NAME varchar(255),
    ID_CARD_ID bigint,
    primary key (id)
)

Важным фактом является то, что мы можем настроить время загрузки объекта ID-карты. Поэтому мы можем добавить атрибут fetch к аннотации @OneToOne :

1
@OneToOne(fetch = FetchType.EAGER)

Значение FetchType.EAGER является значением по умолчанию и указывает, что каждый раз, когда мы загружаем человека, мы также хотим загрузить удостоверение личности. С другой стороны, мы можем указать, что мы хотим загружать идентификатор только тогда, когда мы фактически получаем к нему доступ, вызывая person.getIdCard() :

1
@OneToOne(fetch = FetchType.LAZY)

Это приводит к следующим операторам SQL при загрузке всех лиц:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Hibernate:
    select
        person0_.id as id1_3_,
        person0_.FIRST_NAME as FIRST_NA2_3_,
        person0_.ID_CARD_ID as ID_CARD_4_3_,
        person0_.LAST_NAME as LAST_NAM3_3_,
        person0_1_.FAV_PROG_LANG as FAV_PROG1_1_,
        case
            when person0_1_.id is not null then 1
            when person0_.id is not null then 0
        end as clazz_
    from
        T_PERSON person0_
    left outer join
        T_GEEK person0_1_
            on person0_.id=person0_1_.id
Hibernate:
    select
        idcard0_.id as id1_2_0_,
        idcard0_.ID_NUMBER as ID_NUMBE2_2_0_,
        idcard0_.ISSUE_DATE as ISSUE_DA3_2_0_
    from
        T_ID_CARD idcard0_
    where
        idcard0_.id=?

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

Е.2. Один ко многим

Другое важное отношение — это отношение @OneToMany . В нашем примере каждый Person должен иметь один или несколько телефонов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Entity
@Table(name = "T_PHONE")
public class Phone {
    private Long id;
    private String number;
    private Person person;
 
    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    @Column(name = "NUMBER")
    public String getNumber() {
        return number;
    }
 
    public void setNumber(String number) {
        this.number = number;
    }
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PERSON_ID")
    public Person getPerson() {
        return person;
    }
 
    public void setPerson(Person person) {
        this.person = person;
    }
}

Каждый телефон имеет внутренний идентификатор, а также номер. Кроме того, мы также должны указать отношение к Person с @ManyToOne , так как у нас «много» телефонов для «одного» человека. Аннотация @JoinColumn указывает столбец в таблице T_PHONE котором хранится внешний ключ для этого человека.

С другой стороны отношения мы должны добавить объекты List of Phone к человеку и аннотировать соответствующий метод получения с помощью @OneToMany поскольку у нас есть «один» человек с «многими» телефонами:

1
2
3
4
5
6
private List<Phone> phones = new ArrayList<>();
...
@OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
public List<Phone> getPhones() {
    return phones;
}

Значение атрибута mappedBy сообщает JPA, какой список на другой стороне отношения (здесь Phone.person ) Phone.person эту аннотацию.

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

01
02
03
04
05
06
07
08
09
10
select
    phones0_.PERSON_ID as PERSON_I3_3_0_,
    phones0_.id as id1_4_0_,
    phones0_.id as id1_4_1_,
    phones0_.NUMBER as NUMBER2_4_1_,
    phones0_.PERSON_ID as PERSON_I3_4_1_
from
    T_PHONE phones0_
where
    phones0_.PERSON_ID=?

Поскольку значение для атрибута fetch устанавливается во время компиляции, мы, к сожалению, не можем изменить его во время выполнения. Но если мы знаем, что мы хотим загрузить все телефонные номера в этом случае использования, а в других случаях использования нет, мы можем оставить отношение для загрузки ленивым и добавить предложение left join fetch в наш запрос JPQL, чтобы сообщить провайдеру JPA также загрузить все телефоны в этом конкретном запросе, даже если для отношения установлено значение FetchType.LAZY . Такой запрос может выглядеть следующим образом:

1
TypedQuery<Person> query = entityManager.createQuery("from Person p left join fetch p.phones", Person.class);

Мы даем Person псевдоним p и сообщаем JPA, чтобы он также выбирал все экземпляры phones , принадлежащих каждому человеку. Это приводит к Hibernate в следующем запросе выбора:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
select
    person0_.id as id1_3_0_,
    phones1_.id as id1_4_1_,
    person0_.FIRST_NAME as FIRST_NA2_3_0_,
    person0_.ID_CARD_ID as ID_CARD_4_3_0_,
    person0_.LAST_NAME as LAST_NAM3_3_0_,
    person0_1_.FAV_PROG_LANG as FAV_PROG1_1_0_,
    case
        when person0_1_.id is not null then 1
        when person0_.id is not null then 0
    end as clazz_0_,
    phones1_.NUMBER as NUMBER2_4_1_,
    phones1_.PERSON_ID as PERSON_I3_4_1_,
    phones1_.PERSON_ID as PERSON_I3_3_0__,
    phones1_.id as id1_4_0__
from
    T_PERSON person0_
left outer join
    T_GEEK person0_1_
        on person0_.id=person0_1_.id
left outer join
    T_PHONE phones1_
        on person0_.id=phones1_.PERSON_ID

Обратите внимание, что без left ключевого слова (т. join fetch Только join fetch ) Hibernate создаст внутреннее объединение и загрузит только тех людей, у которых фактически есть хотя бы один номер телефона.

Д.3. ManyToMany

Еще одно интересное отношение — @ManyToMany . Поскольку один гик может присоединиться ко многим проектам, а один проект состоит из множества гиков, мы моделируем отношения между Project и Geek как отношения @ManyToMany :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entity
@Table(name = "T_PROJECT")
public class Project {
    private Long id;
    private String title;
    private List<Geek> geeks = new ArrayList<Geek>();
 
    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    @Column(name = "TITLE")
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    @ManyToMany(mappedBy="projects")
    public List<Geek> getGeeks() {
        return geeks;
    }
 
    public void setGeeks(List<Geek> geeks) {
        this.geeks = geeks;
    }
}

Наш проект имеет внутренний идентификатор, заголовок типа String и список Geeks . Метод @ManyToMany(mappedBy="projects") для атрибута geeks аннотируется @ManyToMany(mappedBy="projects") . Значение атрибута mappedBy сообщает JPA члену класса с другой стороны отношения, к которому относится это отношение, поскольку для гика может быть несколько списков проектов. Класс Geek получает список проектов и:

01
02
03
04
05
06
07
08
09
10
private List<Project> projects = new ArrayList<>();
...
@ManyToMany
@JoinTable(
        name="T_GEEK_PROJECT",
        joinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")},
        inverseJoinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")})
public List<Project> getProjects() {
    return projects;
}

Для @ManyToMany нам нужна дополнительная таблица. Эта таблица конфигурируется аннотацией @JoinTable которая описывает таблицу, используемую для хранения назначений гика различным проектам. Он имеет имя GEEK_PROJECT и хранит идентификатор гика в столбце GEEK_ID и идентификатор проекта в столбце PROJECT_ID . Указанный столбец с обеих сторон является просто ID как мы назвали внутренний идентификатор в идентификаторе обоих классов.

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

Поскольку отношение @ManyToMany одинаково с обеих сторон, мы могли бы также аннотировать два списка в обоих классах наоборот:

1
2
3
4
5
6
7
8
@ManyToMany
@JoinTable(
        name="T_GEEK_PROJECT",
        joinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")},
        inverseJoinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")})
public List<Geek> getGeeks() {
    return geeks;
}

А с другой стороны Geek :

1
2
3
4
@ManyToMany(mappedBy="geeks")
public List<Project> getProjects() {
    return projects;
}

В обоих случаях Hibernate создает новую таблицу T_GEEK_PROJECT с двумя столбцами PROJECT_ID и GEEK_ID :

1
2
3
4
5
sql> select * from t_geek_project;
PROJECT_ID | GEEK_ID
1          | 2
1          | 4
(2 rows, 2 ms)

Код Java для сохранения этих отношений следующий:

01
02
03
04
05
06
07
08
09
10
11
List<Geek> resultList = entityManager.createQuery("from Geek g where g.favouriteProgrammingLanguage = :fpl", Geek.class).setParameter("fpl", "Java").getResultList();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
Project project = new Project();
project.setTitle("Java Project");
for (Geek geek : resultList) {
    project.getGeeks().add(geek);
    geek.getProjects().add(project);
}
entityManager.persist(project);
transaction.commit();

В этом примере мы хотим добавить гиков только к нашему «проекту Java», чей любимый язык программирования, конечно, Java. Следовательно, мы добавляем предложение where к нашему запросу select, который ограничивает результирующий набор вундеркиндами с конкретным значением для столбца FAV_PROG_LANG . Поскольку этот столбец сопоставлен с полем favouriteProgrammingLanguage , мы можем напрямую ссылаться на него по имени его поля Java в инструкции JPQL. Динамическое значение для запроса передается в оператор путем вызова setParameter() для соответствующей переменной в запросе JPQL (здесь: fpl ).

Д.4. Embedded / ElementCollection

Может случиться так, что вы захотите структурировать модель Java более детально, чем модель базы данных. Примером такого варианта использования является класс Java Period который моделирует время между начальной и конечной датой. Эту конструкцию можно повторно использовать в разных сущностях, поскольку вы не хотите копировать два поля класса startDate и endDate для каждой сущности, у которой есть период времени.

Для таких случаев JPA предлагает возможность моделирования встроенных объектов. Эти объекты моделируются как отдельные классы Java с аннотацией @Embeddable :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Embeddable
public class Period {
    private Date startDate;
    private Date endDate;
 
    @Column(name ="START_DATE")
    public Date getStartDate() {
        return startDate;
    }
 
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }
 
    @Column(name ="END_DATE")
    public Date getEndDate() {
        return endDate;
    }
 
    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }
}

Этот объект затем может быть включен в наш объект Project :

01
02
03
04
05
06
07
08
09
10
private Period projectPeriod;
 
@Embedded
public Period getProjectPeriod() {
    return projectPeriod;
}
 
public void setProjectPeriod(Period projectPeriod) {
    this.projectPeriod = projectPeriod;
}

Поскольку эта сущность встроена, Hibernate создает два столбца START_DATE и END_DATE для таблицы T_PROJECT :

1
2
3
4
5
6
7
8
create table T_PROJECT (
    id bigint generated by default as identity,
    END_DATE timestamp,
    START_DATE timestamp,
    projectType varchar(255),
    TITLE varchar(255),
    primary key (id)
)

Хотя эти два значения смоделированы в отдельном классе Java, мы можем запросить их как часть проекта:

1
2
3
4
sql> select * from t_project;
ID | END_DATE                | START_DATE              | PROJECTTYPE       | TITLE
1  | 2015-02-01 00:00:00.000 | 2016-01-31 23:59:59.999 | TIME_AND_MATERIAL | Java Project
(1 row, 2 ms)

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

1
entityManager.createQuery("from Project p where p.projectPeriod.startDate = :startDate", Project.class).setParameter("startDate", createDate(1, 1, 2015));

Это приводит к следующему запросу SQL:

01
02
03
04
05
06
07
08
09
10
select
    project0_.id as id1_5_,
    project0_.END_DATE as END_DATE2_5_,
    project0_.START_DATE as START_DA3_5_,
    project0_.projectType as projectT4_5_,
    project0_.TITLE as TITLE5_5_
from
    T_PROJECT project0_
where
    project0_.START_DATE=?

Начиная с версии 2.0 JPA вы даже можете использовать сущности @Embeddable в отношениях один-ко-многим. Это достигается с помощью новых аннотаций @ElementCollection и @CollectionTable как показано в следующем примере для класса Project :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
private List<Period> billingPeriods = new ArrayList<Period>();
 
@ElementCollection
@CollectionTable(
        name="T_BILLING_PERIOD",
        joinColumns=@JoinColumn(name="PROJECT_ID")
)
public List<Period> getBillingPeriods() {
    return billingPeriods;
}
 
public void setBillingPeriods(List<Period> billingPeriods) {
    this.billingPeriods = billingPeriods;
}

Поскольку Period является сущностью @Embeddable мы не можем просто использовать нормальное отношение @OneToMany .

G. Типы данных и преобразователи

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

Тип Java Тип базы данных
Строка (char, char []) VARCHAR (CHAR, VARCHAR2, CLOB, TEXT)
Число (BigDecimal, BigInteger, Integer, Double, Long, Float, Short, Byte) ЧИСЛЕННЫЙ (НОМЕР, ИНТ, ДЛИННЫЙ, ПЛАВУТ, ДВОЙНОЙ)
int, long, float, double, short, byte ЧИСЛЕННЫЙ (НОМЕР, ИНТ, ДЛИННЫЙ, ПЛАВУТ, ДВОЙНОЙ)
байт[] VARBINARY (BINARY, BLOB)
логическое (булево) BOOLEAN (BIT, SMALLINT, INT, NUMBER)
java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar TIMESTAMP (дата, дата)
java.lang.Enum ЧИСЛЕННЫЙ (VARCHAR, CHAR)
java.util.Serializable VARBINARY (BINARY, BLOB)

Интересным моментом в этой таблице является отображение типов перечислений. Чтобы продемонстрировать использование перечислений в JPA, давайте добавим enum ProjectType к сущности:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Entity
@Table(name = "T_PROJECT")
public class Project {
...
    private ProjectType projectType;
 
    public enum ProjectType {
        FIXED, TIME_AND_MATERIAL
    }
    ...
    @Enumerated(EnumType.ORDINAL)
    public ProjectType getProjectType() {
        return projectType;
    }
 
    public void setProjectType(ProjectType projectType) {
        this.projectType = projectType;
    }
}

Как видно из приведенного выше фрагмента, аннотация @Enumerated позволяет нам отображать перечисления в столбцы базы данных, определяя, как сопоставлять различные значения со столбцом. Выбор EnumType.ORDINAL означает, что каждая константа перечисления сопоставляется с определенным числом в базе данных. Когда мы устанавливаем наш «Java-проект» на TIME_AND_MATERIAL мы получаем следующий вывод:

1
2
3
4
sql> select * from t_project;
ID | PROJECTTYPE | TITLE
1  | 1           | Java Project
(1 row, 2 ms)

В качестве альтернативы мы также можем использовать значение EnumType.STRING . В этом случае столбец имеет тип String и кодирует перечисление, вызывая его метод name() :

1
2
3
4
sql> select * from t_project;
ID | PROJECTTYPE       | TITLE
1  | TIME_AND_MATERIAL | Java Project
(1 row, 2 ms)

Если оба решения не удовлетворяют вашим требованиям, вы можете написать свой собственный конвертер. Это делается путем реализации интерфейса Java AttributeConverterи аннотирования класса с помощью @Converter. Следующий класс, например, преобразует логическое значение в два числовых значения 1и -1:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Converter
public class BooleanConverter implements AttributeConverter<Boolean, Integer> {
 
    @Override
    public Integer convertToDatabaseColumn(Boolean aBoolean) {
        if (Boolean.TRUE.equals(aBoolean)) {
            return 1;
        } else {
            return -1;
        }
    }
 
    @Override
    public Boolean convertToEntityAttribute(Integer value) {
        if (value == null) {
            return Boolean.FALSE;
        } else {
            if (value == 1) {
                return Boolean.TRUE;
            } else {
                return Boolean.FALSE;
            }
        }
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
private boolean valid;
...
@Column(name = "VALID")
@Convert(converter = BooleanConverter.class)
public boolean isValid() {
    return valid;
}
 
public void setValid(boolean valid) {
    this.valid = valid;
}

Вставка удостоверения личности с falseатрибутом validприводит к следующему выводу:

1
2
3
sql> select * from t_id_card;
ID | ID_NUMBER | ISSUE_DATE              | VALID
1  | 4711      | 2015-02-04 16:43:30.233 | -1

H. Критерии API

До сих пор мы использовали язык запросов постоянства Java (JPQL) для выдачи запросов к базе данных. Альтернативой JPQL является «Критериальный API». Этот API предоставляет чистый API-интерфейс на основе методов Java для создания запроса.

В следующем примере запрашивается база данных для лиц с firstName = 'Homer':

1
2
3
4
5
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> personRoot = query.from(Person.class);
query.where(builder.equal(personRoot.get("firstName"), "Homer"));
List<Person> resultList = entityManager.createQuery(query).getResultList();

При начале построения запроса критериев, мы должны спросить EntityManagerдля CriteriaBuilderобъекта. Этот строитель может быть использован для создания реального Queryобъекта. Способ сообщить этому запросу, какую таблицу (таблицы) следует запрашивать, вызывая метод from()и передавая сущность, которая сопоставлена ​​с соответствующей таблицей. QueryОбъект также предлагает способ добавления ИНЕКЯ:

1
query.where(builder.equal(personRoot.get("firstName"), "Homer"));

Само условие затем создается с помощью CriteriaBuilderи его equal()метод. Более сложные запросы могут быть собраны с использованием соответствующего логического соединения:

1
2
3
query.where(builder.and(
    builder.equal(personRoot.get("firstName"), "Homer"),
    builder.equal(personRoot.get("lastName"), "Simpson")));

В целом CriteriaQueryопределяются следующие пункты и варианты:

  • distinct() Указывает, должна ли база данных отфильтровывать повторяющиеся значения.
  • from() : Определяет таблицу / сущность, для которой отправляется запрос.
  • select(): Определяет selectзапрос.
  • multiselect() : Определяет список выборов.
  • where(): Определяет whereпредложение запроса.
  • orderBy() : Указывает порядок для запроса.
  • groupBy() : Определяет группы, которые сформированы по результату.
  • having() : Определяет ограничения для групп, которые определены в результате.
  • subquery() : Указывает подзапрос, который можно использовать в других запросах.

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

I. Последовательности

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

  • TABLEЭта стратегия позволяет провайдеру JPA создать отдельную таблицу, содержащую по одной строке для каждой сущности. Эта строка содержит рядом с именем объекта также текущее значение для идентификатора. Каждый раз, когда требуется новое значение, строка в таблице обновляется соответствующим образом.
  • SEQUENCE: Если база данных предоставляет последовательности, эта стратегия запрашивает уникальные значения из предоставленной последовательности базы данных. Не все продукты баз данных поддерживают последовательности.
  • IDENTITYЕсли в базе данных есть столбцы идентификаторов, в этой стратегии используются столбцы такого типа, предоставляемые базовой реализацией базы данных. Не все продукты баз данных поддерживают столбцы идентификаторов.

Чтобы использовать эту TABLEстратегию, мы также должны сообщить провайдеру JPA некоторые подробности о таблице, которую следует использовать для управления последовательностями:

1
2
3
4
5
6
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "TABLE_GENERATOR")
@TableGenerator(name = "TABLE_GENERATOR", table="T_SEQUENCES", pkColumnName = "SEQ_NAME", valueColumnName = "SEQ_VALUE", pkColumnValue = "PHONE")
public Long getId() {
    return id;
}

@TableGeneratorАннотацию сказать нашему провайдеру JPA , что таблица должна иметь название T_SEQUENCESи две колонки SEQ_NAMEи SEQ_VALUE. Имя для этой последовательности в таблице должно быть PHONE:

1
2
3
sql> select * from t_sequences;
SEQ_NAME | SEQ_VALUE
PHONE    | 1

SEQUENCEСтратегия может быть использована аналогичным образом:

1
2
3
4
5
6
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "S_PROJECT")
@SequenceGenerator(name = "S_PROJECT", sequenceName = "S_PROJECT", allocationSize = 100)
public Long getId() {
    return id;
}

Используя аннотацию, @SequenceGeneratorмы сообщаем провайдеру JPA, как должна быть названа последовательность ( S_PROJECT), и сообщаем ей размер выделения (здесь 100), то есть сколько значений должно быть предварительно выделено. Атрибуты generatorи nameсвязывают две аннотации друг с другом.

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

Чтобы использовать IDENTITYстратегию, нам просто нужно установить соответствующий strategyатрибут:

1
2
3
4
5
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
    return id;
}

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

1
2
3
4
5
6
7
create table T_ID_CARD (
    id bigint generated by default as identity,
    ID_NUMBER varchar(255),
    ISSUE_DATE timestamp,
    VALID integer,
    primary key (id)
)

J. Скачать исходный код учебника JPA

Это было руководство по Java Persistence API (JPA).