Проблема заключалась в 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();
}
Встает 2 вопроса:
- Откуда столько SELECT запросов на эту сущность, если требуется только один экзэмпляр.
- Почему происходит UPDATE полученных сущностей.
Второй вопрос гораздо интереснее, с чего 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.»
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’ам. Будьте внимательны. Удачи.