Проблема с производительностью NHibernate при загрузке больших коллекций

#nhibernate

#nhibernate

Вопрос:

(просто чтобы прояснить: мое приложение на самом деле не предназначено для сотрудников и отделов. Я просто использую эти термины для примера).
В каждом отделе есть коллекция employees, которая загружается лениво. Всякий раз, когда я добавляю нового сотрудника, я хочу убедиться, что он еще не существует в коллекции, поэтому я загружаю коллекцию в память и выполняю ее проверку.
Проблема в том, что в производственной среде у меня есть несколько отделов с более чем 10000 сотрудниками.
Я обнаружил, что получение коллекции и последующее сохранение нового сотрудника занимает МНОГО времени.
Я провел небольшой эксперимент, в котором скопировал точно такой же оператор select, сгенерированный nH, в ADO.Net SqlDataAdapter. Вот результаты:

 ***16:04:50:437*** DEBUG NHibernate.SQL - SELECT ... FROM dbo.[Employee] emp0_ left outer join dbo.[Department] department1_ on emp0_.Department_id=department1_.Id left outer join dbo.[TableC] TableC2_ on department1_.TableC_id=TableC2_.Id WHERE emp0_.SomeField_id=@p0;@p0 = 2
***16:05:00:250*** DEBUG NHibernate.SQL - SELECT ... FROM dbo.TableD codeshared0_ left outer join dbo.[Department] department1_ on codeshared0_.Department_id=department1_.Id left outer join dbo.[TableC] TableC2_ on department1_.TableC_id=TableC2_.Id WHERE codeshared0_.Employee_id in (select emp0_.Id FROM dbo.[Employee] emp0_ left outer join dbo.[Department] department1_ on emp0_.Department_id=department1_.Id left outer join dbo.[TableC] TableC2_ on department1_.TableC_id=TableC2_.Id WHERE emp0_.SomeField_id=@p0);@p0 = 2
16:05:04:984 DEBUG NHibernate.SQL - Reading high value:select next_hi from dbo._uniqueKey with (updlock, rowlock)
16:05:05:078 DEBUG NHibernate.SQL - Updating high value:update dbo._uniqueKey set next_hi = @p0 where next_hi = @p1;@p0 = 10686, @p1 = 10685
***16:05:05:328*** DEBUG MyApp.Managers - commiting
16:05:12:000 DEBUG NHibernate.SQL - INSERT INTO dbo.[Employee] (...) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9);@p0 = 23/04/2011 04:04:49, @p1 = 23/04/2011 03:34:49, @p2 = 23/04/2011 04:04:49, @p3 = 23/04/2011 03:34:49, @p4 = '', @p5 = False, @p6 = 433, @p7 = NULL, @p8 = 2, @p9 = 10685
16:05:12:140 DEBUG NHibernate.SQL - UPDATE dbo.[Employee] SET Department_id = @p0 WHERE Id = @p1;@p0 = 2, @p1 = 10685
16:05:12:343 DEBUG MyApp.Managers - success
16:05:12:359 DEBUG MyApp.Tests - ------------------------------------------------------------
16:05:12:359 DEBUG MyApp.Tests - Finished nHib stuff- now switching to ADO 
16:05:12:359 DEBUG MyApp.Tests - starting SQL: SELECT ... FROM dbo.[Employee] emp0_ left outer join dbo.[Department] department1_ on emp0_.Department_id=department1_.Id left outer join dbo.[TableC] TableC2_ on department1_.TableC_id=TableC2_.Id WHERE emp0_.SomeField_id=2
16:05:14:750 DEBUG MyApp.Tests - total rows received: 10036
16:05:14:750 DEBUG MyApp.Tests - SQL: SELECT ... FROM dbo.TableD codeshared0_ left outer join dbo.[Department] department1_ on codeshared0_.Department_id=department1_.Id left outer join dbo.[TableC] TableC2_ on department1_.TableC_id=TableC2_.Id WHERE codeshared0_.Employee_id in (select emp0_.Id FROM dbo.[Employee] emp0_ left outer join dbo.[Department] department1_ on emp0_.Department_id=department1_.Id left outer join dbo.[TableC] TableC2_ on department1_.TableC_id=TableC2_.Id WHERE emp0_.SomeField_id=2)
16:05:15:250 DEBUG MyApp.Tests - total rows received: 2421
  

как вы можете видеть — выборка занимает ~ 15 секунд с nH, по сравнению с ~ 2 секундами с ADO.Net .
Из небольшого исследования я знаю, что nH, вероятно, не предназначен для хранения такого количества элементов в сеансе. Можете ли вы назвать какую-либо другую возможную причину этой проблемы или другое предложение, отличное от фильтрации сотрудников на уровне базы данных?

Спасибо

