Управление сущностями в Hibernate

61dYcНаписав о отображении классов в таблицы можно написать и о работе с этими классами и таблицами.  Для управления сущностями использует подход, схожий с JPA, когда каждая сущность имеет какое-то собственное состояние, а вызовами методов Hibernate это состояние изменяется, при этом одновременно изменяя и обновляя данные в JVM и в базе данных .

Bootstrapping

Как и всё остальное, Hibernate имеет начало и это начало называется Bootstrapping. Hibernate Boostrapping или, говоря простыми словами, запуск и конфигурирование Hibernate, выполняется в три шага.

В первую очередь необходимо создать ServiceRegistry, которая создаёт и предоставляет сервисы, которые нужны Hibernate для старта и дальнейшей работы. Можно построить ServiceRegistry самостоятельно, а можно воспользоваться стандартной. Второй вариант, очевидно, проще:

Вызывая различные методы класса StandardServiceRegistryBuilder можно управлять, какой будет построенная  ServiceRegistry. В частности именно тут можно изменить имя файла конфигурации со значения по умолчанию hibernate.cfg.xml на своё собственное.

Следующим шагом настраивается связь между классами и таблицами:

Создание MetadataSources оборачивается в try/catch блок, чтобы очистить ServiceRegistry если MetadataSources не сможет создаться. В случае успеха же  MetadataSource сам будет управлять жизненным циклом ServiceRegistry.

Создать MetadatsSources можно и без ServiceRegistry, но тогда не будут автоматически прочитаны определения сущностей из конфигурации Hibernate и их придётся задавать вручную:

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

Наконец, последним шагом, из MetadataSources строится SessionFactory:

Session

Из SessionFactory, построенной при запуске Hibernate, можно получить одну или несколько сессий. Если проводить аналогию с JDBC, то SessionFactory будет аналогом DataSource, а Session  — аналогом Connection. Для нас главное различие в том, что SessionFactory объект достаточно тяжёлый и его создание занимает довольно много времени. Поэтому в программе обычно используется один экземпляр SessionFactory. В тоже время объект Session весьма легковесный и конструируется быстро, поэтому их создают по мере необходимости.

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

Контекст сессии понятие достаточно эфемерное. Один или несколько объектов Session могут образовывать persistence context Я не буду переводить этот термин, попробую лучше его объяснить. Наличие persistence context означает, что для каждой существующей на данный момент сущности существует Session, которая с сущностью связана и следит за её состоянием. Что это значит? Смотри ниже.

Сущности и состояния

В Hibernate каждая сущность имеет своё состояние и может менять его подчиняясь строгим правилам перехода из состояния в состояние. Эти состояния и правила отображены на рисунке в начале статьи.

Когда будущая сущность создаётся оператором new, она чиста и невинна и Hibernate про неё ещё не знает. Такая свежесозданная сущность считается «транзитной» (transient). Транзитная сущность не имеет связи с базой данных, не имеет данных в базе данных и, за редким исключением, не имеет id.

Такая сущность довольно бесполезна, поэтому сразу переведём её в состояние «постоянная» (persistent):

Метод save() возвращает id, который был присвоен сущности в момент сохранения в базу. Кроме save() есть ещё несколько методов, способных перевести сущность из «transient» в «persistent»: saveOrUpdate() позволяет не задумываться, новая это сущность или просто изменённая старая и сам выбирает, сохранить новую или обновить существующую; persist() ведёт себя как save(), но он не возвращает id вновь сохранённой сущности.

В отличие от JPA, Hibernate сам (по умолчанию) не следит за изменениями в объектах и требует явного обновления сущности в базе данных, когда она меняется в коде:

Когда объект op был сохранён вызовом save() и в нём после этого были изменены данные вызовом op.setAccountId(9000), эти изменения не попали автоматически в базу данных, даже после того как транзакция была подтверждена и закрыта. Hibernate передаёт изменения объектов в базу только явно, с помощью вызовов update() или saveOrUpdate().

Чтение из базы

Разумеется, создание новых объектов и сохранение их в базу, это не единственный метод сделать какой-то объект «persistent» и, строго говоря, даже не самый популярный. Чаще всего объекты наоборот, читаются из базы данных с помощью методов get() и load(). Оба метода принимают одинаковый набор параметров и в общем случае это будет тип загружаемого класса и его id:

Отличаются эти методы лишь поведением: get() возвращает null, если объекта указанного типа и с указанным id в базе не нашлось. В то же время load() бросит исключение при попытке загрузить несуществующий объект. А если быть точным, то не при попытке загрузить объект, а при попытке обратиться к данным сущности. Дело в том, что load() загружает данные из базы «лениво», откладывая фактическую загрузку на момент обращения к этим данным. А если к данным не обращаться, то и фактического запроса к базе тоже не произойдёт

Ленивая загрузка

Когда сущность стала persistent, она обретает связь с объектом Session, который её породил и включается в persistent context.  Метод load() возвращает на самом деле не объект запрошенного класса, а ссылку на обёртку вокруг этого класса, которая знает, как ей обратиться к Session и загрузить данные в тот момент, когда они понадобятся. Очевидно, что такая обёртка бесполезна за пределами своего peristence context.

Однако не только load() может вернуть неполный класс. Любое поле может быть помечено для ленивой загрузки аннотацией @Basic( fetch = FetchType.LAZY ) и даже если загрузить объект с таким полем методом get(), для доступа к этому полю всё равно будет требоваться наличие persistence context. Используя аннотацию @LazyGroup("groupname") можно объединять лениво загружаемые поля в группы и тогда обращение к одному полю в группе автоматически вызовет загрузку всех полей в этой группе.

По умолчанию у любого класса, который не указывает явно, какие поля стоит загружать отложенно и как такие поля группировать, все поля которые содержат единственное значение, такие как String, BigDecimal, Long и другие, объединены в группу загрузки по умолчанию. В свою очередь поля с множественными значениями, такие как Set, List и другие, имеют собственную группу отложенной загрузки для каждого поля.

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

Detached entities

Итак, когда сущность только создана и записана в базу данных или когда наоборот, прочитана из базы данных, она входит в persistence context и обладает неким экземпляром Session, который ей управляет. Однако из этого состояния она может внезапно перейти в состояние «отделённая» (detached). В этом состоянии сущность не связана со своим контекстом (отделена от него) и нет экземпляра Session, который бы ей управлял.

Перейти в это состояние сущность может по следущим причинам:

  • Явный перевод из persisted в detached вызовом метода evict() у Session.
  • Сброс контекста методом clear() у Session.
  • Явное закрытие сессии методом close().
  • Неявное закрытие сессии связанное с удалением объекта Session.

Над detached объектом нельзя выполнять операции, которые требуют наличия persistence context:

detached сущность можно вернуть в состояние persisted  вызовами merge(), lock() или update(), но не saveOrUpdate()!

Удаление сущностей

Последняя важная деталь в управлении сущностями, это их удаление. Удаляется сущность методом delete(), который принимает в себя объект класса, который необходимо удалить, и помечает его на удаление. С точки зрения Hibernate удалённая сущность переходит в состояние transient и с ней можно дальше работать как с новой сущностью. С тем лишь условием, что id у неё уже заполнен.

Само удаление, скорее всего, не произойдёт в момент вызова delete(), а будет отложено на потом. Обычно такие отложенные операции гарантированно будут завершены с завершением транзакции, но можно и попросить Hibernate выполнить отложенные операции и раньше, вызвав метод flush().

Код примера доступен на github.