пятница, 29 февраля 2008 г.

NHibernate unexpected updates

Часто начиная работать над одной проблемой, приходишь к совершенно неожиданным местам кода, которые казалось бы никак не связаны целью нынешнего «расследования». На примере одного такого своего расследования я расскажу о том как в некоторых случаях ведет себя NHibernate.
Проблема заключалась в deadlock’ах при одновременном обращении пользователей к некоторым сущностям. Читая код, я обнаружил, что посредством NHibernate у нас идет самое простое обращение к базе, получение объекта и вывод некоторых его полей. Все это по логике вещей должно вылиться в один SELECT запрос к базе, который не предвещает никаких блокировок. Что собственно и подтверждала замечательная программа Ayende.NHibernateQueryAnalyzer, которая позволяет видеть как NHibernate преобразует встроенный язык запросов hql в требуемый диалект TSQL.
Но посмотрим, что происходит на практике, ведь есть возможность настроить NHibernate, так чтобы он показывал все запросы к базе которые он выполняет. Для этого необходимо в конфигурационный файл добавить следующую строку:

<property name="show_sql">true</property>

Теперь можно смело проводить эксперименты. Попробуем промоделировать самую простейшую ситуацию, которая у нас может получиться. Проект на ASP.NET и на каждый Request-Response у нас создается одна сессия и одна транзакция. Т.о. простейший случай выглядит приблизительно так:

string hqlQuery = "from EventMessage entity where entity.ID = :entityID";
ISession session = NHibernateSessionManager
.Instance
.GetSessionFor("hibernate.cfg.xml");
using (session.BeginTransaction())
{
EventMessage eventMessage = session.CreateQuery(hqlQuery)
.SetParameter("entityID", "123")
.UniqueResult<EventMessage>();
session.Transaction.Commit();
}


В результате, по запуску этого кода, при правильном mapping’е и других факторах мы должны получить один или несколько SELECT запросов к базе. Вот тут моё скромное investigation и начало давайть интересные результаты. На экране я увидел не один запрос, а сразу несколько запросов получения, как к примеру данном случае сущностей EventMessage, следом за ними, что самое интересное следовали UPDATE запросы на все полученные сущности EventMessage. Вот он deadlock, транзакции просто перекрывали друг друга с таким количеством UPDATE запросов.
Встает 2 вопроса:
  1. Откуда столько SELECT запросов на эту сущность, если требуется только один экзэмпляр.
  2. Почему происходит UPDATE полученных сущностей.
Довольно не сложно ответить на первый вопрос, просто просмотрев mapping файлы на наличие lazy="false", что успешно и было проведено.
Второй вопрос гораздо интереснее, с чего NHibernate решил делать UPDATE ?
В замечательной книге “NHibernate in Action” можно найти следующие слова:
«We have taken advantage of a NHibernate feature called transparent persistence: This feature saves us
the effort of explicitly asking NHibernate to update the database when we modify the state of an object
inside a transaction.»

Т.е. такое поведение называется “transparent persistence” и все вроде как было бы нормально, если бы мы действительно изменяли объект. NHibernate на самом деле для определения состояния объекта хранит его копию сразу после получения и сравнивает её с тем экзэмпляром, который попал к нам в руки и, если находит несовпадения, то делает UPDATE. Конечно, можно заставить NHibernate думать, что объект не менялся:

NHibernate.Impl.SessionImpl impSession =(NHibernate.Impl.SessionImpl)session;
NHibernate.Impl.EntityEntry eEntry = impSession.GetEntry(eventMessage);
eEntry.Status = NHibernate.Impl.Status.Gone;

Но такой хак, конечно не будет выходом. Нужно посмотреть, что именно «изменяется».
Сделать можно это несколькими способами: зарегестрировать на сессию свою реализацию интерфейся IIntercepter или на основе того же кода который приведен выше сравнить два массива

eEntry.Persister.GetPropertyValues(eventMessage)

и

eEntry.LoadedState

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

В моем случае выяснилась следующая интересная ситуация. Объект содержал несколько полей типа DateTime, в БД у некоторых строк колонки с этими датами содержали NULL.
NHibernate получая получая из БД значения NULL для значимого типа передавал в сеттер значение default(DateTime). В результате в эти свойства содержали значение 01.01.0001, которое конечно же отличалось от того NULL, который сохранил для себя NHibernate. И все это в итоге приводило к «unexpected update». Способ борьбы очень прост, раз уж эти даты могут быть NULL, то и тип у них должен быть Nullable…т.е. DateTime?

Вот так пара простых промашек с мэппингом привели к deadlock’ам. Будьте внимательны. Удачи.

Комментариев нет: