Размер шрифта:
Hibernate. Основные принципы работы с сессиями и транзакциями
Play

Hibernate. Основные принципы работы с сессиями и транзакциями

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

Библиотека Hibernate является самой популярной ORM-билиотекой и реализацией Java Persistence API. Часто используется как ORM-провайдер в обычных Java-приложениях, контейнерах сервлетов, в частности, в сервере приложений JBoss (и его потомке WildFly).

1). Объекты-сущности (Entity Objects)

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

Теперь приведём классы-сущности для этих таблиц:

Об аннотациях JPA можно прочитать здесь.

2). Интерфейс Session

Интерфейс org.hibernate.Session является мостом между приложением и Hibernate. С помощью сессий выполняются все CRUD-операции с объектами-сущностями. Объект типа Session получают из экземпляра типа org.hibernate.SessionFactory, который должен присутствовать в приложении в виде singleton.

3). Состояния объектов

Объект-сущность может находиться в одном из 3-х состояний (статусов):

  • transient object. Объекты в данном статусе — это заполненные экземпляры классов-сущностей. Могут быть сохранены в БД. Не присоединены к сессии. Поле Id не должно быть заполнено, иначе объект имеет статус detached ;
  • persistent object. Объект в данном статусе — так называемая хранимая сущность, которая присоединена к конкретной сессии. Только в этом статусе объект взаимодействует с базой данных. При работе с объектом данного типа в рамках транзакции все изменения объекта записываются в базу;
  • detached object. Объект в данном статусе — это объект, отсоединённый от сессии, может существовать или не существовать в БД.
  • persist(Object) — преобразует объект из transient в persistent, то есть присоединяет к сессии и сохраняет в БД. Однако, если мы присвоим значение полю Id объекта, то получим PersistentObjectException — Hibernate посчитает, что объект detached, т. е. существует в БД. При сохранении метод persist() сразу выполняет insert, не делая select.
  • merge(Object) — преобразует объект из transient или detached в persistent. Если из transient, то работает аналогично persist() (генерирует для объекта новый Id, даже если он задан), если из detached — загружает объект из БД, присоединяет к сессии, а при сохранении выполняет запрос update
  • replicate(Object, ReplicationMode) — преобразует объект из detached в persistent, при этом у объекта обязательно должен быть заранее установлен Id. Данный метод предназначен для сохранения в БД объекта с заданным Id, чего не позволяют сделать persist() и merge(). Если объект с данным Id уже существует в БД, то поведение определяется согласно правилу из перечисления org.hibernate.ReplicationMode: ReplicationMode.IGNORE — ничего не меняется в базе. ReplicationMode.OVERWRITE — объект сохраняется в базу вместо существующего. ReplicationMode.LATEST_VERSION — в базе сохраняется объект с последней версией. ReplicationMode.EXCEPTION — генерирует исключение.
  • delete(Object) — удаляет объект из БД, иными словами, преобразует persistent в transient. Object может быть в любом статусе, главное, чтобы был установлен Id.
  • save(Object) — сохраняет объект в БД, генерируя новый Id, даже если он установлен. Object может быть в статусе transient или detached
  • update(Object) — обновляет объект в БД, преобразуя его в persistent (Object в статусе detached)
  • saveOrUpdate(Object) — вызывает save() или update()
  • refresh(Object) — обновляет detached-объект, выполнив select к БД, и преобразует его в persistent
  • get(Object.class, id) — получает из БД объект класса-сущности с определённым Id в статусе persistent

А теперь обратим внимание на аннотации @OneToMany и @ManyToOne в классах-сущностях. Параметр fetch в @OneToMany обозначает, когда загружать дочерние объекты. Может иметь одно из двух значений, указанных в перечислении javax.persistence.FetchType:

FetchType.EAGER — загружать коллекцию дочерних объектов сразу же, при загрузке родительских объектов. FetchType.LAZY — загружать коллекцию дочерних объектов при первом обращении к ней (вызове get) — так называемая отложенная загрузка.

Параметр cascade обозначает, какие из методов интерфейса Session будут распространяться каскадно к ассоциированным сущностям. Например, в классе-сущности User для коллекции tasks укажем:

