Современный мир разработки полон различных спецификаций призванных упроститить жизнь. Зная инструменты, можно выбрать подходящий. Не зная — можно усложнить себе жизнь. Данный обзор приоткроет завесу тайны над понятием JPA — Java Persistence API. Надеюсь, после прочтения захочется погрузиться в этот таинственный мир ещё глубже.

Время на прочтение
Эта статья не будет затрагивать основы hibernate (как определить entity или написать criteria query). Тут я постараюсь рассказать о более интересных моментах, действительно полезных в работе. Информацию о которых я не встречал в одной месте.

Сразу оговорюсь. Все ниже изложенное справедливо для Hibernate 5.2. Также возможны ошибки в силу того, что я что-то неправильно понял. Если обнаружите — пишите.
- N+1 проблема
- Вступление
- Постоянные сущности
- Загрузка объектов
- Отделение сущностей
- Слияние сущностей
- Запросы для сущностей
- Удаление сущностей
- Обзор
- Различные способы удаления объектов
- Удаление с помощью Entity Manager
- Каскадное удаление
- Удаление сирот
- Удаление с использованием оператора JPQL
- Удаление с использованием собственных запросов
- Мягкое удаление
- Заключение
- Добавление JPA
- Проблемы отображения объектной модели в реляционную
- Проблема 1. Наследование и полиморфные запросы.
- Проблема 2. Отношение композиции в ООП
- Entity Lifecycle
- Сущности (Entities)
- Set, Bag, List
- Создание проекта
- Дата и время
- JPA Transactions
- Mapping
- Batching
- Генераторы
- Deadlock
- Тестирование
- Criteria API
- Сила References
N+1 проблема
Это достаточно изъезженная тема, поэтому пробежимся по ней быстро.
N+1 проблема — это ситуация, когда вместо одного запроса на выбор N книг происходит по меньшей мере N+1 запрос.
Самый простой способ решения N+1 проблемы это сделать fetch связанных таблиц. В этом случае у нас может возникнуть несколько других проблем:
Есть и другие способы решения N+1 проблемы.
Вступление
Как мы знаем, одна из основных задач программ — хранение и обработка данных.
В старые добрые времена люди хранили данные просто в файлах. Но как только нужен одновременный доступ на чтение и редактирование, когда появляется нагрузка (т.е. одновременно поступает несколько обращений), хранение данных просто в файлах становится проблемой.
Подробнее о том, какие проблемы решают БД и каким образом, советую прочитать в статье «Как устроены базы данных».
Значит, наши данные мы решаем хранить в базе данных. С давних пор Java умеет работать с базами данных при помощи JDBC API (The Java Database Connectivity). Подробнее про JDBC можно прочитать тут: «JDBC или с чего всё начинается».
Но время шло и разработчики каждый раз сталкивались с необходимостью писать однотипный и ненужный «обслуживающий» код (так называемый Boilerplate code) для тривиальных операций по сохранению Java объектов в БД и наоборот, созданию Java объектов по данным из БД. И тогда для решения этих проблем на свет появилось такое понятие, как ORM.
ORM — Object-Relational Mapping или в переводе на русский объектно-реляционное отображение. Это технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования. Если упростить, то ORM это связь Java объектов и записей в БД:


APIEntityManager предоставляет набор методов. Мы можем взаимодействовать с базой данных, используя эти методы.
Постоянные сущности
Чтобы иметь объект, связанный с EntityManager, мы можем использовать методpersist():
После сохранения объекта в базе данных он находится в состоянииpersistent.
Загрузка объектов
Для получения объекта из базы данных мы можем использовать методfind().
Здесь метод выполняет поиск по первичному ключу. Фактически, метод ожидает тип класса сущности и первичный ключ:
However, if we just need the reference to the entity, we can use the getReference() вместо этого. По сути, он возвращает прокси для сущности:
Movie movieRef = em.getReference(Movie.class, new Long(movieId));
Отделение сущностей
В случае, если нам нужно отсоединить объект от контекста постоянства,we can use the detach() method. Мы передаем объект, который будет отсоединен, в качестве параметра методу:
Как только объект отсоединен от контекста постоянства, он будет в отключенном состоянии.
Слияние сущностей
На практике многие приложения требуют модификации сущности для нескольких транзакций. Например, мы можем захотеть получить сущность в одной транзакции для рендеринга в пользовательский интерфейс. Затем другая транзакция внесет изменения, сделанные в пользовательском интерфейсе.
В таких ситуациях мы можем использовать методmerge(). The merge method helps to bring in the modifications made to the detached entity, in the managed entity, if any:
Запросы для сущностей
Кроме того, мы можем использовать JPQL для запроса сущностей. Мы вызовемgetResultList() для их выполнения.
Конечно, мы можем использоватьgetSingleResult(),, если запрос возвращает только один объект:
Удаление сущностей
Дополнительноwe can remove an entity from the database using the remove() method. Важно отметить, что объект не отсоединяется, а удаляется.
Здесь состояние объекта меняется с постоянного на новое:
Обзор
Как полнофункциональная платформа ORM, Hibernate отвечает за управление жизненным циклом постоянных объектов (объектов), включая операции CRUD, такие как
read
,
save
,
update
и
delete
.
В этой статье мы рассмотрим различные
способы удаления объектов из базы данных с помощью Hibernate
и объясним типичные проблемы и возможные подводные камни.
Мы используем JPA и делаем шаг назад и используем нативный API Hibernate для тех функций, которые не стандартизированы в JPA.
Различные способы удаления объектов
Объекты могут быть удалены в следующих случаях:
В оставшейся части статьи мы рассмотрим эти моменты подробно.
Удаление с помощью Entity Manager
Удаление с помощью
EntityManager
— самый простой способ удаления экземпляра объекта:
Foo foo = new Foo(«foo»);
entityManager.persist(foo);
flushAndClear();
foo = entityManager.find(Foo.class, foo.getId());
assertThat(foo, notNullValue());
entityManager.remove(foo);
flushAndClear();
assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());
В примерах в этой статье мы используем вспомогательный метод для очистки и очистки контекста персистентности, когда это необходимо:
После вызова метода
EntityManager.remove
предоставленный экземпляр переходит в состояние
removed
, и соответствующее удаление из базы данных происходит при следующей очистке.
Когда мы удаляем экземпляр
Bar
, на который ссылается экземпляр
Foo
, который также загружается в контексте постоянства, экземпляр
Bar
не будет удален из базы данных:
Bar bar = new Bar(«bar»);
Foo foo = new Foo(«foo»);
foo.setBar(bar);
entityManager.persist(foo);
flushAndClear();
foo = entityManager.find(Foo.class, foo.getId());
bar = entityManager.find(Bar.class, bar.getId());
entityManager.remove(bar);
flushAndClear();
bar = entityManager.find(Bar.class, bar.getId());
assertThat(bar, notNullValue());
foo = entityManager.find(Foo.class, foo.getId());
foo.setBar(null);
entityManager.remove(bar);
flushAndClear();
assertThat(entityManager.find(Bar.class, bar.getId()), nullValue());
Если на удаленный
Bar
ссылается
Foo
, операция
PERSIST
каскадно переходит из
Foo
в
Bar
, поскольку ассоциация помечена как
cascade = CascadeType. ALL
и удаление не запланировано. Чтобы убедиться, что это происходит, мы можем включить уровень журнала трассировки для пакета
org.hibernate
и выполнить поиск записей, таких как
un-scheduling entity deletion
.
Каскадное удаление
Удаление может быть связано с дочерними объектами при удалении родителей:
Bar bar = new Bar(«bar»);
Foo foo = new Foo(«foo»);
foo.setBar(bar);
entityManager.persist(foo);
flushAndClear();
foo = entityManager.find(Foo.class, foo.getId());
entityManager.remove(foo);
flushAndClear();
assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());
assertThat(entityManager.find(Bar.class, bar.getId()), nullValue());
Здесь
bar
удаляется, потому что удаление связано с
foo
, так как объявляется, что ассоциация каскадирует все операции жизненного цикла от
Foo
до
Bar
.
Удаление сирот
Директива
orphanRemoval
объявляет, что связанные экземпляры сущностей должны быть удалены, когда они отсоединены от родителя, или эквивалентно, когда родитель удален.
Мы показываем это, определяя такую связь от
Bar
до
Baz:
Затем экземпляр
Baz
удаляется автоматически, когда он удаляется из списка родительского экземпляра
Bar
:
Bar bar = new Bar(«bar»);
Baz baz = new Baz(«baz»);
bar.getBazList().add(baz);
entityManager.persist(bar);
flushAndClear();
bar = entityManager.find(Bar.class, bar.getId());
baz = bar.getBazList().get(0);
bar.getBazList().remove(baz);
flushAndClear();
assertThat(entityManager.find(Baz.class, baz.getId()), nullValue());
Удаление с использованием оператора JPQL
Hibernate поддерживает операции удаления в стиле DML:
Foo foo = new Foo(«foo»);
entityManager.persist(foo);
flushAndClear();
entityManager.createQuery(«delete from Foo where id = :id»)
.setParameter(«id», foo.getId())
.executeUpdate();
assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());
Важно отметить, что операторы JPQL в стиле DML не влияют ни на состояние, ни на жизненный цикл экземпляров сущностей, которые уже загружены в контекст постоянства ** , поэтому рекомендуется выполнять их до загрузки затронутых сущностей.
Удаление с использованием собственных запросов
Иногда нам нужно обратиться к собственным запросам, чтобы достичь чего-то, что не поддерживается Hibernate или специфично для поставщика базы данных.
Мы также можем удалить данные в базе данных с помощью собственных запросов:
Foo foo = new Foo(«foo»);
entityManager.persist(foo);
flushAndClear();
entityManager.createNativeQuery(«delete from FOO where ID = :id»)
.setParameter(«id», foo.getId())
.executeUpdate();
assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());
Та же рекомендация применяется к собственным запросам, что и к операторам JPA-стиля DML, т. Е.
Собственные запросы не влияют ни на состояние, ни на жизненный цикл экземпляров сущностей, которые загружаются в контекст постоянства до выполнения запросов
.
Мягкое удаление
Часто нежелательно удалять данные из базы данных из-за целей аудита и ведения истории. В таких ситуациях мы можем применить метод, называемый мягким удалением. По сути, мы просто помечаем строку как удаленную и отфильтровываем ее при извлечении данных.
Следующий тест подтверждает, что все работает как положено:
Foo foo = new Foo(«foo»);
entityManager.persist(foo);
flushAndClear();
foo = entityManager.find(Foo.class, foo.getId());
foo.setDeleted();
flushAndClear();
assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());
Заключение
В этой статье мы рассмотрели различные способы удаления данных с помощью Hibernate. Мы объяснили основные концепции и некоторые лучшие практики. Мы также продемонстрировали, как программные удаления могут быть легко реализованы с помощью Hibernate.
Реализация этого Руководства по удалению объектов с помощью Hibernate доступна на
over на Github
. Это проект, основанный на Maven, поэтому его легко импортировать и запускать как есть.
JPA вводит интересный инструмент — запросы на языке Java Persistence Query Language.
Этот язык похож на SQL, но использует объектную модель Java, а не SQL таблицы. Рассмотрим пример:
Как мы видим, в запросе мы использовали указание на сущность Category, а не таблицу. А так же на поле этой сущности title. J PQL предоставляет множество полезных возможностей и претендует на отдельную статью. Подробнее можно ознакомиться в обзоре:
Добавление JPA
Итак, как мы помним, JPA — это про то, что мы будем сохранять что-то в БД. Следовательно, нам нужна база данных. Чтобы использовать подключение к БД в своём проекте нам нужно добавить в зависимости библиотеку, для подключения к БД.
Как мы помним, мы использовали Gradle, который создал нам билд скрипт build.gradle. В нём то мы и опишем зависимости, которые нужны нашему проекту. Зависимости — это те библиотеки, без которых не может работать наш код.
Начнём с описания зависимости от подключения к БД. Делаем это так же, как бы делали, работая просто с JDBC:
Теперь у нас есть БД. Мы можем теперь добавить в наше приложение уровень или слой (layer), отвечающий за отображение наших Java объектов в понятия базы данных (с Java языка на язык SQL). Как мы помним, мы собираемся использовать для этого реализацию спецификации JPA под названием Hibernate:
Теперь нам надо сконфигурировать JPA. Если мы прочитаем спецификацию и раздел «8.1 Persistence Unit», то мы узнаем, что Persistence Unit — это некоторая некоторое объединение конфигураций, метаданных и сущностей. И чтобы JPA заработал, нужно описать хотя бы один Persistence Unit в конфигурационном файле, который имеет название persistence.xml. Его расположение описано в главе специфкиации «8.2 Persistence Unit Packaging». Согласно этому разделу, если у нас Java SE окружение, то мы должны положить его в корень каталога META-INF.

