Документация разработчика Hibernate – Глава II. Транзакции и контроль многопоточности

Представляю вашему вниманию перевод второй главы официальной документации Hibernate.

Перевод статьи актуален для версии Hibernate 4.2.19.Final

Содержание

 2.1. Определение транзакции
 2.2. Физические транзакции
   2.2.1. Физические транзакции — JDBC
   2.2.2. Физические транзакции — JTA
   2.2.3. Физические транзакции — CMT
   2.2.4. Физические транзакции — Прочее
   2.2.5. Физические транзакции — Устаревшее
 2.3. Применение транзакций Hibernate
 2.4. Паттерны и анти-паттерны транзакций
   2.4.1. Анти-паттерн сессия-на-операцию
   2.4.2. Паттерн сессия-на-запрос
   2.4.3. Диалоги (Conversations)
 2.5. Идентичность объекта
 2.6. Общие вопросы

2.1. Определение транзакции


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

  • Может иметь отношение к физическим транзакциям БД.
  • Может иметь отношение к логическому понятию “транзакция”, как связанному с контекстом персистентности
  • Может отсылать нас к понятию Unit-of-Work, как вполне определенному архитектурному шаблону.


Важно

В данной документации рассматривается логическое и физическое понятия транзакции как одно понятие

2.2. Физические транзакции


Hibernate использует JDBC API для персистентности. В мире Java есть два хорошо определенных механизма работы с транзакциями: непосредственно JDBC и JTA. Hibernate поддерживает оба механизма интеграции с транзакциями и позволяет приложениям управлять физическими транзакциями.

Первая концепция в понимании поддержки транзакций в Hibernate – это интерфейс org.hibernate.engine.transaction.spi.TransactionFactory, который предоставляет две основные функции:

  • Он позволяет Hibernate понимать семантику транзакций текущего окружения. Работаем ли мы сейчас в окружении JTA? Является ли физическая транзакция в данной момент уже активной, и.т.д
  • Он выступает как фабрика экземпляров org.hibernate.Transaction, используемых приложением для управления и проверки состояния транзакций, org.hibernate.Transaction – понятие логической транзакции в Hibernate's. JPA имеет похожее понятие в интерфейсе javax.persistence.EntityTransaction.


Важно

javax.persistence.EntityTransaction доступен только тогда, когда вы используете resource-local транзакции. Hibernate предоставляет доступ к org.hibernate.Transaction в независимости от окружения.


org.hibernate.engine.transaction.spi.TransactionFactory – стандартный сервис Hibernate. Cм. Подробности в секции 7.5.16, “org.hibernate.engine.transaction.spi.TransactionFactory”.

2.2.1. Физические транзакции — JDBC


Управление транзакциями при помощи JDBC достигается методами java.sql.Connection.commit() и java.sql.Connection.rollback() (JDBC не определяет явного метода для инициирования транзакции). В Hibernate, данный подход представлен классом org.hibernate.engine.transaction.internal.jdbc.JdbcTransactionFactory

2.2.2. Физические транзакции — JTA


JTA-подход к транзакциям достигается интерфейсом javax.transaction.UserTransaction, получаемым из API org.hibernate.service.jta.platform.spi.JtaPlatform. Этот подход представлен классом org.hibernate.engine.transaction.internal.jta.JtaTransactionFactory
См. по интеграции с JTA Секция 7.5.9,“org.hibernate.service.jta.platform.spi.JtaPlatform”

2.2.3. Физические транзакции — CMT


Другой JTA-ориентированный подход к транзакциям использует интерфейс javax.transaction.TransactionManager, получаемый из API org.hibernate.service.jta.platform.spi.JtaPlatform. Этот подход представлен классом org.hibernate.engine.transaction.internal.jta.CMTTransactionFactory. В актуальном окружении JEE CM, доступ до javax.transaction.UserTransaction закрыт.

Важно

Термин CMT потенциально может ввести в заблуждение. Важная часть заключается в том, что физические JTA-транзакции управляются каким-то другим средством, отличным от API Hibernate.


См. по интеграции с JTA Секция 7.5.9,“org.hibernate.service.jta.platform.spi.JtaPlatform”.

2.2.4. Физические транзакции — Прочее