Тогда при выполнении session.persist(user) или session.merge(user) операции persist или merge будут применены ко всем объектам из tasks. Аналогично для остальных операций из перечисления javax.persistence.CascadeType. CascadeType.ALL применяет все операции из перечисления. Необходимо правильно настроить CascadeType, дабы не подгружать из базы кучу лишних ассоциированных объектов-сущностей.

4). Извлечение объектов из БД

Приведём простой пример:

Вместо метода session.get() можно использовать session.load(). Метод session.load() возвращает так называемый proxy-object. Proxy-object — это объект-посредник, через который мы можем взаимодействовать с реальным объектом в БД. Он расширяет функционал объекта-сущности. Взаимодействие с proxy-object полностью аналогично взаимодействию с объектом-сущностью. Proxy-object отличается от объекта-сущности тем, что при создании proxy-object не выполняется ни одного запроса к БД, т. е. Hibernate просто верит нам, что объект с данным Id существует в БД. Однако первый вызванный get или set у proxy-object сразу инициирует запрос select, и если объекта с данным Id нет в базе, то мы получим ObjectNotFoundException. Основное предназначение proxy-object — реализация отложенной загрузки.

Вызов user.getTasks() инициирует загрузку задач юзера из БД, так как в классе User для tasks установлен FetchType.LAZY.

LazyInitializationException

С параметром FetchType.LAZY нужно быть аккуратнее. Иногда при загрузке ассоциированных сущностей мы можем поймать исключение LazyInitializationException. В вышеуказанном коде во время вызова user.getTasks() user должен быть либо в статусе persistent, либо proxy.

Также LazyInitializationException может вызвать небольшое изменение в нашем коде:

Здесь теоретически всё верно. Но при попытке обращения к tasksList мы МОЖЕМ получить LazyInitializationException. Но в дебагере данный код отрабатывает верно. Почему? Потому, что user.getTasks() только возвращает ссылку на коллекцию, но не ждёт её загрузки. Не подождав, пока загрузятся данные, мы закрыли сессию. Выход — выполнять в транзакции, т. е.:

Выборка с условиями

А теперь приведём несколько простых примеров выборки данных с условиями. Для этого в Hibernate используются объекты типа org.hibernate.Criteria:

Здесь понятно, что мы выполняем select * from user where login='login'. В метод add мы передаём объект типа Criterion, представляющий определённый критерий выборки. Класс org.hibernate.criterion.Restrictions предоставляет множество различных видов критериев. Параметр «login» обозначает название свойства класса-сущности, а не поля в таблице БД. Приведём ещё пару примеров:

Здесь мы выбираем по содержимому свойства name класса-сущности Task. MatchMode.ANYWHERE означает, что нужно искать подстроку name в любом месте свойства «name».

б). А здесь мы получаем 50 строк, начиная с 20-го номера в таблице.

5). Сохранение объектов

Давайте разберём несколько способов сохранения объекта-сущности в базу данных.

а). Создаём transient-object и сохраняем в базу:

Отметим несколько нюансов. Во-первых, сохранение в БД можно производить только в рамках транзакции. Вызов session.openTransaction() открывает для данной сессии новую транзакцию, а session.getTransaction().commit() её выполняет. Во-вторых, в метод task.setUser(user) мы передаём user в статусе detached. Можно передать и в статусе persistent.

Данный код выполнит (не считая получения user) 2 запроса — select nextval('task_task_id_seq') и insert into task. Вместо saveOrUpdate() можно выполнить save(), persist(), merge() — будет также 2 запроса. Вызов session.flush() применяет все изменения к БД, но, если честно, этот вызов здесь бесполезен, так как ничего не сохраняется в БД до commit(), который сам вызовет flush().

Помним, что если мы внутри транзакции что-то изменим в загруженном из БД объекте статуса persistent или proxy-object, то выполнится запрос update. Если task должен ссылаться на нового user, то делаем так:

Внимание: в классе Task для поля user должен быть установлен CascadeType.PERSIST, CascadeType.MERGE или CascadeType.ALL.

Если мы имеем на руках userId существующего в БД юзера, то нам не обязательно загружать объект User из БД, делая лишний select. Так как мы не можем присвоить ID юзера непосредственно свойству класса Task, нам нужно создать объект класса User с единственно заполненными userId. Естественно, это не может быть transient-object, поэтому здесь следует воспользоваться известным нам proxy-объектом.

