Статьи

Ленивая загрузка с Flex, BlazeDS и Hibernate

В этой статье рассматриваются некоторые проблемы, связанные с отправкой графов сложных объектов из Java в Flex, некоторые распространенные обходные пути и то, как отложенная загрузка представляет собой более эффективное решение.

Мы рассмотрим одно из доступных решений с открытым исходным кодом для упрощения обсуждаемых решений — dpHibernate — и рассмотрим его настройку.

Кроме того, мы рассмотрим краткий обзор того, как все это работает, и некоторые распространенные ошибки.

Пример проекта, обсуждаемый в этой статье , доступен для ознакомления здесь

Проблема часть первая: LazyInitializationException

BlazeDS не предоставляет никакой встроенной поддержки отложенной загрузки или поддержки загружаемых объектов Hibernate.

Когда серверный метод возвращает сущность, извлеченную из базы данных через спящий режим, разработчики часто видят исключение LazyInitializationException, выдаваемое из Hibernate / BlazeDS.

Это связано с тем, что сеанс гибернации, который использовался для извлечения из базы данных, закрывается после выхода вызова из уровня DAO. Во время сериализации BlazeDS обходит средства доступа к объекту, вызывая каждый из них и сериализуя его значение. Если он попадает в свойство, которое Hibernate проксирует, генерируется исключение LazyInitializationException, поскольку Hibernate больше не имеет активного сеанса для извлечения значения из базы данных.

dpHibernate

dpHibernate — это adpater сериализации с открытым исходным кодом для BlazeDS, который обеспечивает ленивую загрузку и постоянство для Hibernate. Он решает проблему LazyInitializationException, управляя сеансом гибернации способом, аналогичным OpenSessionInViewFilter в Springs.

Сеанс базы данных открывается и поддерживается dpHibernate, гарантируя, что, когда BlazeDS перейдет к сериализации ответа, сеанс базы данных все еще будет доступен, если это потребуется.

Это предотвращает исключение LazyInitializationException и позволит сериализовать полный граф объектов и вернуть его клиенту Flex.

Слишком много данных

Хотя возможность вернуть полный граф объектов клиенту удобна, она не очень реалистична в реальном сценарии. Это часто может привести к тому, что очень раздутые графы объектов будут передаваться по проводам, что приведет к ненужной дорогостоящей сериализации и десериализации. DpHibernate решает эту проблему, предоставляя истинную поддержку отложенной загрузки сущностей между Flex и Java и прокси-объектов, передаваемых по проводам.

Например, вот пара классов из приложения форума вопрос / ответ:

public class User {
String name;
Set<Post> posts;
}
public class Post {
String title;
String body;
User author;
Set<Post> replies;
}

Эта, казалось бы, простая модель предметной области оказывается чрезвычайно дорогой для сериализации без отложенной загрузки. Например, при сериализации пользователя его коллекция сообщений сериализуется; Каждое сообщение включает в себя собственный набор ответных сообщений; каждый ответ включает его автора, и каждый автор включает свой полный набор сообщений; и т.п.

В результате простой вызов для возврата отдельного пользовательского объекта может привести к графу тяжелых объектов, подобному показанному ниже:

public User getUser(int userId) {}

 

Обходной путь для решения этой проблемы — использовать шаблон DTO, создавая урезанную версию класса User с подмножеством полей, специально для передачи между слоями.

Затем пишутся пользовательские методы для дальнейшего углубления в поля объекта, например:

    public UserDTO getUser(int userId) {}
public List<PostDTO> getPostsByUser(int userId) {};
public List<PostDTO> getRepliesToPost(int postId) {};

Наряду с этим каждой паре «Доменный объект / DTO» обычно требуется транслятор для управления отображением между двумя объектами.

Ленивая загрузка

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

Например, тот же самый вызов для загрузки пользователя в службу dpHibernate возвращает следующее:

Ответ значительно упрощен и подходит для отправки клиенту, поскольку ни один из сложных объектов (например, сообщений) фактически не сериализуется в начальном вызове. Вместо этого dpHibernate отправляет прокси-серверы для всех сложных свойств пользователя. Поскольку клиент вызывает методы получения для проксируемых свойств, их значения загружаются асинхронно с сервера и заполняются обратно в пользовательском объекте.

Поскольку коллекция «posts» заполнена прокси-серверами, ее длина по-прежнему указывается правильно. Это полезно, когда коллекция привязана к сетке данных или списку, поскольку сетки по-прежнему имеют правильный размер и правильно отображают полосы прокрутки, даже если большая часть данных фактически не была загружена на клиенте. Фактически, из-за того, что списки и dataGrids работают во Flex, с сервера будут загружаться только элементы, отображаемые на экране, а не полная коллекция — эта функция особенно полезна при работе с очень большими наборами данных.

Начало работы с dpHibernate

Давайте посмотрим, как настроить dpHibernate в проекте Spring / BlazeDS. В этом примере я буду использовать последнюю версию dpHibernate (2.0-RC3) вместе с Spring 3.0. (Обратите внимание, что dpHibernate не зависит от Spring и работает так же хорошо в средах, не использующих Spring.)

Загрузки

Для начала скачайте загрузку dpHibernate отсюда.

Поскольку мы работаем в среде Spring, нам нужно добавить следующие два jar-файла в classpath:

dpHibernate-core-2.0-RC3.jar
dpHibernate-springExtensions3.0-2.0-RC3.jar

 

Конфигурация: web.xml

При отправке и получении сообщений с BlazeDS dpHibernate необходим активный сеанс базы данных. Чтобы включить это, нам нужен собственный фильтр, чтобы сохранить сеанс:

    <filter>
<filter-name>dpHibernateSessionFilter</filter-name>
<filter-class>org.dphibernate.filters.HibernateSessionServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>dpHibernateSessionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Обратите внимание — это имеет тот же эффект, что и добавление Spring OpenSessionInViewFilter. Итак, если вы уже объявили это в своем файле web.xml, вы можете пропустить этот шаг.

Конфигурация пружины

Далее нам нужно добавить несколько пользовательских бобов Spring. Это простая конфигурация, которая позволяет выполнять отложенную загрузку и пакетную загрузку (кое-что будет рассмотрено позже).

    <!-- Defines the remoting adapter, which intercepts inbound & outbound messages, and routes them thruogh dpHibernate -->
<bean id="dpHibernateRemotingAdapter"
class="org.springframework.flex.core.ManageableComponentFactoryBean">
<constructor-arg
value="org.dphibernate.adapters.RemotingAdapter" />
<property name="properties">
<value>
{"dpHibernate" :
{
"serializerFactory" : "org.dphibernate.serialization.SpringContextSerializerFactory"
}
}
</value>
</property>
</bean>
<!-- Provides a basic service for lazy loading operations through dpHibernate.
It's also exported as a remoting destination, which makes it accessible to flex clients
-->
<bean id="dataAccessService"
class="org.dphibernate.services.SpringLazyLoadService"
autowire="constructor">
<flex:remoting-destination />
</bean>
<!-- The main serializer. Converts outbound POJO's to ASObjects with dpHibernate proxies for lazy loading. Required -->
<bean id="dpHibernateSerializer"
class="org.dphibernate.serialization.HibernateSerializer"
scope="prototype">
<property name="pageSize" value="10"/>
</bean>
<bean id="dpHibernateDeserializer"
class="org.dphibernate.serialization.HibernateDeserializer"
scope="prototype" />


<!-- Set up the dpHibernate adapter to be the default adapter for BlazeDS -->
<flex:message-broker services-config-path="/WEB-INF/flex/services-config.xml">
<flex:remoting-service default-adapter-id="dpHibernateRemotingAdapter"
default-channels="my-amf,my-secure-amf" />
</flex:message-broker>

Настройка модели

Чтобы dpHibernate облегчал отложенную загрузку, классы сущностей должны реализовывать интерфейс org.dphibernate.core.IHibernateProxy как в классах Java, так и в ActionScript. Кроме того, классы Actionscript должны либо реализовывать IManaged, либо использовать метатег [Managed].

