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>
...
<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>
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).
Понравилось сообщение - подпишись на блог
а как быть с Lazy loading если у меня клиент на флексе написан (Remote Object)?
ОтветитьУдалитьНу вообще я так думаю решать данную проблему надо на сервере, в частности с помощью интерцепторов, если у вас не приложение на базе сервлетов.
ОтветитьУдалитьничего не понял про Interceptor. А именно про HibernateInterceptor. Зачем нужен proxyInterfaces и т.д. Если вас не затруднит, напишите самое простое не вебовское конечно же приложение, в котором вы это используете. А то тема интересная, а понять сложнова-то.
ОтветитьУдалитьOSIVF - это жестокий анти-паттерн :)
ОтветитьУдалитьКак показывает практика - усложнение зачастую приводит к печальным последствиям. Решать эту проблему надо определённо на сервере =) Эффективнее всего забороть эти исключения sql отжигами - как то так: http://bwinterberg.blogspot.com/2009/08/how-to-eagerly-fetch-associations-with.html
ОтветитьУдалитьСпасибо за ссылку, интересно.
ОтветитьУдалить> OSIVF - это жестокий анти-паттерн :)
ОтветитьУдалитьВы не могли бы пояснить, какие именно недостатки есть у такого подхода ?
А я всегда проблему с LazyLoading решал через HQL введением "join fetch". Например, есть у нас Авторы и Книги. У Автора много Книг, допустим. Режим lazy="true". Создаем запрос вида: "from Author a left join fetch a.books where ...". Тут так же можно ограничивать выгрузку коллекций через инструменты хибернейта. В общем, грузим то, что нам необходимо в данном случае. Далее нормально работаем с коллекцией книг, никаких проблем. Данный подход позволяет не заботится о состоянии сессии и избавиться от лишних запросов.
ОтветитьУдалитьЗдесь описано решение немного другой проблемы: мы вытащили сущность с помощью get() и теперь хотим получить какое-то ее свойство, которое представляет собой коллекцию. Делаем myEntity.getListOfThings() и получаем LazyLoadingException.
ОтветитьУдалитьЯ о том же самом. Попробуйте сделать то, что я написал и посмотрите результат. Подобного эксцепшена вы не увидите. :)
ОтветитьУдалитьЯ все же считаю, что загружать отдельные объекты из БД проще через Session#get() чем писать каждый раз свой HQL запрос. Плюс, если я добавлю новые ассоциации - то мне придется переписывать все HQL-запросы, поднимающие мой объект. Тем более, что если в случае критериев я еще могу указывать стратегию фетчинга в мэпинге, то в HQL это не работает.
ОтветитьУдалитьКогда выбирается коллекция объектов и точно известно какие поля понадобятся - ваш метод действительно приемлем, но только в этом случае.
Да, согласен, критерии более абстрактны. Но я говорил именно о конечной реализации, как вариант.
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьПроверил, 1.OpenSessionInViewFilter работает прекрасно. А вот 2.HibernateInterceptor... и так и сяк... немогу понять как его реализовать.
ОтветитьУдалитьВы не могли бы конкретизировать приведенный пример?
В частности, что такое org.beq.BusinessObjectImpl и org.beq.IBusinessObject ?
И зачем вообще упоминается DAO?
Заранее спасибо.
BuisinessObjectImpl - класс, реализующий интерфейс IBusinessObject. Данный класс предоставляет некий сервис для работы с данными. Собственно для этого ему и нужен DAO. Суть в следующем: в DAO помещаются методы непосредственно манипулирования данными - сохранение объекта в БД, его удаление, изменение, какие-то выборки и т.д.
ОтветитьУдалитьВ BusinessObjectImpl же находятся методы, реализующие бизнес-логику приложения (или части приложения). При реализации бизнес-логики иногда приходится и манипулировать данными, причем часто требуется выполнить несколько запросов в рамках одного бизнс-действия - метода данного класса. Например, нужно выбрать всех пользователей, которые логинились в систему ранее, чем 20 дней назад и удалить их. И вот на протяжении выполнения всего этого метода нам нужна одна сессия, т.к. мы хотим использовать ленивую загрузку. Для этого и служит HibernateInterceptor - он позволяет "держать" сессию на протяжении исполнения каждого метода класса BusinessObjectImpl
OpenSessionInViewFilter же "держит" сессию на протяжении всего жизненного цикла обработки HTTP-запроса.
Можно поподробнее про "Spring-контекст должен быть загружен с помощью web context loader'а, например с помощью WebApplicationContextUtils.getWebApplicationContext(..)"
ОтветитьУдалитьГде это дело прописать, а то вылетает ексепшн No WebApplicationContext found: no ContextLoaderListener registered?
Вероятно Ваш контекст приложения (xml-файлы с описанием бинов находятся не в classpath), тогда вам нужно зарегистрировать ContextLoaderListener в файле web.xml: http://forum.springsource.org/showthread.php?t=10284
ОтветитьУдалитьСтранно, сделал я по первому способу, и вот я хожу по линкам, где каждая страница генерится из базы... и на некоторых страницах пишет no session. Причем никакой закономерности, если перегружу томкет, то на других, из-за чего такое может быть то???
ОтветитьУдалитьвот пример: http://catalog.x-fisher.org.ua/lures/c84-Tsuribito.html тут всё ок, а вот тут уже нет:
ОтветитьУдалитьhttp://catalog.x-fisher.org.ua/lures/i553-Mushi.html
Так, вроде разобрался из-за чего глючит, из-за того что у меня некоторые DAO-методы, кешируются. Интересно, как можно решить проблема Lazy Loading с кешированием ДАО-методов?
ОтветитьУдалитьОформил второй способ, как показали. Все равно летит LazyInitializationException.
ОтветитьУдалитьможет есть пример с подробной архитектурой такого
ОтветитьУдалитьне web приложения?
Примера проекта с открытыми исходными кодами нет.
ОтветитьУдалитьПо поводу вашей проблемы... Не могли бы вы подробно написать что конкретно вы делаете и где именно "летит" Exception. Можно на почту, если проект небольшой - можете заслать исходники, попробую разобраться.
отправил на почту
ОтветитьУдалитьДобавлена ссылка на проект, демонстрирующий второй способ.
ОтветитьУдалить@phoenix_1 По почте отправил вам подробный ответ.
Спасибо за полезную статью.
ОтветитьУдалить