б). Добавляем объект в коллекцию дочерних объектов:

В User для свойства tasks должен стоять CascadeType.ALL. Если стоит CascadeType.MERGE, то после user.getTasks().add(task) выполнить session.merge(user). Данный код выполнит 3 запроса — select * from user, select nextval('task_task_id_seq') и insert into task

6). Удаление объектов

а). Можно удалить, создав transient-object:

Данный код удалит только task. Однако, если task — объект типа proxy, persistent или detached и в классе Task для поля user действует CascadeType.REMOVE, то из базы удалится также ассоциированный user. Если удалять юзера не нужно, выполнить что? Правильно, task.setUser(null)

б). Можно удалить и таким способом:

Данный код просто удаляет связь между task и user. Здесь мы применили новомодное лямбда-выражение. Объект task удалится из БД при одном условии — если изменить кое-что в классе-сущности User:

Параметр orphanRemoval = true указывает, что все объекты Task, которые не имеют ссылки на User, должны быть удалены из БД.

7). Декларативное управление транзакциями

Для декларативного управления транзакциями мы будем использовать Spring Framework. Управление транзакциями осуществляется через менеджер транзакций. Вместо вызовов session.openTransaction() и session.commit() используется аннотация @Transactional. В конфигурации приложения должно присутствовать следующее:

Здесь мы определили бин transactionManager, к которому привязан бин sessionFactory. Класс HibernateTransactionManager является реализацией общего интерфейса org.springframework.transaction.PlatformTransactionManager для SessionFactory библиотеки Hibernate. annotation-driven указывает менеджеру транзакций обрабатывать аннотацию @Transactional.

— Болтовня ничего не стоит. Покажите мне код. (Linus Torvalds)

Аннотация @Transactional указывает, что метод должен выполняться в транзакции. Менеджер транзакций открывает новую транзакцию и создаёт для неё экземпляр Session, который доступен через sessionFactory.getCurrentSession(). Все методы, которые вызываются в методе с данной аннотацией, также имеют доступ к этой транзакции, потому что экземпляр Session является переменной потока (ThreadLocal). Вызов sessionFactory.openSession() откроет совсем другую сессию, которая не связана с транзакцией.

Параметр rollbackFor указывает исключения, при выбросе которых должен быть произведён откат транзакции. Есть обратный параметр — noRollbackFor, указывающий, что все исключения, кроме перечисленных, приводят к откату транзакции.

Параметр propagation самый интересный. Он указывает принцип распространения транзакции. Может принимать любое значение из перечисления org.springframework.transaction.annotation.Propagation. Приведём пример:

Метод UserDao.getUserByLogin() также может быть помечен аннотацией @Transactional. И здесь параметр propagation определит поведение метода UserDao.getUserByLogin() относительно транзакции метода saveTask():

  • Propagation.REQUIRED — выполняться в существующей транзакции, если она есть, иначе создавать новую.
  • Propagation.MANDATORY — выполняться в существующей транзакции, если она есть, иначе генерировать исключение.
  • Propagation.SUPPORTS — выполняться в существующей транзакции, если она есть, иначе выполняться вне транзакции.
  • Propagation.NOT_SUPPORTED — всегда выполняться вне транзакции. Если есть существующая, то она будет остановлена.
  • Propagation.REQUIRES_NEW — всегда выполняться в новой независимой транзакции. Если есть существующая, то она будет остановлена до окончания выполнения новой транзакции.
  • Propagation.NESTED — если есть текущая транзакция, выполняться в новой, так называемой, вложенной транзакции. Если вложенная транзакция будет отменена, то это не повлияет на внешнюю транзакцию; если будет отменена внешняя транзакция, то будет отменена и вложенная. Если текущей транзакции нет, то просто создаётся новая.
  • Propagation.NEVER — всегда выполнять вне транзакции, при наличии существующей генерировать исключение.
Ну что ж, подведём итоги

В моей статье я осветил самые основные принципы работы с сессиями и транзакциями в Hibernate. Надеюсь, что начинающим Java-программистам статья будет полезна при преодолении первого порога в изучении суперклассной (не для всех, возможно) библиотеки Hibernate. Желаю всем успехов в нашей сложной и интересной программерской деятельности!

📎📎📎📎📎📎📎📎📎📎