Глава 13. Транзакции и параллельное выполнение
Оглавление-
13.1. Сессия и области видимости транзакций
- 13.1.1. Единица работы (Unit of work)
- 13.1.2. Длинные разговоры (Long conversations)
- 13.1.3. Учёт идентичности объекта
- 13.1.4. Общие проблемы
-
13.2. Разграничение транзакций базы данных
- 13.2.1. Неуправляемая среда
- 13.2.2. Использование JTA
- 13.2.3. Обработка исключений
- 13.2.4. Таймаут транзакции
- 13.3. Оптимистический контроль параллельного выполнения
- 13.4. Пессимистическая блокировка
- 13.5. Способы освобождения соединения
Самым важным моментом в управлении Hibernate и параллельном выполнении является то, что его легко понять. Hibernate напрямую использует JDBC-соединения и ресурсы JTA без добавления каких-либо дополнительных действий по блокировке. Рекомендуется провести некоторое время со спецификациями JDBC, ANSI и изоляции транзакций вашей системы управления базами данных.
Hibernate не блокирует объекты в памяти. Ваше приложение может ожидать поведение, определяемое
уровнем изоляции транзакций базы данных. Через Session
, который также является кэшем транзакций,
Hibernate обеспечивает повторяемые чтения для поиска по идентификатору и запросам сущности
и не сообщает запросы, которые возвращают скалярные значения.
В дополнение к управлению версиями для автоматического оптимистического контроля параллельного
выполнения, Hibernate также предлагает, используя синтаксис SELECT FOR UPDATE
,
(младший) API для пессимистической блокировки записей. Оптимистический контроль параллельного выполнения
и этот API обсуждаются далее в этой главе.
Обсуждение контроля параллельного выполнения в Hibernate начинается с детального рассмотрения
Configuration
, SessionFactory
и Session
, а также транзакций
базы данных и длинных разговоров (long conversations).
13.1. Сессия и области видимости транзакций
SessionFactory
— это дорогостоящий в плане создания, потокобезопасный объект,
предназначенный для совместного использования всеми потоками приложений. Он создается один раз,
обычно при запуске приложения, из экземпляра Configuration
.
Session
— это недорогой в плане создания, потоконебезопасный объект, который следует
использовать один раз, а затем отбрасывать для: одного запроса, разговора или отдельной единицы работы.
Session
не получит JDBC Connection
или Datasource
, если только
это не понадобится. Он не будет потреблять какие-либо ресурсы до тех пор, пока они
не будут использованы.
Чтобы уменьшить конфликт блокировок в базе данных, транзакция базы данных должна быть как можно короче. Длинные транзакции базы данных будут препятствовать масштабированию вашего приложения до высокой нагрузки параллельного выполнения. Не рекомендуется открывать транзакцию базы данных во время пока пользователь думает (user think time), пока единица работы не будет завершена.
Какова область видимости единицы работы? Может ли один Hibernate Session
проходить
несколько транзакций базы данных или это отношение областей видимости «один-к-одному»?
Когда вы должны открывать и закрывать Session
и как разграничивать
транзакции базы данных? Эти вопросы рассматриваются в следующих разделах.
13.1.1. Единица работы (Unit of work)
Сначала давайте определим, что такое «единица работы». Единица работы — это шаблон проектирования, описанный Мартином Фаулером как "[поддерживает] список объектов, присоединённых бизнес-транзакцией, и координирует запись изменений и разрешение проблем параллельного выполнения. «[PoEAA] Другими словами, это серия операций, которые мы хотим отправить в базу данных одним пакетом. В принципе, это одна транзакция, хотя выполнение единицы работы часто будет охватывать несколько транзакций физической базы данных (см. раздел 13.1.2. «Длинные разговоры»). Поэтому мы говорим о более абстрактном понятии транзакции. Термин «бизнес-транзакция» также иногда используется вместо «единица работы».
Не используйте анти-шаблон сессия-на-операцию (session-per-operation): не открывайте
и не закрывайте Session
для каждого простого вызова базы данных в одном потоке.
То же самое верно для транзакций базы данных. Вызов базы данных в приложении производится
с использованием запланированной последовательности; они сгруппированы в атомные единицы работы.
Это также означает, что автоматическая фиксация (auto-commit) после каждой отдельной инструкции SQL
бесполезна в приложении, так как этот режим предназначен для работы с SQL-консолью ad-hoc.
Hibernate отключает или ожидает, что сервер приложений отключит, режим автоматической фиксации.
Операции с базой данных не являются необязательными. Вся связь с базой данных должна
происходить внутри транзакции. Следует избегать поведения в режиме автоматической фиксации данных для
чтения, поскольку многие небольшие транзакции вряд ли будут работать лучше, чем одна четко определенная
единица работы. Последняя также более удобна и расширяема.
Наиболее распространённым шаблоном в многопользовательском клиент-серверном приложении является
сессия-на-запрос (session-per-request) В этой модели запрос от клиента отправляется
на сервер, где выполняется постоянный слой (persistence layer) Hibernate. Открывается новый
Session
Hibernate, и все операции с базой данных выполняются в этой части работы.
По завершении работы, и после того, как был подготовлен ответ для клиента, сессия сбрасывается
(flush) и закрывается. Используйте единую транзакцию базы данных для обслуживания запроса клиентов,
начиная и фиксируя (commit) её при открытии и закрытии Session
. Отношения между
ними это «один-к-одному», и эта модель идеально подходит для многих приложений.
Проблема лежит в реализации. Hibernate обеспечивает встроенное управление «текущей сессией»,
чтобы упростить этот шаблон. Начните транзакцию, когда запрос сервера должен быть обработан, и завершите
транзакцию до того, как ответ будет отправлен клиенту. Обычными решениями являются
ServletFilter
, AOP-перехватчик с pointcut в методах обслуживания
или контейнер прокси/перехвата. Контейнер EJB является стандартизированным способом реализации сквозных
аспектов, таких как разграничение транзакций на сессионных бинах EJB, декларативно с CMT
(container-managed transactions (транзакции, управляемые контейнерами)).
Если вы используете программное разграничение транзакций, для удобства использования и переносимости
кода используйте API транзакций Hibernate, показанный далее в этой главе.
Ваш код приложения может получить доступ к «текущей сессии» для обработки запроса,
вызвав sessionFactory.getCurrentSession()
. Вы всегда будете получать Session
,
в области видимости текущей транзакции базы данных. Это должно быть настроено как для локальных ресурсов,
так и для среды JTA. См. раздел
2.2 «Контекстные сессии».
Вы можете расширить область видимости Session
и транзакции базы данных до тех пор,
пока «вид (view) не будет отрисован (rendered)». Это особенно полезно в приложениях
сервлетов, которые используют отдельную фазу рендеринга после обработки запроса. Расширение транзакции
базы данных до рендеринга вида достигается путем реализации вашего собственного перехватчика.
Однако это будет сложно, если вы будете полагаться на EJB с транзакциями, управляемыми
контейнерами. Транзакция будет завершена, когда метод EJB вернет управление, прежде чем сможет начаться
рендеринг любого вида. См. веб-сайт и форум Hibernate для получения советов и примеров,
относящихся к шаблону Open Session in View
.
13.1.2. Длинные разговоры (Long conversations)
Шаблон сессия-на-запрос не является единственным способом проектирования единиц работы. Многие бизнес-процессы требуют целого ряда взаимодействий с пользователем, чередующихся с доступом к базе данных. В веб и корпоративных приложениях недопустимо, чтобы транзакция базы данных охватывала взаимодействие с пользователем. Рассмотрим следующий пример:
-
Открывается первый экран диалога. Данные, просматриваемые пользователем, были загружены
в конкретный
Session
и транзакцию базы данных. Пользователь волен изменять объекты. - Пользователь нажимает «Сохранить» через 5 минут и ожидает, что его изменения станут постоянными. Пользователь также ожидает, что он был единственным человеком, который редактировал эту информацию и что никаких противоречивых изменений не произошло.
С точки зрения пользователя мы называем эту единицу работы долго выполняющимся «разговором» или транзакцией приложения. Существует много способов реализовать это в своем приложении.
Первая наивная реализация может привести к тому, что Session
и транзакция базы данных
будут открыты во время пока пользователь думает (user think time), причем блокировки хранятся
в базе данных, чтобы предотвратить одновременную модификацию и гарантировать изоляцию
и атомарность. Это анти-шаблон, поскольку конфликт блокировок не позволяет масштабировать
приложение с увеличением числа одновременных пользователей.
Вам необходимо использовать несколько транзакций базы данных, чтобы реализовать разговор. В этом случае поддержание изоляции бизнес-процессов становится частичной ответственностью слоя (tier) приложения. Один разговор обычно охватывает несколько транзакций базы данных. Он будет атомарным, если только одна из этих транзакций базы данных (последняя) хранит обновлённые данные. Все остальные просто читают данные (например, в диалоге в стиле «Мастера» (wizard-style), охватывающего несколько циклов запрос/ответ). Это проще реализовать, чем может показаться, особенно если вы используете некоторый функционал Hibernate:
- Автоматическое ведение версий (Automatic Versioning): Hibernate может автоматически выполнять для вас оптимистический контроль параллельного выполнения. Он может автоматически определять, произошла ли параллельная модификация во время пока пользователь думает (user think time). Проверьте это в конце разговора.
- Отсоединённые объекты (Detached Objects): если вы решите использовать шаблон сессия-на-запрос, все загруженные экземпляры будут находиться в отсоединённом состоянии во время пока пользователь думает (user think time). Hibernate позволяет повторно присоединять (reattach) объекты и сохранять (persist) изменения. Шаблон называется сессия-на-запрос-с-отсоединёнными-объектами (session-per-request-with-detached-objects). Автоматическое управление версиями используется для изоляции параллельных изменений.
-
Расширенная (или длинная) сессия:
Session
Hibernate может быть отключена от низлежащего JDBC-соединения после того, как транзакция базы данных была зафиксирована (commit) и повторно подключена при появлении нового клиентского запроса. Этот шаблон известен как сессия-на-разговор (session-per-conversation) и делает ненужным повторное присоединение. Автоматическое управление версиями используется для изоляции параллельных модификаций, иSession
не будет сбрасываться (flush) автоматически, но явно.
Преимущество и недостатки имеют как сессия-на-запрос-с-отсоединёнными-объектами, так и сессия-на-разговор. Эти недостатки обсуждаются далее в этой главе в контексте оптимистического контроля параллельного выполнения.
13.1.3. Учёт идентичности объекта
Приложение может одновременно обращаться к одному и тому же постоянному состоянию
в двух разных Session
. Однако экземпляр постоянного класса никогда не делится между
двумя экземплярами Session
. Именно по этой причине существуют два разных понятия идентичности:
- Идентичность БД
-
foo.getId().equals( bar.getId() )
- Идентичность JVM
-
foo==bar
Для объектов, присоединённых к определенному Session
(т.е. в области видимости
Session
), эти два понятия эквивалентны, а идентификатор JVM для идентификации базы данных
гарантируется Hibernate. Хотя приложение может одновременно обращаться к «одному
и тому же» (постоянная идентичность) бизнес-объекту в двух разных сессиях,
два экземпляра будут фактически «разными» (идентичность JVM). Конфликты разрешаются
с использованием оптимистического подхода и автоматического управления версиями при времени
сброса/фиксации (flush/commit).
Такой подход позволяет Hibernate и базе данных не беспокоиться о параллельном выполнении.
Он также обеспечивает наилучшую масштабируемость, поскольку гарантирование идентичности
в однопоточных единицах работы означает, что для этого не требуется дорогостоящая блокировка
или другие средства синхронизации. Приложению не требуется синхронизировать любой бизнес-объект,
если он поддерживает один поток на Session
. В рамках Session
приложение может безопасно использовать ==
для сравнения объектов.
Однако приложение, которое использует ==
за пределами Session
,
может привести к неожиданным результатам. Это может произойти даже в некоторых неожиданных местах.
Например, если вы помещаете два отдельных экземпляра в один и тот же Set
,
оба могут иметь одинаковый идентификатор базы данных (т.е. они представляют одну запись).
Идентификация JVM, однако, по определению не гарантируется для экземпляров в отсоединённом
состоянии. Разработчик должен переопределить методы equals()
и hashCode()
в постоянных классах и реализовать собственное понятие равенства объектов. Существует одна оговорка:
никогда не используйте идентификатор базы данных для реализации равенства.
Используйте бизнес-ключ, который представляет собой комбинацию уникальных, обычно неизменяемых атрибутов.
Идентификатор базы данных будет изменяться, если переходный объект станет постоянным. Если переходный
экземпляр (обычно вместе с отсоединёнными экземплярами) удерживается в Set
,
изменение хэш-кода прерывает контракт Set
. Атрибуты для бизнес-ключей не обязательно
должны быть такими же стабильными, как первичные ключи базы данных; вам нужно только гарантировать
стабильность, пока объекты находятся в одном наборе. См. веб-сайт Hibernate для более подробного
обсуждения этой проблемы. Обратите внимание, что это не проблема Hibernate, а просто то,
как должны быть реализованы идентификация и равенство Java объектов.
13.1.4. Общие проблемы
Не используйте анти-шаблоны сессия-на-сессию-пользователя (session-per-user-session) или сессия-на-приложение (session-per-application) (однако есть редкие исключения из этого правила). Некоторые из следующих проблем также могут возникать в рекомендуемых шаблонах, поэтому убедитесь, что вы понимаете последствия перед принятием дизайнерского решения:
-
Session
не является потокобезопасной. Вещи, которые работают параллельно, например, HTTP-запросы, сессионные бины (session beans) или Swing воркеры (workers), будут вызывать условия гонки, если разделяют экземплярSession
. Если вы храните HibernateSession
вHttpSession
(это обсуждается далее в главе), вам следует рассмотреть возможность синхронизации доступа к вашей сессии Http. В противном случае пользователь, который быстро вызывает перезагрузку, может использовать один и тот жеSession
в двух одновременно работающих потоках. -
Исключение, выбрасываемое Hibernate, означает, что вам необходимо отменить транзакцию с базой данных
и немедленно закрыть
Session
(это более подробно обсуждается далее в этой главе). Если вашSession
связан с приложением, вам необходимо остановить приложение. Откат транзакции базы данных не возвратит ваши бизнес-объекты в состояние, в котором они находились в начале транзакции. Это означает, что состояния базы данных и бизнес-объектов будут рассинхронизированы. Обычно это не проблема, потому что исключения не восстанавливаются, и вам все равно придется начинать работу после отката. -
Session
кэширует каждый объект, находящийся в постоянном состоянии (наблюдаемый и проверенный Hibernate для грязного состояния). Если вы держите его открытым в течение длительного времени или просто загружаете слишком много данных, он будет расти бесконечно, пока вы не получите исключение OutOfMemoryException. Одним из решений является вызовclear()
иevict()
для управления кэшемSession
, но вы должны рассмотреть Хранимую процедуру, если вам нужны массовые операции с данными. Некоторые решения показаны в главе 15 «Пакетная обработка». СохранениеSession
, открытого в течение сессии пользователя, также означает более высокую вероятность устаревания данных.
13.2. Разграничение транзакций базы данных
База данных или система - границы транзакций всегда необходимы. Никакая связь с базой данных не может произойти за пределами транзакции базы данных (это, похоже, путает многих разработчиков, которые привыкли к режиму автоматической фиксации). Всегда используйте четкие границы транзакций, даже для только для операций чтения. В зависимости от уровня изоляции и возможностей базы данных это может не потребоваться, но нет нечего страшного, если вы всегда четко разделяете транзакции. Конечно, одна транзакция с базой данных будет работать лучше, чем много мелких транзакции, даже для чтения данных.
Приложение Hibernate может работать в неуправляемых (то есть автономных, простых приложениях Web или Swing) и управляемых средах J2EE. В неуправляемой среде Hibernate обычно несёт ответственность за свой собственный пул соединений с базой данных. Разработчик приложения должен вручную установить границы транзакций (начать, фиксировать или откатить транзакции базы данных). Управляемая среда обычно предоставляет транзакции, управляемые контейнерами (container-managed transactions (CMT)), причем сборка транзакций определена декларативно (например, в дескрипторах развертывания сессионных бинах EJB). Демонстрация программной транзакции больше не нужна.
Тем не менее, часто желательно поддерживать переносимость уровня персистентности между
неуправляемыми локальными средами и системами, которые могут полагаться на JTA,
но использовать BMT вместо CMT. В обоих случаях используется программное разграничение транзакций.
Hibernate предлагает API-интерфейс оболочки, называемый Transaction
, который преобразуется
в собственную систему транзакций среды развертывания. Этот API на самом деле является необязательным,
но мы настоятельно рекомендуем его использовать, если вы не используете сессионный
бин CMT.
Завершение Session
обычно включает в себя четыре различные фазы:
- сброс (flush) сессии
- фиксация (commit) транзакции
- закрытие сессии
- обработку исключений
Мы обсудили сброс сессии раньше, поэтому теперь мы будем более подробно рассматривать разграничение транзакций и обработку исключений в управляемых и неуправляемых средах.
13.2.1. Неуправляемая среда
Если слой персистентности Hibernate работает в неуправляемой среде, соединения с базой данных обычно обрабатываются простыми (то есть не-DataSource) пулами соединений, из которых Hibernate получает соединения по мере необходимости. Идиома обработки сессии/транзакции выглядит так:
// Идиома неуправляемой среды Session sess = factory.openSession(); Transaction tx = null; try { tx = sess.beginTransaction(); // делаем какую-то работу ... tx.commit(); } catch (RuntimeException e) { if (tx != null) tx.rollback(); throw e; // или пробрасываем исключение } finally { sess.close(); }
Вам не нужно вызывать flush()
сессии явно: вызов commit()
автоматически
запускает синхронизацию в зависимости от раздела
«11.10. Сброс (flush) сессии» для сессии.
Вызов close()
обозначает завершение сессии. Основное значение close()
заключается
в том, что соединение JDBC будет отстранено сессией. Этот Java-код переносится и работает как
в неуправляемых, так и в JTA-средах.
Как было сказано ранее, гораздо более гибкое решение — это встроенное в Hibernate контекстное управление «текущая сессия»:
// Идиома неуправляемой среды с getCurrentSession() try { factory.getCurrentSession().beginTransaction(); // делаем какую-то работу ... factory.getCurrentSession().getTransaction().commit(); } catch (RuntimeException e) { factory.getCurrentSession().getTransaction().rollback(); throw e; // или пробрасываем исключение }
Вы не увидите эти фрагменты кода в обычном приложении; фатальные (системные) исключения
должны всегда быть перехвачены на «верху». Другими словами, код, который выполняет вызовы
Hibernate в слое персистентности, и код, который обрабатывает RuntimeException
(и обычно может только очищать и выходить), находятся в разных слоях. Текущее управление
контекстом Hibernate может значительно упростить этот дизайн, обратившись к SessionFactory
.
Обработка исключений обсуждается далее в этой главе.
Вы должны выбрать org.hibernate.transaction.JDBCTransactionFactory
, который
по умолчанию, а для второго примера выберите «thread
» как ваш
hibernate.current_session_context_class
.
13.2.2. Использование JTA
Если ваш слой персистентности работает на сервере приложений (например, за сессионными бинах EJB), каждое соединение с источником данных, полученное Hibernate, будет автоматически включено в глобальную транзакцию JTA. Вы также можете установить автономную реализацию JTA и использовать её без EJB. Hibernate предлагает две стратегии интеграции JTA.
Если вы используете транзакции, управляемые бинами (bean-managed transactions (BMT)), Hibernate сообщит серверу приложений для начала и завершения транзакции BMT, если вы используете API транзакций. Код управления транзакцией идентичен неуправляемой среде.
// Идиома BMT Session sess = factory.openSession(); Transaction tx = null; try { tx = sess.beginTransaction(); // делаем какую-то работу ... tx.commit(); } catch (RuntimeException e) { if (tx != null) tx.rollback(); throw e; // или пробрасываем исключение } finally { sess.close(); }
Если вы хотите использовать Session
, связанный с транзакциями, то есть
метод getCurrentSession()
для лёгкого распространения контекста, напрямую используйте
API-интерфейс JTA UserTransaction
:
// Идиома BMT с getCurrentSession() try { UserTransaction tx = (UserTransaction) new InitialContext() .lookup("java:comp/UserTransaction"); tx.begin(); // делаем какую-то работу в Session связанным с транзакцией factory.getCurrentSession().load(...); factory.getCurrentSession().persist(...); tx.commit(); } catch (RuntimeException e) { tx.rollback(); throw e; // или пробрасываем исключение }
С CMT разграничение транзакций завершается дескрипторами развертывания бина сессии, а не программно. Код сводится к:
// Идиома CMT Session sess = factory.getCurrentSession(); // делаем какую-то работу ...
В CMT/EJB даже откат происходит автоматически. Необработанное исключение RuntimeException
,
выброшенное методом сеансового бина, сообщает контейнеру о необходимости отменить глобальную транзакцию.
Вам вообще не нужно использовать API Transaction
Hibernate с BMT или CMT,
и вы получаете автоматическое всплытие «текущей» Session, связанной с транзакцией.
При настройке фабрики транзакций Hibernate выберите org.hibernate.transaction.JTATransactionFactory
,
если вы используете JTA напрямую (BMT) и org.hibernate.transaction.CMTTransactionFactory
в бине сессии CMT. Не забудьте также установить
hibernate.transaction.manager_lookup_class
. Убедитесь, что ваш
hibernate.current_session_context_class
либо не настроен (обратная совместимость),
либо установлен на «jta».
Операция getCurrentSession()
имеет один недостаток в среде JTA. Существует одно
предостережение от использования режима освобождения соединения after_statement
,
который затем используется по умолчанию. Из-за ограничения спецификации JTA Hibernate не может
автоматически очищать любые незакрытые экземпляры ScrollableResults
или Iterator
,
возвращаемые scroll()
или iterate()
. Вы должны освободить низлежащий
курсор базы данных, вызвав ScrollableResults.close()
или Hibernate.close(Iterator)
явно из блока finally
. В большинстве приложений можно легко избежать использования
scroll()
или iterate()
из кода JTA или CMT.
13.2.3. Обработка исключений
Если Session
выбрасывает исключение, включая любое SQLException
, немедленно
откатите транзакции базы данных, вызовите Session.close()
и отбросьте экземпляр
Session
. Некоторые методы Session
не оставляют сессию в согласованном
состоянии. Никакое исключение, выброшенное Hibernate, не может рассматриваться как подлежащее
восстановлению. Убедитесь, что Session
будет закрыт вызовом функции close()
в блоке finally
.
Исключение HibernateException
, которое обертывает большую часть ошибок, которые могут возникать
на слое персистентности Hibernate, является непроверяемым исключением. Этого не было в более
старых версиях Hibernate. По нашему мнению, мы не должны заставлять разработчика приложений
перехватывать невосстанавливаемое исключение на низком уровне. В большинстве систем непроверяемые
и фатальные исключения обрабатываются в одном из первых кадров стека вызовов метода
(то есть в более высоких слоях), и либо пользователю представляется сообщение об ошибке
приложения, либо предпринимается какое-либо другое соответствующее действие. Обратите внимание, что Hibernate
также может выбрасывать другие непроверяемые исключения, которые не являются исключениями
HibernateException
. Они не подлежат восстановлению и соответствующие меры должны быть
приняты.
Hibernate обертывает SQLExceptions
, возникающие при взаимодействии с базой данных
в JDBCException
. Фактически, Hibernate пытается преобразовать исключение в более
значимый подкласс JDBCException
. Низлежащее SQLExceptions
всегда доступно через
JDBCException.getCause()
. Hibernate преобразует SQLException
в соответствующий
подкласс JDBCException
, используя SQLExceptionConverter
, подключенный
к SessionFactory
. По умолчанию SQLExceptionConverter
определяется
настроенным диалектом. Тем не менее, также возможно подключить пользовательскую реализацию.
Подробнее см. Javadocs для класса SQLExceptionConverterFactory
. Стандартными подтипами
JDBCException
являются:
-
JDBCConnectionException
: указывает на ошибку взаимодействия с низлежащим JDBC. -
SQLGrammarException
: указывает на проблему с грамматикой или синтаксисом в переданном SQL. -
ConstraintViolationException
: указывает на некоторые формы нарушения ограничений целостности. -
LockAcquisitionException
: указывает на ошибку, приобретающую уровень блокировки, необходимый для выполнения запрошенной операции. -
GenericJDBCException
: общее исключение, которое не попадает ни в одну из других категорий.
13.2.4. Таймаут транзакции
Важной функцией, предоставляемой управляемой средой, такой как EJB, которая никогда не предоставляется
для неконтролируемого кода, является таймаут транзакции. Таймауты транзакций гарантируют, что ни одна
неверная транзакция не может бесконечно связывать ресурсы, не возвращая пользователю никакого ответа.
Вне управляемой (JTA) среды Hibernate не может полностью обеспечить эту функциональность. Однако Hibernate
может, по крайней мере, контролировать операции доступа к данным, гарантируя, что взаимные блокировки
(deadlocks) и запросы на уровне базы данных с огромными наборами результатов ограничены
определенным таймаутом. В управляемой среде Hibernate может делегировать таймаут транзакции JTA.
Эта функциональность абстрагируется объектом Transaction
Hibernate.
Session sess = factory.openSession(); try { // установить таймаут транзакции на 3 секунды sess.getTransaction().setTimeout(3); sess.getTransaction().begin(); // делаем какую-то работу ... sess.getTransaction().commit() } catch (RuntimeException e) { sess.getTransaction().rollback(); throw e; // или пробрасываем исключение } finally { sess.close(); }
setTimeout()
не может быть вызван в бине CMT, где таймауты транзакций должны быть
определены декларативно.
13.3. Оптимистический контроль параллельного выполнения
Единственный подход, который согласуется с высоким параллельным выполнением и высокой масштабируемостью — это оптимистический контроль параллельного выполнения при управлении версиями. Проверка версий использует номера версий или временные метки для обнаружения конфликтующих обновлений и предотвращает потерю обновлений. Hibernate предоставляет три возможных подхода к написанию кода приложения, использующего оптимистическое параллельное выполнение. Обсуждаемые нами варианты использования заключаются в контексте длинных разговоров, но проверка версий также помогает предотвратить потерю обновлений в транзакциях с одной базой данных.
13.3.1. Проверка версии приложения
В реализации без большой помощи Hibernate каждое взаимодействие с базой данных происходит
в новом Session
, и разработчик отвечает за перезагрузку всех постоянных
экземпляров из базы данных, прежде чем манипулировать ими. Приложение принудительно выполняет собственную
проверку версии, чтобы обеспечить изоляцию транзакций в разговорах. Этот подход является наименее
эффективным с точки зрения доступа к базе данных. Это подход, наиболее похожий на сущности EJB.
// foo - экземпляр, загруженный предыдущей сессией session = factory.openSession(); Transaction t = session.beginTransaction();
int oldVersion = foo.getVersion(); session.load( foo, foo.getKey() ); // загрузить текущее состояние if ( oldVersion != foo.getVersion() ) throw new StaleObjectStateException(); foo.setProperty("bar");
t.commit(); session.close();
Свойство version
отражается с помощью <version>, и Hibernate автоматически
увеличивает его во время сброса (flush), если сущность грязная.
Если вы работаете в среде с низким уровнем параллельных данных и не нуждаетесь в проверке версий, вы можете использовать этот подход и пропустить проверку версии. В этом случае стратегия последняя фиксация выигрывает (last commit wins) являются стратегией по умолчанию для длинных разговоров. Имейте в виду, что это может запутать пользователей приложения, так как они могут потерять обновления без сообщений об ошибках или получат возможность слить (merge) конфликтующие изменения.
Ручная проверка версий возможна только в тривиальных условиях и не применима для большинства
приложений. Часто нужно проверять не только отдельные экземпляры, но и полные графы
модифицированных объектов. Hibernate предлагает автоматическую проверку версий либо расширенным
Session
, либо отсоединёнными экземплярами в качестве парадигмы проектирования.
13.3.2. Расширенная сессия и автоматическое управление версиями
Один экземпляр Session
и его постоянные экземпляры, которые используются для всего
разговора, называются сессия-на-разговор (session-per-conversation). Hibernate проверяет версии
экземпляров во время сброса (flush), выбрасывая исключение, если обнаруживается одновременная модификация.
Разработчик должен перехватить и обработать это исключение. Общие параметры — это возможность
для пользователя слить (merge) изменения или перезапустить бизнес-разговор с актуальными данными.
Session
отключается от любого базового JDBC-соединения при ожидании взаимодействия
с пользователем. Этот подход является наиболее эффективным с точки зрения доступа к базе данных.
Приложение не проверяет версию или не присоединяет отсоединённые экземпляры, а также
не перезагружает экземпляры в каждой транзакции базы данных.
// foo — это экземпляр, загруженный ранее старой сессией Transaction t = session.beginTransaction(); // Получить новое соединение JDBC, начать транзакцию
foo.setProperty("bar");
session.flush(); // Только для последней транзакции в разговоре t.commit(); // Также возвратите соединение JDBC session.close(); // Только для последней транзакции в разговоре
Объект foo
знает, на каком Session
он был загружен. Начало новой
транзакции базы данных на старой сессии получает новое соединение и возобновляет сессию.
Фиксация (commit) транзакции базы данных отключает сессию от соединения JDBC и возвращает соединение
в пул. После повторного подключения, чтобы принудительно проверить версию данных, которые
вы не обновлили, вы можете вызвать Session.lock()
с помощью
LockMode.READ
для любых объектов, которые могли быть обновлены другой транзакцией.
Вам не нужно блокировать данные, которые вы обновляете. Обычно вы должны установить
FlushMode.MANUAL
на расширенном Session
, чтобы только последний цикл транзакций
базы данных позволял фактически сохранять все изменения, внесенные в этот разговор. Только эта последняя
транзакция базы данных будет включать операцию flush()
, а затем close()
сессию,
чтобы завершить разговор.
Этот шаблон является проблемным, если Session
слишком велик, чтобы его можно было сохранить
во время пока пользователь думает (user think time) (например, HttpSession
следует хранить
как можно меньше). Поскольку Session
также является кэшем первого уровня и содержит все
загруженные объекты, мы, вероятно, можем использовать эту стратегию только для нескольких циклов
запрос/ответ. Используйте Session
только для отдельного разговора, так как он скоро будет
иметь устаревшие данные.
Заметка
Более ранние версии Hibernate требовали явного отключения и повторного подключения
Session
. Эти методы устарели, так как начало и конец транзакции имеют тот же эффект.
Держите отключенный Session
близко к слою персистентности. Используйте сессионный бин
состояния EJB для хранения Session
в трехслойной среде. Не переносите его
на веб-слой или даже сериализуйте его на отдельный уровень, чтобы сохранить его
в HttpSession
.
Расширенный шаблон сессии или сессия-на-разговор (session-per-conversation) сложнее реализовать
с автоматическим управлением контекстом текущей сессии. Для этого вам необходимо предоставить
собственную реализацию CurrentSessionContext
. См. Hibernate Wiki для примеров.
13.3.3. Отсоединённые объекты и автоматическое управление версиями
Каждое взаимодействие с постоянным хранилищем происходит в новом Session
.
Тем не менее, одни и те же постоянные экземпляры повторно используются для каждого
взаимодействия с базой данных. Приложение управляет состоянием отдельных экземпляров, первоначально
загруженных в другой Session
, а затем повторно присоединяет их с помощью
Session.update()
, Session.saveOrUpdate()
или Session.merge()
.
// foo - экземпляр, загруженный предыдущей сессией foo.setProperty("bar"); session = factory.openSession(); Transaction t = session.beginTransaction(); session.saveOrUpdate(foo); // Используйте merge(), если «foo», возможно, уже был загружен t.commit(); session.close();
Опять же, Hibernate проверит версии экземпляра во время сброса (flush), выбрасит исключение, если встретятся конфликтующие обновления.
13.3.4. Настройка автоматического управления версиями
Вы можете отключить автоматический инкремент версии для определенных свойств и коллекций,
установив для атрибута optimistic-lock
значение false
. Hibernate больше
не будет увеличивать версии, если свойство грязное.
Унаследованные схемы баз данных часто статичны и не могут быть изменены. Или другие приложения могут
обращаться к одной и той же базе данных и не будут знать, как обращаться
с номерами версий или даже с метками времени. В обоих случаях управление версиями не может
полагаться на конкретный столбец в таблице. Чтобы принудительно проверить версию
с сопоставлением состояния всех полей в строке, но без отображения свойств версии или времени,
включите optimistic-lock=«all»
в отображении <class>. Это
работает только в том случае, если Hibernate может сравнивать старое и новое состояние
(то есть, если вы используете один длинный Session
,
а не сессия-на-запрос-с-отсоединёнными-объектами (session-per-request-with-detached-objects)).
Одновременная модификация может быть разрешена в тех случаях, когда сделанные изменения
не перекрываются. Если вы установите optimistic-lock=«dirty»
когда отражаете <class>, Hibernate будет сравнивать только грязные поля во время сброса (flush).
В обоих случаях, с выделенными столбцами версии/метками времени или с полным/грязным
сопоставлением полей, Hibernate использует единую инструкцию UPDATE
с соответствующим
предложением WHERE
для каждой сущности для выполнения проверки версии и обновления
информации. Если вы используете переходное постоянство (transitive persistence) для каскадирования
повторной привязки к связанным сущностям, Hibernate может выполнять ненужные обновления.
Обычно это не проблема, но триггеры on update в базе данных могут выполняться даже
в том случае, если в отсоединённые экземпляры не были внесены изменения. Вы можете
настроить это поведение, установив select-before-update=«true»
в отображении
<class>, заставив Hibernate сделать SELECT
для выбора экземпляра, чтобы убедиться,
что изменения произошли до обновления записи.
13.4. Пессимистическая блокировка
Не предполагается, что пользователи тратят много времени на заботу о стратегиях блокировки. Обычно достаточно указать уровень изоляции для JDBC-соединений, а затем просто позволить базе данных выполнить всю работу. Однако продвинутые пользователи могут захотеть получить эксклюзивные пессимистические блокировки или повторно получить блокировки в начале новой транзакции.
Hibernate всегда будет использовать механизм блокировки базы данных; он никогда не блокирует объекты в памяти.
Класс LockMode
определяет различные уровни блокировки, которые могут быть получены Hibernate.
Блокировка обеспечивается следующими механизмами:
-
LockMode.WRITE
приобретается автоматически, когда Hibernate обновляет или вставляет строку. -
LockMode.UPGRADE
можно получить по явному пользовательскому запросу, используяSELECT ... FOR UPDATE
в базах данных, которые поддерживают этот синтаксис. -
LockMode.UPGRADE_NOWAIT
можно получить по явному запросу пользователя, используяSELECT ... FOR UPDATE NOWAIT
для Oracle. -
LockMode.READ
получается автоматически, когда Hibernate считывает данные в режиме изоляции Repeatable Read или Serializable. Он может быть повторно получен с помощью явного запроса пользователя. -
LockMode.NONE
представляет собой отсутствие блокировки. Все объекты переключаются в этот режим блокировки в конце транзакции. Объекты, связанные с сесией с помощью вызоваupdate()
илиsaveOrUpdate()
, также запускаются в этом режиме блокировки.
«Явный запрос пользователя» выражается одним из следующих способов:
- Вызовом
Session.load()
с указаниемLockMode
. - Вызовом
Session.lock()
. - Вызовом
Query.setLockMode()
.
Если Session.load()
вызывается с UPGRADE
или UPGRADE_NOWAIT
,
а запрошенный объект еще не загружен сесиией, объект загружается с помощью
SELECT ... FOR UPDATE
. Если load()
вызывается для объекта, который уже загружен
с менее ограничивающей блокировкой, чем запрошенный, Hibernate вызывает lock()
для этого
объекта.
Session.lock()
выполняет проверку номера версии, если указанный режим блокировки
READ
, UPGRADE
или UPGRADE_NOWAIT
. В случае UPGRADE
или UPGRADE_NOWAIT
используется SELECT ... FOR UPDATE
.
Если запрошенный режим блокировки не поддерживается базой данных, Hibernate использует соответствующий альтернативный режим вместо того, чтобы выбрасывать исключение. Это гарантирует переносимость приложения.
13.5. Способы освобождения соединения
Одним из преимуществ управления соединением JDBC в Hibernate 2.x стало то, что
Session
будет получать соединение, когда это впервые потребовалось, а затем поддерживать
это соединение до закрытия сессии. Hibernate 3.x представил понятие режимов освобождения соединения,
которые будут инструктировать сессию как обрабатывать его соединения JDBC. Следующее обсуждение относится
только к соединениям, предоставляемым через сконфигурированный ConnectionProvider
.
Пользовательские подключения находятся за пределами этой дискуссии. Различные режимы освобождения
идентифицируются перечисляемыми значениями org.hibernate.ConnectionReleaseMode
:
-
ON_CLOSE
: это унаследованное поведение, описанное выше. Сессия Hibernate получает соединение, когда сначала требуется выполнить некоторый доступ JDBC и поддерживает это соединение до закрытия сессии. -
AFTER_TRANSACTION
: освобождает соединения после того, какorg.hibernate.Transaction
завершится. -
AFTER_STATEMENT
(также называемый агрессивным освобождением): освобождает соединения после выполнения каждого оператора. Это агрессивное освобождение пропускается, если эта инструкция оставляет открытые ресурсы, связанные с данной сессией. В настоящее время единственная ситуация, когда это происходит, — это использованиеorg.hibernate.ScrollableResults
.
Параметр конфигурации hibernate.connection.release_mode
используется для указания режима
освобождения. Возможные значения:
-
auto
(по умолчанию): этот вариант делегирует выбор режима освобождения методуorg.hibernate.transaction.TransactionFactory.getDefaultReleaseMode()
. ДляJTATransactionFactory
возвращаетсяConnectionReleaseMode.AFTER_STATEMENT
; дляJDBCTransactionFactory
возвращаетсяConnectionReleaseMode.AFTER_TRANSACTION
. Не изменяйте это поведение по умолчанию, поскольку ошибки из-за значения этого параметра имеют тенденцию указывать на ошибки и/или недопустимые предположения в коде пользователя. -
on_close
: используетConnectionReleaseMode.ON_CLOSE
. Этот параметр оставлен для обратной совместимости, но его использование не рекомендуется. -
after_transaction
: используетConnectionReleaseMode.AFTER_TRANSACTION
. Этот параметр не должен использоваться в средах JTA. Также обратите внимание, что приConnectionReleaseMode.AFTER_TRANSACTION
, если сессия считается в режиме автоматической фиксации, соединения будут освобождены, как если бы режим освобождения былAFTER_STATEMENT
. -
after_statement
: используетConnectionReleaseMode.AFTER_STATEMENT
. Кроме того, сконфигурированConnectionProvider
, чтобы узнать, поддерживает ли он этот параметр (supportsAggressiveRelease()
). Если нет, режим освобождения сбрасывается наConnectionReleaseMode.AFTER_TRANSACTION
. Этот параметр является безопасным только в средах, где мы можем либо повторно получать одно и то же низлежащее соединение JDBC каждый раз, когда вы совершаете вызов вConnectionProvider.getConnection()
или в средах с автоматической фиксацией, где не имеет значения, если мы повторно устанавливаем то же соединение.