Глава 10. Транзакционность и Конкурентность (Transactions And Concurrency)

Hibernate не является базой данных. Это легковесный иструмент объектно-реляционного отображения (маппинга). Управление транзакциями делегируется на уровень базы данных. Если соединение с базой данных вовлечена с JTA, операции выполняемые Session автоматически становятся частью JTA транзакции. В этом смысле Hibernate можно рассматривать как тонкий адаптер к JDBC, добавляющий объектно ориентированные семантики.

10.1. Конфигурация, cессии (Sessions) и фабрики (Factories)

SessionFactory это дорогой в создании, объект предусматривающий паралельные вызовы (threadsafe). Он предназначен для работы в режиме одновременного доступа из любого потока приложения. Session в свою очередь объект дешевый и не не расчитанный на работу в многопоточном режиме (non-threadsafe). Он используется один раз для выполения единичной бизнес-операции и затем отбрасывается. Например, при использовании Hibernate в основанном на сервлетах приложении, сервлет может получить SessionFactory используя следующий вызов:

SessionFactory sf = (SessionFactory)getServletContext().getAttribute("my.session.factory");

Каждый вызов метода service может создать новый экземпляр сессии Session, сбросить его (вызовом метода flush()), зафиксировать тразнакцию (вызовом метода commit()), закрыть сессию (метод close()) и в завершении отбросить его. (SessionFactory в сво. очередь может хранится в JNDI или в статической переменной какого-нибудь Singleton'а.)

Для stateless session bean может использоваться похожий же подход. SessionFactory можно получать в методе setSessionContext(). Каждый бизнес-метод может создавать Session, сбрасывать ее (flush()) и закрывать (close()). (Оставляя транзакции на совести JTA, которая управляет ими автоматически в модели container-managed transactions.)

Мы используем транзакции (Hibernate Transaction) как это описано выше, единственный метод commit() Hibernate Transaction сбрасывает состояние и фиксирует транзакцию базы данных (со специальной обработкой в случае использования JTA транзакций).

Убедитесь в том, что вы поняли семантику метода flush(). Сбрасывание синхронизирует персистентное хранилище с изменениями которые произошли в памяти но не наобарот. Учтите что для всех Hibernate JDBC транзакций/соединений, транзакционный уровень изоляции применяется для всех операций исполняемых Hibernate!

В следующих разделах будут рассмотрены альтернативные подходы которые используют версионность для обеспечения атомарности тразакций. Это продуманные "передовые" подходы при приминении которых нужно проявлять внимательность и осторожность.

10.2. Потоки и соединения (Threads and connections)

Вы должны обратить внимание на следующие практики создания сессий Hibernate:

  • Никогда не создавайте в одном потоке более чем один экземпляр Session или Transaction для каждого соединения с базой данных в одном потоке.

  • Будьте предельно внимательны при создании более чем одной Session для каждой базы данных каждой транзакции. Сама по себе сессия Session содержит информацию об изменениях осуществленных для загруженных объектов, таким образом другая сессия Session может содержать устаревшие данные.

  • Объект Session не предназначен к одновременному использованию из нескольких потоков (not threadsafe)! Никогда не разделяйте доступ к одному и тому же экземпляру сессии Session между разными потоками. Сессию обычно используют в контексте одной единицы работы (unit-of-work)!

10.3. Рассмотрение идентичности объектов

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

Идентичность в базе данных

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

Идентичность в JVM

foo==bar

Для объектов одной сессии (Session) эти два представления эквивалентны. Однако, при одновременном доступе из разных потоков к одному и тому же бизнес объекту в контексте двух различных сессий, они различаются с точки зрения идентичности JVM.

Такой подход предоставляет Hibernae и базе данных беспокоится по поводу одновременного доступа. Приложению нет нужды синхронизировать каждый бизнес-объект, так как сессия использует его в одном потоке. Приложение так же не беспокоится о идентичности и может использовать == для сравнения объектов.

10.4. Оптимистическая стратегия контроля параллельного доступа

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

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

Только подход оптимистичного контроля конкурентности обеспечивает высокую производительность и расширяемость. Hibernate поддерживает три возможных пути для написания кода приложений с использованием стратегии оптимистической конкурентности.

10.4.1. Длинная сессия с автоматической версионностью

Для всех транзакций приложения используется единственный экзепляр Session и ее персистентые объекты.

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

// Экземпляр foo был раньше загружен в сессию
session.reconnect();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.disconnect();

Объект foo все еще опознаваем сессией в которая его загрузила. Пока в сессии Session есть JDBC содеинение, мы коммитим изменения в объект.

Такой прием проблематичен если наша сессия слишком велика для ее хранения в в период, когда пользователь думает (то есть между действиями пользователя), так как HttpSession должна быть настолько мала, насколько это возможно. Так как сессия (Session) так же (обязательно) является нешлм первого уровня и хранит все загруженные объекты, мы можем импользовать данную статегию отлько для ограниченого количества циклов запрос/ответ. Это действительно рекомендуется так как сессия так же со временем будет содержать устаревшие данные (объекты обновленные другими сессиями).

10.4.2. Множество сессий с автоматической версионностью

Каждое взаимодействие с персистентным хранилищем происходит в новой сессии. Несмотря на это, одни и те же персистентные экзепляры используются при каждом взаимодействии с базой данных. Приложение манипулирует отсоединенными (detached) экзеплярами которые были загружены другой сессией, затем повторно ассоциирует их вызывая Session.update() или Session.saveOrUpdate().

// foo- экзепляр, загруженный предыдущей сессией.
foo.setProperty("bar");
session = factory.openSession();
session.saveOrUpdate(foo);
session.flush();
session.connection().commit();
session.close();

Можно так же вызывать lock() вместо update() и использовать LockMode.READ (осуществяет проверку версии в обход всех кешей) если вы уверены в том, что объект не был модифицирован.

10.4.3. Проверка версий на уровне приложения

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

// foo - экземпляр загруженный в предыдущей сессии (Session)
session = factory.openSession();
int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() );
if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.close();