Также возможно подключить пользовательское решение по управлению транзакциями, реализовав контракт org.hibernate.engine.transaction.spi.TransactionFactory. Инициатор службы по-умолчанию имеет встроенную поддержку распознавания пользовательских решений через hibernate.transaction.factory_class, которое может указывать на:

  • Экземпляр org.hibernate.engine.transaction.spi.TransactionFactory.
  • Имя класса-реализации org.hibernate.engine.transaction.spi.TransactionFactory. Класс-реализация должен иметь конструктор без аргументов.

2.2.5. Физические транзакции — Устаревшее


Большинство их тех классов, названных выше, были перенесены в новые пакеты во время разработки версии 4.0. Для помощи при переходе на новую версию, Hibernate будет распознавать устаревшие имена на непродолжительный период времени.

  • org.hibernate.transaction.JDBCTransactionFactory маппится на org.hibernate.engine.transaction.internal.jdbc.JdbcTransactionFactory
  • org.hibernate.transaction.JTATransactionFactory маппится на org.hibernate.engine.transaction.internal.jta.JtaTransactionFactory
  • org.hibernate.transaction.CMTTransactionFactory маппится на org.hibernate.engine.transaction.internal.jta.CMTTransactionFactory

2.3. Применение транзакций Hibernate


Hibernate использует соединения JDBC и ресурсы JTA напрямую, без дополнительной логики синхронизации. Для вас важно ознакомится с JDBC, ANSI SQL, и спецификой изоляции транзакций в вашей СУБД.
Hibernate не проводит синхронизацию на объектах в памяти. Поведение, определенное уровнем изоляции ваших БД-транзакций не меняется, когда вы используете Hibernate. Объект org.hibernate.Session выступает как предоставляющий повторяющиеся чтения (repeatable reads) и запросы кэш, ограниченный пределами транзакции.

Важно

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

2.4. Паттерны и анти-паттерны транзакций


2.4.1. Анти-паттерн сессия-на-операцию


Это анти-паттерн про открытие и закрытие объекта Session на каждую операцию к БД в одном потоке. Это также анти-паттерн в терминах транзакций БД. Группируйте ваши вызовы в одну запланированную последовательность. Также, не делайте авто-коммит транзакции на каждое SQL-выражение. Hibernate выключает, или ожидает, что сервер приложений немедленно выключит режим авто-коммита. Транзакции к БД никогда не являлись чем-то необязательным. Все коммуникации с БД должны быть обернуты в транзакцию. Избегайте авто-коммита при чтении данных, потому как довольно редко множество небольших транзакций будут работать быстрее, чем одна должным образом определенная транзакция. К тому же, такое множество транзакций трудно поддерживать и расширять.

Важно

Использование автокоммита не обязательно приводит к использованию БД-транзакций на каждое выражение. Вместо этого, в режиме автокоммита, драйверы JDBC просто проводят каждый вызов в рамках неявного вызова транзакции. Это тоже самое, как если бы ваше приложение проводило вызов commit() транзакции после каждого вызова JDBC.


2.4.2. Паттерн сессия-на-запрос


Наиболее распространенный паттерн транзакций. Термин “запрос” здесь следует понимать в контексте системы, реагирующей на серии запросов от пользователя/клиента. Веб-приложения является основным примером таких систем, но, конечно, не только они одни. На этапе начала обработки запроса, приложение открывает объект Session, инициирует транзакцию, проводит всю сопутствующую работу с данными, завершает транзакцию и закрывает Session. Суть паттерна – это отношение один-к-одному между транзакцией и сессией.
В рамках паттерна есть распространенная техника определения текущей сессии для упрощения передачи этой Session между компонентами приложения. Hibernate предоставляет поддержку данной техники через метод getCurrentSession() класса SessionFactory. Концепция «текущей» сессии должна иметь область видимости, которая определяет границы, в которых определение “текущая” верно. Это задача контракта org.hibernate.context.spi.CurrentSessionContext. Есть две надежно определенных области видимости:

  • JTA транзакция, которая через callback может дать знать Hibernate, когда она завершилась, что в свою очередь предоставляет возможность завершить текущую сессию. Данная стратегия представлена org.hibernate.context.internal.JTASessionContext – реализацией контракта org.hibernate.context.spi.CurrentSessionContext. С использованием этой реализации, Session будет открыт, как только вызовется getCurrentSession() в пределах транзакции.
  • Цикл запроса сам по себе. Лучше всего представлено org.hibernate.context.internal.ManagedSessionContext — реализацией контракта org.hibernate.context.spi.CurrentSessionContext. Здесь есть внешний компонент, ответственный за управлением жизненным циклом и областью видимости “текущей” сессии. На этапе старта области видимости, метод bind() вызывается у ManagedSessionContext с передачей ссылки на сессию. В конце, вызывается метод unbind().


Важно

Метод getCurrentSession() имеет одну неприятную сторону в JTA. Если вы используете его, after_statement режим освобождения соединений также будет использоваться по умолчанию. Из-за ограничений JTA, Hibernate не может автоматически очищать любой незакрытый экземпляр ScrollableResults или Iterator, возвращаемых scroll() или iterate(). Освобождение курсоров БД осуществляется вызовом ScrollableResults.close() или Hibernate.close(Iterator) явно из секции finally.


2.4.3. Диалоги


Паттерн сессия-на-запрос не является единственным средством дизайна units of work. Множество бизнес-процессов требуют всей серии взаимодействий с пользователем, которые чередуются с доступом к БД. В веб- и энтерпрайз- приложениях, не приемлемо для транзакции БД охватывать все пользовательское взаимодействие. Рассмотрим следующий пример:

Процедура 2.1. Пример “долгоиграющего” диалога

  1. Открывается первый экран диалога. Данные, показываемые пользователю, подгружаются в отдельной сессии Session и БД-транзакции. Пользователь может модифицировать любые поля диалога.
  2. После пяти минут редактирования, пользователь использует UI элемент для сохранения. Изменения отразились в БД. Пользователь также ожидает эксклюзивного доступа к данным на время сессии редактирования

Даже, хотя мы и имеем несколько случаев доступа к БД, с точки зрения пользователя, данная серия шагов представляет одну единицы совершенной работы (Unit of Work). Есть множество путей реализации этого в приложении.

Первый (наивный) метод заключается в удержании открытыми сессий Session и транзакции на время редактирования пользователя, с использованием механизмов синхронизации БД для обеспечения эксклюзивного доступа пользователя к редактируемым данным, и предотвращению обращения к ним со стороны других пользователей, гарантируя изоляцию и атомарность. Это анти-паттерн, так как лишняя синхронизация является узким местом при проблемах производительности, встающих в высоконагруженных приложениях.

Ряд транзакций БД используется для реализации диалога с БД. В данном случае, обеспечение изоляции бизнес-процессов ложится на плечи приложения. Один диалог обычно покрывает несколько транзакций. Множественные доступы к БД могут быть атомарными, если только одна транзакция (обычно последняя) осуществляет запись в БД. Все другие только читают данные. Типичный путь реализации – через создание wizard-style диалога, покрывающего несколько шагов цикла запрос/ответ. Hibernate включает в себя некоторые возможности, позволяющие реализовать подобный функционал.

  • Автоматическое версионирование
    Hibernate может осуществлять за вас concurrency-контроль. Он может автоматически обнаружить, осуществлялись ли сторонние
    обновления данных за время ожидания пользователя.
  • Отсоединенные (Detached) объекты
    Если вы предпочтете использовать шаблон сессия-на-запрос,
    все загруженные экземпляры будут отсоединены за время ожидания пользователя. Hibernate позволяет вам обратно подсоединить
    объекты и сохранить модификации. Данный паттерн называется сессия-на-запрос-с-отсоединенными объектами. Автоматическое версионирование используется для изоляции параллельно выполняющихся запросов.
  • Расширенная сессия
    Сессия Session может быть отсоединена от нижележащего JDBC соединения после того, как БД транзакция будет закоммичена, и переподсоединена, когда возникнет новый клиентский запрос. Этот паттерн называется сессия-на-диалог, делающий повторное соединение (reattachment) объектов ненужным. Автоматическое версионирование используется для изоляции параллельных модификаций, при этом сессия не может быть сброшена (flushed) автоматически, только явно.


Сессия-на-запрос-с-отсоединенным-объектами и сессия-на-диалог имеют свои плюсы и минусы.

2.5. Идентичность объекта


Приложение может осуществлять параллельный доступ к одному и тому же persistent-состоянию (строка в базе данных) в двух разных сессиях. Однако, экземпляр persistent-класса никогда не разделяется между двумя разными сессиями. Имеют место быть и вступают в игру два разных понятия идентичности: БД-идентичность и JVM-идентичность.

Пример 2.1. БД-идентичность

foo.getId().equals( bar.getId() )

Пример 2.2. JVM-идентичность

foo==bar

Для объектов, присоединенных к одной и той же сессии Session, два понятия идентичности эквивалентны, и JVM-идентичность гарантируется БД-идентичность Hibernate’ом. Приложение может параллельно осуществлять доступ к бизнес-объекту с одной и той же БД-идентичностью в двух разных сессиях, тем временем он будет представлен двумя разными экземплярами Java-объектов, в терминах JVM-идентичности. Разрешение конфликтов осуществляется оптимистичной стратегией и автоматическим версионированием во время сброса(flush)/коммита.

Этот подход возлагает ответственность за управлением параллельностью на Hibernate и БД. Он также обеспечивает лучшую масштабируемость, так как дорогие блокировки не нужны для гарантии идентичности в однопоточной единице работы (single-threaded unit of work). Приложению нет нужды синхронизироваться на каком бы то ни было бизнес-объекте, пока он работает в одном потоке. Хоть это и не рекомендуется, в пределах сессии Session приложение может безопасно использовать оператор == для сравнения объектов.

Однако, приложение, использующее оператор == за пределами сессии Session может внести некоторые проблемы. Если вы добавите два отсоединенных экземпляра объекта в один Set, они возможно будут иметь одну БД-идентичность, т.е они представляют одну и ту же строку в таблице. Совсем не гарантировано, что они будут иметь одну и ту же JVM-идентичность, будучи в состоянии detached. Переопределите методы equals() и hashCode() в persistent-классах, так что они будут иметь собственное определение объектной эквивалентности. Не используйте БД-идентичность для реализации проверки на равенство. Вместо этого, используйте бизнес-ключ, являющийся комбинацией уникальных, неизменяемых атрибутов. БД идентификатор может измениться, если объект перейдет из состояния transient в состояние persistent. Если transient экземпляр находится вместе с detached экземпляром в одном Set’e – изменение хэшкода нарушит контракт Set’а. Атрибуты для бизнес-ключа могут быть менее устойчивыми чем основные ключи. Вам только необходимо гарантировать стабильность до тех пор, пока объекты находятся в одном Set’е. Это не проблема Hibernate, так как относится к реализации объектной идентичности и эквивалентности в Java.

2.6. Общие вопросы


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

  • Сессия Session не является потокобезопасной. Сущности, работающие параллельно, такие как HTTP-запросы, session-бины, или Swing worker’ы, приведут к возникновению ситуаций гонки (race conditions) если сессия Session делится между потоками. Если вы храните вашу сессию Hibernate в вашей сессии javax.servlet.http.HttpSession (будет обсуждено позднее), вам нужно рассмотреть проблему синхронизированного доступа к вашей HttpSession; иначе, пользователь, кликающий кнопку ‘Обновить’ слишком быстро, будет использовать одну и ту же сессию в двух параллельно выполняемых потоках.
  • Исключение, выбрасываемое Hibernate’ом означает, что вы должны откатить(rollback) вашу транзакцию и закрыть сессию Session немедленно (обсуждается более подробно в следующих главах). Если ваша сессия ограничена приложением, вы должны остановить приложение. Откат транзакции не откатывает ваши бизнес-объекты до состояния, в котором они находились на момент начала транзакции. Это означает, что состояние в БД и состояние объектов подверглось рассинхронизации. Обычно, это не проблема т.к исключения не восстановимы, и вам все равно нужно будет начинать сначала после отката.
  • Сессия кэширует каждый объект, находящийся в состоянии persistent (т.е он мониторится и проверяется на изменения Hibernate’ом). Если вы оставите ее на долговременный период, или просто загрузите слишком много данных, она вырастет многократно, до тех пор, пока вы не получите OutOfMemoryException. Есть решение вызывать clear() и evict() для управления кэшем сессии Session, но вам следует рассмотреть альтернативные способы работы с большим количеством данных, такие как хранимые процедуры. Java не является подходящим инструментом для подобного рода операций. Некоторые решения показаны в Главе 4, Пакетная обработка. Сессия, остающаяся открытой на период работы сессии пользователя также означает высокую вероятность появления “несвежих” данных.

© Habrahabr.ru