Вот простой пример:

BaseEntity.java

@MappedSuperclass
public abstract class BaseEntity implements IHibernateProxy {

@Id
private Integer id;
// Marked as Transient so the field is not persisted to the database by Hibernate
@Transient
private Boolean proxyInitialized = true;

public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
@Override
public Boolean getProxyInitialized() {
return proxyInitialized;
}
@Override
public Object getProxyKey() {
return id;
}
@Override
public void setProxyInitialized(Boolean value) {
proxyInitialized = value;
}

В этом примере я использую общий базовый класс — BaseEntity, который является базовым классом для всех сущностей в проекте, и реализую IHibernateProxy.

BaseEntity.as

[RemoteClass(alias="com.mangofactory.pepper.model.BaseEntity")]
public class BaseEntity implements IHibernateProxy
{
public var id:Number;
private var hibernateProxy:IHibernateProxy=new HibernateBean();

/* Constructor */
public function BaseEntity():void
{
super();
}

//==================================================
// IHibernateProxy impl..
//==================================================
public function get proxyKey():Object
{
return hibernateProxy.proxyKey;
}

public function set proxyKey(value:Object):void
{
hibernateProxy.proxyKey=value;
}

public function get proxyInitialized():Boolean
{
return hibernateProxy.proxyInitialized;
}

public function set proxyInitialized(value:Boolean):void
{
hibernateProxy.proxyInitialized=value;
}

       
Post.as (фрагмент)

    [Managed]
[RemoteClass(alias="com.mangofactory.pepper.model.Post")]
public class Post extends BaseEntity
{
 }

Объявите и инициализируйте сервис dpHibernate

Последний шаг — добавить в проект службу dpHibernate. Этот сервис расширяет класс FlexObject и обеспечивает функциональность dpHibernate в клиенте.

Сначала мы объявляем удаленный объект в теге <Declarations />:

    <fx:Declarations>
<dphibernate:HibernateRemoteObject id="dataService" destination="dataService" bufferProxyLoadRequests="true" fault="faultHandler(event)"/>
</fx:Declarations>

Затем мы инициализируем dpHibernate с этим сервисом по умолчанию:

    protected function onAppComplete(event:FlexEvent):void
{
// Setup a default service.
// This is used by beans to perform dpHibernate operations
HibernateManaged.defaultHibernateService=dataService;
}

Обратите внимание, что здесь мы устанавливаем «bufferProxyLoadRequests» в true. Это дает указание службе пакетировать запросы на загрузку прокси-серверов с сервера, где это возможно. Например, при прокрутке в List или DataGrid несколько запросов на выборку прокси обычно отправляются на сервер (и, следовательно, в базу данных). Когда пакетная обработка включена, dpHibernate использует небольшую задержку между первым запросом прокси и фактической отправкой вызова на сервер. Любые другие прокси, необходимые во время этой паузы, объединяются в один и тот же серверный вызов, сводя к минимуму нагрузку на сервер и базу данных.

Настройка завершена!

Вот и все. Теперь dpHibernate настроен и готов к работе. Вызовы выполняются через удаленный объект dpHibernate так же, как и через обычный удаленный объект (либо путем объявления операции, либо путем непосредственного выполнения метода).

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

Демо-проект

Приведенные выше примеры взяты из демонстрационного проекта, созданного для этой статьи — сайта вопросов / ответов, созданного на основе дампа творческих ресурсов Stack Overflow. (Это не значит, что я думаю, что переполнение стека относится к Flex, это просто хороший, свободно доступный источник большого количества данных!)

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

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

Обычно для этого требуется много разных обращений к серверу (часто по одному для каждой детализации), трансляторы и объекты DTO. Однако благодаря возможности отложенной загрузки данных требовался только один серверный метод, и одна и та же модель расширенного объекта используется от домена Java до домена Flex. (т.е. больше нет DTO!)

Загрузка некоторых данных

Теперь, когда мы настроили и настроили наш проект, dpHibernate остался в стороне. Загрузка данных из удаленного сервиса точно такая же, как и всегда. Давайте посмотрим на пример кода:

  <fx:Script>
<![CDATA[
[Bindable]
public var questionList:ArrayCollection;

public function loadData():void {
var token:AsyncToken = dataService.getRecentPosts();
token.addResponder(new mx.rpc.Responder(resultHandler,faultHandler));
}
protected function resultHandler(event:ResultEvent):void
{
questionList = event.result as ArrayCollection;
}

protected function faultHandler(event:FaultEvent):void
{
Alert.show("Fault when loading data: " + event.fault.faultString);
}

]]>
</fx:Script>
<s:List width="100%" dataProvider="{questionList}" height="100%" contentBackgroundAlpha="0"
itemRenderer="com.mangofactory.pepper.view.QuestionRenderer"
borderAlpha="0"/>

Здесь мы используем стандартный шаблон респондента, чтобы выполнить вызов метода getRecentPosts () на сервере и обработать результат, обновляя ArrayCollection questionList. ArrayCollection служит поставщиком данных для List, который выглядит следующим образом:

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

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

Таким образом, благодаря сервисному методу, который эффективно возвращал список объектов Post, мы также смогли детализировать теги, ответы, пользователей и комментарии, не записывая никаких дополнительных обращений к серверу и не требуя каких-либо объектов DTO или классов переводчиков. ,

Остерегайтесь асинхронных вызовов сервера!

Одно из основных отличий между решением для отложенной загрузки, реализованным во Flex, и решением для Java в том, что Flex выполняет все свои серверные вызовы асинхронно. При первом вызове метода получения прокси-значения dpHibernate вызывает сервер для загрузки значения. Однако до тех пор, пока результат сервера не будет получен, значение на клиенте будет либо нулевым, либо пустым прокси. Привязка данных Flex делает все это достаточно прозрачным при работе в MXML, однако при работе в Actionscript разработчики должны опасаться нулевых значений.

Например — взгляните на этот рендер-манекен:

<s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009" >
<fx:Script>
<![CDATA[
[Bindable("dataChange")]
public function get post():Post
{
return data as Post;
}

private function doSomething():void
{
// A NPE could be thrown here while data is being loaded
var name:String = post.author.displayName;
}
]]>
</fx:Script>
<!-- Flex will swallow any NPEs thrown here while data is being lazy loaded -->
<s:Label text="{post.author.displayName}" />
</s:ItemRenderer>

Доступ к свойству post.author.displayName безопасен в операторе привязки данных, так как механизм dataBinding Flex проглатывает исключения нулевого указателя. Однако доступ к тому же свойству из ActionScript может привести к созданию нулевого указателя.

дальнейшее чтение

Доступны более продвинутые параметры конфигурации, которые позволяют более детально контролировать ленивую загрузку — настраивая, как и когда данные передаются через прокси. Это дает разработчику возможность оптимизировать поездки на сервер.  Проверьте эту статью для получения дополнительной информации .

Кроме того, dpHibernate обеспечивает поддержку полных операций CRUD от клиента, с лишь немного большей конфигурацией, чем то, что уже было рассмотрено в этой статье. Для получения дополнительной информации о персистентных функциях, ознакомьтесь с этой статьей . Кроме того, демо-проект, над которым мы работали сегодня, был еще более расширен с помощью функций полного сохранения.  Проверьте проект здесь. 

Наконец, сайт проекта dpHibernate и список рассылки доступны для поддержки.

Вывод

В этой статье вы увидели, как легко настроить и запустить dpHibernate с отложенной загрузкой. Используя ленивую загрузку для возврата богатой доменной модели с сервера, мы можем минимизировать количество шаблонных сервисов и кода, необходимых для передачи данных между клиентом и сервером. dpHibernate разработан, чтобы тихо работать за кулисами, с минимальной конфигурацией и загрязнением вашей модели сущностей.  

об авторе

Он является ведущим разработчиком dpHibernate и нового инструмента для анализа кода, Marmalade.