Содержание скопируем из примера, приведённого в спецификации JPA в главе «8.2.1 persistence.xml file»:
Но этого мало. Надо рассказать, кто наш JPA Provider, т.е. тот, кто реализует спецификацию JPA:
А теперь добавим настройки (properties). Часть из них (начинаются на javax.persistence) являются стандартными JPA конфигурациями и описаны в спецификации JPA в разделе «8.2.1.9 properties». Часть конфигураций являются провайдер-специфичными (в нашем случае, влияют на Hibernate как на Jpa Provider’а. Наш блок настроек будет выглядеть так:
Теперь у нас есть JPA-совместимый конфиг persistence.xml, есть JPA провайдер Hibernate и есть база данных H2, а так же есть 2 класса, который являются нашей доменной моделью. Давайте заставим это всё наконец-то отработать.
В каталоге /test/java наш Gradle любезно нам сгенерировал шаблон для Unit тестов и назвал его AppTest. Давайте используем его.
Как гласит глава «7.1 Persistence Contexts» спецификации JPA, сущности в мире JPA живут в некотором пространстве, которое называется «Контекст персистенции» (или Контексте постоянства, Persistence Context). Но напрямую мы не работаем с Persistence Context. Для этого мы используем Entity Manager или «менеджер сущностей». Именно он знает про контекст и про то, какие там живут сущности. Мы же взаимодействуем с Entity Manager’ом.
Тогда остаётся только понять, откуда нам достать этот Entity Manager?
Согласно главе «7.2.2 Obtaining an Application-managed Entity Manager» спецификации JPA мы должны использовать EntityManagerFactory. Поэтому, вооружимся спецификацией JPA и возьмём пример из главы «7.3.2 Obtaining an Entity Manager Factory in a Java SE Environment» и оформим его в виде простейшего Unit теста:
Уже этот тест покажет ошибку «Unrecognized JPA persistence.xml XSD version». Причина — в persistence.xml нужно правильно указать используемую схему, как это сказано в спецификации JPA в разделе «8.3 persistence.xml Schema»:
Кроме того, важен порядок элементов. Поэтому, provider должен быть указан до перечисления классов. После этого тест выполнится успешно. Непосредственное подключение JPA мы выполнили.
Прежде чем мы будем двигаться дальше, подумаем про остальные тесты. Каждый наш тест будет требовать EntityManager. Давайте сделаем так, чтобы у каждого теста был свой EntityManager на начало выполнения. Кроме того, мы хотим чтобы БД каждый раз была новая. Благодаря тому, что мы используем inmemory вариант, достаточно закрывать EntityManagerFactory.
Создание Factory — дорогая операция. Но для тестов — это оправдано. J Unit позволяет задать методы, которые будут выполняться перед (Before) и после (After) выполнением каждого теста:
Теперь, перед выполнением любого теста будет создана новая EntityManagerFactory, что повлечёт за собой создание новой БД, т.к. hibernate.hbm2ddl.auto имеет значение create. А из новой фабрики получим новый EntityManager.

Проблемы отображения объектной модели в реляционную
Но начнем все же с основ ORM. O RM — объектно-реляционное отображение — соответственно у нас есть реляционная и объектная модели. И при отображении одной в другую существуют проблемы, которые нам нужно решить самостоятельно. Давайте их разберем.
Для иллюстрации возьмем следующий пример: у нас есть сущность “Пользователь”, который может быть либо джедаем либо штурмовиком. У джедая обязательно должна быть сила, а у штурмовика специализация. Ниже приведена диаграмма классов.
Проблема 1. Наследование и полиморфные запросы.
В объектной модели есть наследование, а в реляционной нет. Соответственно это и первая проблема — как правильно отобразить наследование в реляционную модель.
Hibernate предлагает 3 варианта ее отображения такой объектной модели:
Проблема 2. Отношение композиции в ООП
Возвращаясь к нашему примеру заметим, что в объектной модели мы вынесли профиль пользователя в отдельную сущность — Profile. Но в реляционной модели мы не стали выделять под нее отдельную таблицу.
Отношение OneToOne чаще является плохой практикой, т.к. в селекте у нас появляется неоправданный JOIN (даже указав fetchType=LAZY в большинстве случаев у нас JOIN останется — эту проблему обсудим позже).
Entity Lifecycle
Жизненный цикл сущностей описан в спецификации JPA в главе «3.2 Entity Instance’s Life Cycle». Т.к. сущности живут в контексте и ими управляет EntityManager, то говорят, что сущности управляемые, т.е. managed.
Давайте посмотрим на этапы жизни сущности:
// 1. New или Transient (временный)
Category cat = new Category();
cat.setTitle(«new category»);
// 2. Managed или Persistent
entityManager.persist(cat);
// 3. Транзакция завершена, все сущности в контексте detached
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
// 4. Сущность изымаем из контекста, она становится detached
entityManager.detach(cat);
// 5. Сущность из detached можно снова сделать managed
Category managed = entityManager.merge(cat);
// 6. И можно сделать Removed. Интересно, что cat всё равно detached
entityManager.remove(managed);
И вот для закрепления схемка:

Сущности (Entities)
Как мы помним, мы создали ранее классы, описывающие нашу доменную модель. Мы уже говорили, что это наши «сущности». Это и есть Entity, которыми мы будем управлять при помощи EntityManager. Напишем простой тест по сохранению сущности категории:
Но сразу этот тест не заработает, т.к. мы получим различные ошибки, которые нам помогут понять, что такое сущности:
Таким образом, чтобы класс категории стал сущностью мы должны выполнить следующие изменения:
Но если мы его сейчас выполним, то получим ошибку: no transaction is in progress. И тут наступает пора узнать про то, как JPA использует транзакции.
Set, Bag, List
В hibernate есть 3 основных способа представить коллекцию связи OneToMany.
Для Bag в java core нет класса, который бы описывал такую структуру. Поэтому все List и Collection — являются bag-ом если не указана колонка, по которой наша коллекция будет сортироваться(аннотация OrderColumn. Не путать с SortBy). Использовать аннотацию OrderColumn крайне не рекомендую в силу плохой (на мой взгляд) реализации фичи — не оптимальные sql запросы, возможное наличие NULL-ов в листе.
Возникает вопрос, а что все-таки лучше использовать bag или set? Начнем с того, что при использовании bag-а возможны следующие проблемы:
// используем bag
spaceCraft.getCrew().add( luke ); // весь экипаж не загружается из бд
// используем set
spaceCraft.getCrew().put( luke ); // весь экипаж загружается из бд
// хотя вышеописанный вариант связывания мне не очень нравится. На мой взгляд связь ManyToOne удобнее указывать так:
luke.setCurrentSpaceCraft( spaceCraft );
Создание проекта
Так как JPA — это про Java, то нам понадобится Java проект.
Мы могли бы сами вручную создать структуру каталогов, сами добавить нужные библиотеки. Но куда удобнее и правильнее использовать системы автоматизации сборки проектов (т.е. по сути это просто программа, которая за нас будет управлять сборкой проектов. Создавать каталоги, подкладывать в classpath нужные библиотеки и т.д.). Одной из такой систем является Gradle.
Подробнее про Gradle можно прочитать здесь: «Краткое знакомство с Gradle».
Как мы знаем, функциональность Gradle (т.е. действия, которые он может сделать) реализованы при помощи различных Gradle Plugins. Воспользуемся Gradle и плагином «Gradle Build Init Plugin’ом».
Выполним комманду:
gradle init —type java-application
Gradle за нас сделает нужную структуру каталогов, создаст базовое декларативное описание проекта в билд скрипте build.gradle.
Итак, у нас появилось приложение. Нам надо подумать, что мы хотим описывать или моделировать нашим приложением.
Давайте воспользуемся каким-нибудь средством моделирования, например: app.quickdatabasediagrams.com

Тут стоит сказать, что то что мы описали нашу «доменную модель». Домен — это некоторая «предметная область». Вообще, домен — это «владение» на латыни. В средние века так назывались области, которыми владели короли или феодалы. А во французском языке это стало словом «domaine», которое переводится просто как «область».
Таким образом мы описали нашу «доменную модель» = «предметную модель». Каждый элемент этой модели — это некоторая «сущность», что-то из реальной жизни. В нашем случае это сущности: Категория (Category), Тема (Topic).
Создадим для сущностей отдельный пакет, например с именем model. И добавим туда Java классы, описывающие сущности.
В Java коде такие сущности представляют из себя обычный POJO, который может выглядеть так:
Скопируем содержимое класса и сделаем по аналогии класс Topic. Отличаться он будет лишь тем, что он знает про категорию, к которой относится. Поэтому, добавим в класс Topic поле категории и методы работы с ней:
Теперь у нас есть Java приложение, имеющее свою доменную модель. Пора теперь приступать к подключению к проекту JPA.
Дата и время
Не смотря на то что в java 8 появилось прекрасное API для работы с датой и временем, JDBC API по прежнему позволяет работать только со старым API дат. Поэтому разберем некоторые интересные моменты.
Во-первых, нужно четко понимать отличия LocalDateTime от Instant и от ZonedDateTime. ( Не буду растягивать, а приведу отличные статьи на эту тему: первая и вторая)
LocalDateTime и LocalDate представляют обычный кортеж чисел. Они не привязаны к конкретному времени. Т.е. время посадки самолета хранить в LocalDateTime нельзя. А дату рождения через LocalDate вполне нормально. Instant же представляет точку во времени, относительно которой мы можем получить локальное время в любой точке на планете.
Более интересный и важный момент — как даты сохраняется в базу данных. Если у нас проставлен тип TIMESTAMP WITH TIMEZONE то проблем быть не должно, если же стоит TIMESTAMP (WITHOUT TIMEZONE) то есть вероятность, что дата запишется/прочитается неверная. (за исключением LocalDate и LocalDateTime)
Давайте разберемся почему:
Когда мы сохраняем дату, используется метод со следующей сигнатурой:
setTimestamp(int i, Timestamp t, java.util. Calendar cal)
Как видим тут используется старое API. Дополнительный аргумент Calendar нужен для того, чтобы преобразовать timestamp в строковое представление. т.е он хранит в себе timezone-у. Если Calendar не передается, то используется Calendar по-умолчанию с таймзоной JVM.
Решить эту проблему можно 3 способами:
Интересный вопрос, почему LocalDate и LocalDateTime не подпадают под такую проблему?
Для ответа на этот вопрос нужно понимать структуру класса java.util. Date (java.sql. Date и java.sql. Timestamp его наследники и их отличия в данном случае нас не волнуют). Date хранит дату в миллисекундах c 1970 года грубо говоря в UTC, но метод toString преобразует дату согласно системной timeZone.
Соответственно, когда мы получаем из базы данных дату без таймзоны, она отображатеся в объект Timestamp, так чтобы метод toString отобразил ее желаемое значение. При этом количество миллисекунд с 1970-го года может отличаться (в зависимости от временной зоны). Именно поэтому только локальное время отображается всегда корректно.
Также привожу в пример код, ответственный за преобразование Timesamp в LocalDateTime и Instant:
// LocalDateTime
LocalDateTime.ofInstant( ts.toInstant(), ZoneId.systemDefault() );
// Instant
ts.toInstant();
Каждый экземпляр EntityManager-а (EM) определяет сеанс взаимодействия с базой данных. В рамках экземпляра EM-а, существует кэш первого уровня. Тут я выделю следующие значимые моменты:
JPA Transactions
Как мы помним, в основе JPA лежит понятие контекст персистенции (Persistence Context). Это место, где живут сущности. А мы управляем сущностями через EntityManager. Когда мы выполняем комманду persist, то мы помещаем сущность в контекст. Точнее, мы говорим EntityManager’у, что это нужно сделать.
Но контекст этот — это просто некоторая область хранения. Его даже иногда называют «кэшем первого уровня». Но его нужно соединить с базой данных. Комманда flush, которая ранее у нас упала с ошибкой, синхронизирует данные из контекста персистенции с БД. Но для этого требуется транспорт и этим транспортом является транзакция.
Транзакции в JPA описаны в разделе спецификации «7.5 Controlling Transactions». Для использования транзакций в JPA есть специальный API:
Необходимо добавить управление транзакциями в наш код, который выполняется до тестов и после:
После добавления мы увидим в логе insert выражение на языке SQL, которых ранее не было:

Изменения, накопленные в EntityManager было при помощи транзакции закоммичены (подтверждены и сохранены) в БД.
Давайте попробуем теперь найти нашу сущность. Создадим тест на поиск сущности по её ID:
В этом случае мы получим ранее сохранённую нами сущность, но в логе мы не увидим SELECT запросов.
А всё по тому, что мы говорим: «Менеджер сущностей, найди пожалуйста мне сущность Категория с ID=1». А менеджер сущностей сначала смотрит у себя в контексте (использует его своего рода кэш), и только если не находит — идёт искать в БД. Стоит изменить ID на 2 (такого нет, мы сохранили только 1 экземпляр), как мы увидим, что SELECT запрос появляется. Потому что в контексте не найдено сущностей и EntityManager пытается найти сущность БД.
Существуют разные комманды, которыми мы можем управлять состоянием сущности в контексте. Переход сущности из одного состояния в другое называется жизненным циклом сущности — lifecycle.
Mapping
В JPA мы можем описать отношения сущностей между друг другом.
Вспомним, что мы уже разбирали отношения сущностей между друг другом, когда мы разбирались с нашей доменной моделью. Тогда мы использовали ресурс quickdatabasediagrams.com:

Установление связей между сущностями называется маппингом или ассоциированием (Association Mappings).
Виды ассоциаций, которые могут быть установлены при помощи JPA представлены ниже:
Давайте посмотрим на сущность Topic, которая описывает тему. Что мы можем сказать про отношение Topic к Category?
Много Topic будут принадлежать одной категории. Следовательно, нам нужна ассоциация ManyToOne. Выразим эту связь на языке JPA:
Чтобы запомнить, какие аннотации ставить, можно запомнить, что последняя часть отвечает за поле, над которым указана аннотация. ToOne — конкретный экземпляр. ToMany — коллекции.
Сейчас у нас связь односторонняя. Давайте сделаем из неё двустороннюю связь. Добавим в Category знание о всех Topic, которые входят в эту категорию.
Оканчиваться должен на ToMany, потому что у нас список Topic. То есть отношение «Ко многим» темам.
Остаётся вопрос — OneToMany или ManyToMany:

На эту же тему хороший ответ можно прочитать тут: «Explain ORM oneToMany, manyToMany relation like I’m five».
Если категория имеет связь с ToMany топиков, то каждый из этих топиков может иметь только одну категорию, то будет One, а иначе Many. Таким образом, в Category список всех тем будет выглядеть следующий образом:
И не забудем в сущности Category описать геттер для получения списка всех тем:
Двунаправленные отношения — очень сложный для автоматического отслеживания момент. Поэтому, JPA перекладывает эту обязанность на разработчика. Для нас это означает, что когда мы устанавливаем в сущности Topic связь с Category, мы должны обеспечить непротиворечивость данных самостоятельно. Делается это просто:
Напишем для проверки простой тест:
Маппинг — это целая отдельная тема. В рамках данного обзора следует понять, при помощи каких средств это достигается. Подробнее про маппинг можно прочитать тут:

Batching
По-умолчанию запросы отправляются в БД по одному. При включении batching-а hibernate сможет в одном запросе к БД отправлять несколько statement-ов. (т.е. batching сокращает количество round-trip-ов к БД)
Для этого необходимо:
Так же напомню, про эффективность операции em.clear() — она отвязывает сущности от em-а, тем самым вы освобождаете память и сокращаете время на операцию dirty checking.
Если мы используем postgres, то можно так же сказать hibernate использовать multi-raw insert.
Генераторы
Генераторы нужны для описания, каким способом первичные ключи наших сущностей будут получать значения. Давайте быстро пробежимся по вариантам:
Поговорим немного подробнее про sequence. С целью повысить скорость работы hibernate использует разные алгоритмы-оптимизаторы. Все они нацелены на уменьшение количества общений с БД (количество round-trip-ов). Давайте посмотрим на них чуть подробнее:
Теперь давайте разберемся, как выбирается оптимизатор. У hibernate есть несколько генераторов sequence. Нам будет интересно 2 из них:
Deadlock
Давайте разберем на примере псевдокода ситуацию, которая может привести к deadlock-у:
Thread #1:
update entity(id = 3)
update entity(id = 2)
update entity(id = 1)
Thread #2:
update entity(id = 1)
update entity(id = 2)
update entity(id = 3)
Для предотвращения таких проблем у hibernate есть механизм, который позволяет избежать deadlock-ов такого типа — параметр hibernate.order_updates. В этом случае все update-ы будут упорядочены по id и выполнены. Также еще раз упомяну, что hibernate старается “отсрочить” захват коннекшена и выполнение insert-ов и update-ов.
Тестирование
В идеале development окружение должно предоставлять как можно больше полезной информации о работе hibernate и о взаимодействии с БД. А именно:
Из полезных утилит можно выделить следующее:
Но еще раз повторюсь, что это только для development, на production это включать не стоит.
Criteria API
И напоследок хотелось бы затронуть Criteria API. J PA вводит инструмент динамического построения запросов. Пример использования Criteria API:
Данный пример равносилен выполнению запроса «SELECT c FROM Category c».
Criteria API — мощный инструмент. Подробнее про него можно прочитать здесь:
Сила References
Reference — это ссылка на объект, загрузку которого мы решили отложить. В случае отношения ManyToOne с fetchType=LAZY, мы получаем такой reference. Инициализация объекта происходит в момент обращения к полям сущности, за исключением id (т.к. значение этого поля нам известно).
Стоит отметить, что в случае Lazy Loading-а reference всегда ссылается на существующую строку в БД. Именно по этой причине большинство случаев Lazy Loading-а в отношениях OneToOne не работает — hibernate необходимо сделать JOIN для проверки существования связи и JOIN уже был, то hibernate загружает его в объектную модель. Если же мы укажем в OneToOne связи nullable=true, то LazyLoad должен заработать.
Мы можем и самостоятельно создать reference, используя метод em.getReference. Правда в таком случае нет гарантии, что reference ссылается на существующую строку в БД.
Давайте приведем пример использования такой ссылки:
На всякий случай напомню, что мы получим LazyInitializationException в случае закрытого EM-а или отсоединенной(detached) ссылки.