РЕДАКТИРОВАТЬ
Следуя приведенным ниже предложениям, я попытался использовать Reflection Optimizer (безрезультатно) и IStatelessSession для загрузки моей коллекции (выдает исключение — коллекции не могут быть извлечены сеансом без состояния.).
Я думаю, что мой код в классе Department придется изменить с чистого:

 if (this.Employees.Contains(emp))
{
  ...
}  
  

к этой «более грязной» версии:

 var employeesRepository = IOCContainer.Get<IEmployeesRepository>();  
if (employeesRepository.EmployeeExists(this,emp))
{
  ...
}  
  

у кого-нибудь есть предложение получше?

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

1. вы включили оптимизатор отражения?

2. Каковы ограничения на поиск на уровне базы данных. NHibernate действительно накладывает большие накладные расходы из-за процесса регидратации (преобразования записей базы данных в объекты .net). Но в NHibernate есть так много способов поиска, что я уверен, что один из них должен делать то, что вам нужно на уровне базы данных.

3. @driushkin: Не уверен, как это настроить, используя fluent nH; пробовал .ExposeConfiguration(x=> x.setProperty(«use_reflection_optimizer», «true»)), но, похоже, это не помогает. Есть какие-нибудь подсказки? @Michael: можете ли вы предложить какой-либо конкретный вариант, который работал бы для свойства?

4. Почему вы загружаете все, а затем выполняете сравнение? Разве вы не можете использовать запрос критериев или даже LINQ для сравнения в БД?

5. @sJhonny вы должны быть в состоянии сделать это с помощью fluent configuration. Fluently.Configure().Database(PostgreSQLConfiguration.PostgreSQL82.UseReflectionOptimizer() //..

Ответ №1:

У вас нет причин загружать все ресурсы в память. вы должны написать запрос, используя HQL / Critiria API / Linq к NHibernate, чтобы проверить, существует ли employee в базе данных. например:

 var existingEmpoyee = session.Query<Employee>()
                             .Where(e => e.Equals(newEmployee))
                             .FirstOrDefault();
if(existingEmployee != null)
   // Insert new employee to DB
  

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

1. Если все, что вы делаете, это проверяете, существует ли employee, тогда вы можете использовать ‘Any’, который вернет вам true или false.

2. В итоге я сделал нечто подобное, используя IoC, как описано в редактировании моего вопроса. Спасибо.

3. @ItalyMoaz возникает вопрос — мы никогда не должны получать большие коллекции полностью увлажненными, так каков наилучший подход даже для моделирования этого, чтобы разработчики не пытались это сделать?

Ответ №2:

Я бы использовал StatelessSession и пакетную оптимизацию.

Сеанс будет отслеживать все загруженные объекты, и если мы загрузим много данных, он в конечном итоге завершится с исключением нехватки памяти. К счастью, у NHibernate есть готовое решение для этого — сеанс без состояния. Теперь код выглядит следующим образом:

 using (IStatelessSession s = sessionFactory.OpenStatelessSession())
{
    var books = new ActionableList<Book>(book => Console.WriteLine(book.Name));
    s.CreateQuery("from Book")
        .List(books);

}
  

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

Для пакетной оптимизации и многого другого: Хитрости производительности NHibernate.

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

1. Я использую шаблон «сеанс для каждого запроса» в своем веб-приложении. Очевидно, что во всех других случаях, кроме этого, мне нужна обычная ISession. как бы вы посоветовали мне подойти к этому? Я не могу использовать NHibernate. Context.CurrentSessionContext. Привязка с помощью IStatelessSession

2. @sJohnny Хм, я не могу говорить по опыту, поскольку я не использую шаблон, но не могли бы вы в Application_BeginRequest определить, поступает ли запрос из местоположения, где вы хотите сеанс без состояния, и если да, пропустить обычную привязку сеанса?

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

Ответ №3:

хммм. возможно, это слишком много, и на самом деле — я бы ожидал, что «ленивый = дополнительный» ISet будет вести себя подобным образом, но вы можете написать свой собственный «очень ленивый» ISet. если вы не столкнулись с дополнительной отложенной коллекцией — это коллекция, которая, например, когда вы запрашиваете ее количество, не извлекает все, но выдает запрос count. ваш дополнительный ленивый ISet может выдавать запрос exists всякий раз, когда вы пытаетесь что-то добавить.

Если вы реализуете это, ваш код будет чистым, и вы сможете отправить код в ядро nhibernate.

однако вам следует подумать о добавлении диапазона и быть осторожным, чтобы не выдавать N запросов

удачи.

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

1. это немного излишне. кто-то мудрый однажды сказал: «Вы не хотите изобретать колесо». в качестве дополнительного примечания — на самом деле я исследовал «extra = lazy» в то время, и (я не помню, кто из команды NHib) сказал, что поведение запроса count на самом деле не является преднамеренным, и что пользователи не должны полагаться на это.