Конечно, если вы оперируете в среде с низким уровнем конкурентного доступа к данным, и вам не требуется контроля версий, вы можете использовать данный подход и просто пропускать проверку версии.

10.5. Отсоединение сессии

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

  1. фиксировать (commit) транзакцию (Transaction) или JDBC соединение и затем

  2. отсоединять сессию Session от JDBC.

перед тем как передавать управление пользователю. Метод Session.disconnect() отсоединяет сессию от JDBC соединения с базой данных и возвращает JDBC содединение в пул (unless you provided the connection).

Session.reconnect() получает новое соединение и перезапускает сессию. После восстановления соединения, для проведения проверки версии на данные, которые не обновлялись, вы можете вызвать Session.lock() для объектов которые могли быть обновлены в других транзакциях. При этом вам не нужно блокировать (lock) данные, которые вы обновляете.

Пример:

SessionFactory sessions;
List fooList;
Bar bar;
....
Session s = sessions.openSession();

Transaction tx = null;
try {
tx = s.beginTransaction();

fooList = s.find(
	"select foo from eg.Foo foo where foo.Date = current date"
// uses db2 date function
);
bar = (Bar) s.create(Bar.class);

tx.commit();
}
catch (Exception e) {
if (tx!=null) tx.rollback();
s.close();
throw e;
}
s.disconnect();

Позднее:

s.reconnect();

try {
tx = s.beginTransaction();

bar.setFooTable( new HashMap() );
Iterator iter = fooList.iterator();
while ( iter.hasNext() ) {
Foo foo = (Foo) iter.next();
s.lock(foo, LockMode.READ);// проверяем, что foo не устарел.
bar.getFooTable().put( foo.getName(), foo );
}

tx.commit();
}
catch (Exception e) {
if (tx!=null) tx.rollback();
throw e;
}
finally {
s.close();
}

Отсюда видно, что связь между транзакциями (Transaction) и сессией Session является многие-к-одному. Сессия представляет собой взаимодействие между приложением и базой данных. Транзакция разделяет это взаимодействие на атомарные на уровне базы данных единицы работы (unit-of-work).

10.6. Пессимистическая блокировка

Здесь не подразумевается что пользователи тартят много времени на беспокойства по поводу выбора стратегий блокировки. Обычно достаточно укзать уровень изоляции для JDBC соединения и позволить базе данный выполнить всю работу. Тем не менее, продвинутые пользователи в некоторых случаях хотят применять пессимистическую стратегию блокировки, или изменять блокировки в начале новой транзакции.

Hibernate всегда используег мехнаизмы блокировки базы данных, он никогда не блокирует объекты в памяти!

Класс LockMode опредеяет различные уровни блокировки, которые могут быть использованы Hibernate. Блокировки применяются следующими механизмами:

  • LockMode.WRITE автоматически применяется когда Hiberante обновлет или вставляет запись.

  • 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 использует подходящий альтернативный режим (вместо инициации исключения). Это обеспечивает переносимость приложения между базами данных.