вторник, 13 января 2009 г.

Решение проблемы с Lazy loading при использовании Hibernate через Spring


Hibernate поддерживает несколько стратегий загрузки связанных объектов из БД. Одной из самых популярных стратегий является т.н. ленивая загрузка - lazy loading. Предположим, что у нас есть сущность "блог", которая содержит свойство, представляющее собой коллекцию опубликованных постов (коллекцию сущностей типа "пост"). Согласитесь, что незачем выбирать из БД посты, если мы хотим всего лишь получить название блога. Добиться такого поведения нам и помогает ленивая загрузка - коллекция постов будет загружена из БД только, если мы захотим к ней обратиться.

Все операции с БД в Hibernate осуществляются через HibernateSession. В случае ленивой загрузки возможна ситуация, что после получения объекта типа "блог" сессия закроется. Тогда, при попытке обратиться к коллекции постов данного блога мы получим эксепшн. Hibernate не сможит вытянуть эту коллекцию из БД, потому что сессия уже закрыта.


К сожалению, при работе с HibernateSession через Spring, такая ситуация является стандартной. Есть два способа предотвратить негативное развитие событий.

1. OpenSessionInViewFilter
Удобное средство избежать проблем с lazy loading в веб-приложениях - использование специального сервлет-фильтра: org.springframework.orm.hibernate3.support.OpenSessionInViewFilter

Сервлет-фильтр подключается в дескрипторе развертывания web.xml следующим образом:

<web-app>

   ...

   

   <filter>

       <filter-name>hibernateFilter</filter-name>

       <filter-class>

           org.springframework.orm.hibernate3.support.OpenSessionInViewFilter

       </filter-class>

   </filter>



   ...



   <filter-mapping>

       <filter-name>hibernateFilter</filter-name>

       <url-pattern>*.jsp</url-pattern>

   </filter-mapping>

   

   ...

</web-app>


Ограничения:
- Можно использовать только в веб-приложениях
- Для работы с Hibernate в DAO-классах необходимо использовать HibernateTemplate
- Spring-контекст должен быть загружен с помощью web context loader'а, например с помощью WebApplicationContextUtils.getWebApplicationContext(..)
Суть:
Сессия "держится" на протяжении всего цикла обработки HTTP-запроса.

2. HibernateInterceptor
Другим способом является использование HibernateInterceptor'а. Я уже писал про то, как можно подключать HibernateInterceptor'ы с помощью Spring. Но здесь надо понимать, что интерцепторы бывают разными. Есть интерцепторы в терминах Hibernate, которые осуществляют дополнительные действия с БД (про них я собственно и писал) и есть интерцепторы в терминах Spring, которые с помощью AOP влияют на выполнение тех или иных методов. Такие интерцепторы подключаются иначе:

<bean id="hibernateInterceptor"

     class="org.springframework.orm.hibernate3.HibernateInterceptor">

    <property name="sessionFactory">

        <ref bean="sessionFactory"/>

    </property>

</bean>



<bean id="businessObjectTarget" class="org.beq.BusinessObjectImpl">

    <property name="someDAO">

        <ref bean="someDAO" />

    </property>

</bean>



<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">

    <property name="target">

        <ref bean="businessObjectTarget" />

    </property>

    <property name="proxyInterfaces">

        <value>org.beq.IBusinessObject</value>

    </property>

    <property name="interceptorNames">

        <list>

           <value>hibernateInterceptor</value>

        </list>

    </property>

</bean>


Достоинства метода:
Можно использовать как в веб-приложениях, так и в любых других типах приложений.

Недостатки:
К недостаткам можно отнести то, что интерцептор придется подключать к каждому сервису/фасаду/классу, в котором нужно "держать сессию".

Суть:
Сессия "держится" при выполнении метода/последовательности методов объекта, указанного в качестве значения параметра target экземпляра класса ProxyFactoryBean.

Есть еще метод решения данной проблемы, применительно к JUnit тестам. Он подробно описан в статье "Тестируем Spring приложение с помощью JUnit".

UPD 10.03.2011: Проект, демонстрирующий второй способ (Maven-проект, GitHub).

Понравилось сообщение - подпишись на блог

27 комментариев:

Anton комментирует...

а как быть с Lazy loading если у меня клиент на флексе написан (Remote Object)?

Samolisov Pavel комментирует...

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

man.without.face комментирует...

ничего не понял про Interceptor. А именно про HibernateInterceptor. Зачем нужен proxyInterfaces и т.д. Если вас не затруднит, напишите самое простое не вебовское конечно же приложение, в котором вы это используете. А то тема интересная, а понять сложнова-то.

Анонимный комментирует...

OSIVF - это жестокий анти-паттерн :)

Kir комментирует...

Как показывает практика - усложнение зачастую приводит к печальным последствиям. Решать эту проблему надо определённо на сервере =) Эффективнее всего забороть эти исключения sql отжигами - как то так: http://bwinterberg.blogspot.com/2009/08/how-to-eagerly-fetch-associations-with.html

Pavel Samolisov комментирует...

Спасибо за ссылку, интересно.

Анонимный комментирует...

> OSIVF - это жестокий анти-паттерн :)

Вы не могли бы пояснить, какие именно недостатки есть у такого подхода ?

Dzhinn комментирует...

А я всегда проблему с LazyLoading решал через HQL введением "join fetch". Например, есть у нас Авторы и Книги. У Автора много Книг, допустим. Режим lazy="true". Создаем запрос вида: "from Author a left join fetch a.books where ...". Тут так же можно ограничивать выгрузку коллекций через инструменты хибернейта. В общем, грузим то, что нам необходимо в данном случае. Далее нормально работаем с коллекцией книг, никаких проблем. Данный подход позволяет не заботится о состоянии сессии и избавиться от лишних запросов.

Pavel Samolisov комментирует...

Здесь описано решение немного другой проблемы: мы вытащили сущность с помощью get() и теперь хотим получить какое-то ее свойство, которое представляет собой коллекцию. Делаем myEntity.getListOfThings() и получаем LazyLoadingException.

Dzhinn комментирует...

Я о том же самом. Попробуйте сделать то, что я написал и посмотрите результат. Подобного эксцепшена вы не увидите. :)

Pavel Samolisov комментирует...

Я все же считаю, что загружать отдельные объекты из БД проще через Session#get() чем писать каждый раз свой HQL запрос. Плюс, если я добавлю новые ассоциации - то мне придется переписывать все HQL-запросы, поднимающие мой объект. Тем более, что если в случае критериев я еще могу указывать стратегию фетчинга в мэпинге, то в HQL это не работает.

Когда выбирается коллекция объектов и точно известно какие поля понадобятся - ваш метод действительно приемлем, но только в этом случае.

Dzhinn комментирует...

Да, согласен, критерии более абстрактны. Но я говорил именно о конечной реализации, как вариант.

Denis комментирует...
Этот комментарий был удален автором.
BlogUser комментирует...
Этот комментарий был удален автором.
Marat комментирует...

Проверил, 1.OpenSessionInViewFilter работает прекрасно. А вот 2.HibernateInterceptor... и так и сяк... немогу понять как его реализовать.
Вы не могли бы конкретизировать приведенный пример?
В частности, что такое org.beq.BusinessObjectImpl и org.beq.IBusinessObject ?
И зачем вообще упоминается DAO?
Заранее спасибо.

Pavel Samolisov комментирует...

BuisinessObjectImpl - класс, реализующий интерфейс IBusinessObject. Данный класс предоставляет некий сервис для работы с данными. Собственно для этого ему и нужен DAO. Суть в следующем: в DAO помещаются методы непосредственно манипулирования данными - сохранение объекта в БД, его удаление, изменение, какие-то выборки и т.д.

В BusinessObjectImpl же находятся методы, реализующие бизнес-логику приложения (или части приложения). При реализации бизнес-логики иногда приходится и манипулировать данными, причем часто требуется выполнить несколько запросов в рамках одного бизнс-действия - метода данного класса. Например, нужно выбрать всех пользователей, которые логинились в систему ранее, чем 20 дней назад и удалить их. И вот на протяжении выполнения всего этого метода нам нужна одна сессия, т.к. мы хотим использовать ленивую загрузку. Для этого и служит HibernateInterceptor - он позволяет "держать" сессию на протяжении исполнения каждого метода класса BusinessObjectImpl

OpenSessionInViewFilter же "держит" сессию на протяжении всего жизненного цикла обработки HTTP-запроса.

emorozov комментирует...

Можно поподробнее про "Spring-контекст должен быть загружен с помощью web context loader'а, например с помощью WebApplicationContextUtils.getWebApplicationContext(..)"
Где это дело прописать, а то вылетает ексепшн No WebApplicationContext found: no ContextLoaderListener registered?

Pavel Samolisov комментирует...

Вероятно Ваш контекст приложения (xml-файлы с описанием бинов находятся не в classpath), тогда вам нужно зарегистрировать ContextLoaderListener в файле web.xml: http://forum.springsource.org/showthread.php?t=10284

alex комментирует...

Странно, сделал я по первому способу, и вот я хожу по линкам, где каждая страница генерится из базы... и на некоторых страницах пишет no session. Причем никакой закономерности, если перегружу томкет, то на других, из-за чего такое может быть то???

alex комментирует...

вот пример: http://catalog.x-fisher.org.ua/lures/c84-Tsuribito.html тут всё ок, а вот тут уже нет:
http://catalog.x-fisher.org.ua/lures/i553-Mushi.html

alex комментирует...

Так, вроде разобрался из-за чего глючит, из-за того что у меня некоторые DAO-методы, кешируются. Интересно, как можно решить проблема Lazy Loading с кешированием ДАО-методов?

phoenix_1 комментирует...

Оформил второй способ, как показали. Все равно летит LazyInitializationException.

phoenix_1 комментирует...

может есть пример с подробной архитектурой такого
не web приложения?

Pavel Samolisov комментирует...

Примера проекта с открытыми исходными кодами нет.

По поводу вашей проблемы... Не могли бы вы подробно написать что конкретно вы делаете и где именно "летит" Exception. Можно на почту, если проект небольшой - можете заслать исходники, попробую разобраться.

phoenix_1 комментирует...

отправил на почту

Pavel Samolisov комментирует...

Добавлена ссылка на проект, демонстрирующий второй способ.

@phoenix_1 По почте отправил вам подробный ответ.

Влад комментирует...

Спасибо за полезную статью.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!