Статья будет полезна всем, кому интересно узнать способ устранения ошибки LazyInitializationException при JAXB сериализации объектов, созданных при помощи Hibernate.
В конце статьи имеется ссылка на исходный код проекта, реализующего предложенное решение — использование custom AccessorFactory.
Для сравнения рассмотрено, как аналогичная проблема решена в популярном JSON-сериализаторе — Jackson.
1. А в чем, собственно, проблема?
На нашем абстрактном проекте в базе под управлением некой реляционной СУБД в трех таблицах хранятся данные о компаниях, их поставщиках и покупателях:
Допустим, требуется разработать два REST-сервиса: первый возвращает данные о компании и ее поставщиках, второй — о компании и ее клиентах:
- GET /HLS/rest/company/suppliers HTTP/1.1
Accept: some_content_type - GET /HLS/rest/company/customers HTTP/1.1
Accept: some_content_type
(Примечания: компанию, о которой нужно предоставить данные, будем в дальнейшем для простоты определять в базе по ID=0, content-type — по расширению: /HLS/rest/company/suppliers.xml — получить данные о поставщиках в XML.
HLS — context path тестового приложения: hibernate lazy serialization. Ничего умнее не придумал.
Заказчик пожелал получать данные в XML и JSON. По причинам X, Y, Z проектная команда решила для доступа к данным использовать ORM в виде Hibernate, JAXB — для генерации XML, Jackson — для генерации JSON.
Все, начинаем кодировать:
package ru.habr.zrd.hls.domain;
...
@Entity
@Table(name = "COMPANY")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Company {
@Id
@GeneratedValue
private Integer id;
@Column(name = "S_NAME")
private String name;
@OneToMany
@JoinColumn(name = "ID_COMPANY")
@XmlElementWrapper // Обернем коллекцию дополнительным тегом
@XmlElement(name = "supplier")
private Set<Supplier> suppliers;
@OneToMany
@JoinColumn(name = "ID_COMPANY")
@XmlElementWrapper // Обернем коллекцию дополнительным тегом
@XmlElement(name = "customer")
private Set<Customer> customers;
// Getters/setters
Код для Customer.java Supplier.java приводить не буду, там нет ничего особенного.
В package-info.java определим два fetch profile:
@FetchProfiles({
@FetchProfile(name = "companyWithSuppliers", fetchOverrides = {
@FetchProfile.FetchOverride(entity = Company.class, association = "suppliers", mode = FetchMode.JOIN),
}),
@FetchProfile(name = "companyWithCustomers", fetchOverrides = {
@FetchProfile.FetchOverride(entity = Company.class, association = "customers", mode = FetchMode.JOIN)
})
})
package ru.habr.zrd.hls.domain;
Нетрудно заметить, что «companyWithSuppliers» вытянет из базы поставщиков, а покупателей оставит неинициализированными. Второй profile сделает наоборот.
В DAO будем выставлять нужный fetch profile в зависимости от того, какой сервис вызван:
...
public class CompanyDAO {
public Company getCompany(String fetchProfile) {
...
Session session = sessionFactory.getCurrentSession();
session.enableFetchProfile(fetchProfile);
Company company = (Company) session.get(Company.class, 0);
...
return company;
}
...
Разберемся для начала с JSON. Попытка сериализовать объект, возвращенный методом CompanyDAO.getCompany(), стандартным ObjectMapper Jackson'a потерпит неудачу:
Печально, но вполне ожидаемо. Сессия закрылась, Hibernate proxy, которым обернута коллекция suppliers, не может вытянуть данные из базы. Вот было бы здорово, если б такие неинициализированные поля Jackson обрабатывал бы особым образом…
И такое решение есть: jackson-module-hibernate — “add-on module for Jackson JSON processor which handles Hibernate <...> datatypes; and specifically aspects of lazy-loading”. То что надо! Подправим ObjectMapper:
import org.codehaus.jackson.map.ObjectMapper;
import com.fasterxml.jackson.module.hibernate.HibernateModule;
public class JSONHibernateObjectMapper extends ObjectMapper {
public JSONHibernateObjectMapper() {
registerModule(new HibernateModule());
//Справедливости ради, стоит отметить, что тут разработчики рекоммендуют
//установить еще какие-то малопонятные property, см. ссылку в тексте выше.
}
}
И сериализуем результат работы CompanyDAO.getCompany() нашим новым mapper:
Отлично, все заработало — в итоговом JSON только покупатели и нет поставщиков — неинициализированная коллекция просто занулена. Из недостатков стоит отметить отсутствие поддержки для Hibernate4, но судя по информации на GitHub, эта фича в процессе разработки. Переходим к JAXB.
Разработчики JAXB мыслили слишком глобально, чтобы переживать, что их детище не дружит с каким-то там Hibernate lazy-loading, и никакого штатного средства решения проблемы не предоставили:
Что делать? Проект почти провален.
И сказал Гугл:
2. LazyInitializationException: общие методы решения проблемы
- Не создавайте ленивые коллекции — используйте FetchMode.JOIN (FetchType.EAGER).
Нет, этот вариант не подходит — обе коллекции (suppliers и customers) придется сделать неленивыми. Тогда получится, что неважно, какой сервис вызывать: .../suppliers.xml или .../customers.xml — полученный XML будет содержать данные и о поставщиках, и о покупателях сразу. - Не связывайтесь с ленивыми коллекциями — используйте @XmlTransient (конечно, в случаях, где вообще целесообразно говорить о применении этой аннотации).
Нет, этот вариант не подходит — обе коллекции (suppliers и customers) придется маркировать, как @XmlTransient. Тогда получится, что неважно, какой сервис вызывать: .../suppliers.xml или .../customers.xml — полученный XML не будет содержать данных ни о покупателях, ни о поставщиках. - Не давайте сессии закрыться, используя приемы X, Y, Z. (к примеру HibernateInterceptor или OpenSessionInViewFilter — для Spring и Hibernate3).
Нет, этот вариант не подходит. Из незакрытой сессии вытянутся ненужные данные и мы получаем подобие пункта 1. - Используйте DTO — промежуточный слой между DAO и? (в нашем случае? — сериализатор), где разрулите ситуацию.
Можно, но придется писать свое DTO для каждого конкретного случая. И вообще, использование DTO должно быть получше обосновано, ведь это своего рода антипаттерн, т.к. вызывает дублирование данных. - Пройдитесь по object graph «вручную» или с помощью средства XYZ (например, Hibernate lazy chopper, если используете Spring) и разберитесь с ленивыми коллекциями после получения объекта из DAO.
Этот вариант неплох и претендует на звание универсального, но в случае с сериализацией остается одна проблема — придется пройтись по object graph дважды: первый раз для устранения ленивых коллекций, второй раз это сделает сериализатор при сериализации.
Мы подходим к мысли, что в идеале сериализатор должен сам отсекать неинициализированные коллекции — так, так как это делает Jackson.
3. Custom JAXB AccessorFactory
Помимо прочего, гугл выдал 2 ссылки, до которых дело дошло в последнюю очередь:
forum.hibernate и blogs.oracle.
Отпугивало от этих статей отсутствие решения, пригодного для Ctrl+C/Ctrl+V и излишняя перегруженость всякими ненужностями. Так что пришлось содержимое статей творчески доработать и переработать. Результат представлен ниже.
Итак, из упомянутых источников ясно, что нам нужно сделать:
- Написать свою реализацию AccessorFactory (класс этого типа используется JAXB для доступа к fields/properties объекта при marshalling/unmarshalling)
- Сказать JAXB, что он должен использовать custom-реализации AccessorFactory.
- Сказать JAXB, где эта реализация находится.
Поехали по пунктам:
...
import com.sun.xml.bind.AccessorFactory;
import com.sun.xml.bind.AccessorFactoryImpl;
import com.sun.xml.bind.api.AccessorException;
import com.sun.xml.bind.v2.runtime.reflect.Accessor;
public class JAXBHibernateAccessorFactory implements AccessorFactory {
// Реализация AccessorFactory уже написана - AccessorFactoryImpl. Она не содержит public
// конструкторов, и отнаследоваться от нее не получится, поэтому сделаем ее делегатом
// и напишем wrapper.
private final AccessorFactory accessorFactory = AccessorFactoryImpl.getInstance();
// Также потребуется некая реализация Accessor. Поскольку больше она нигде не нужна, сделаем
// ее в виде private inner class, чтобы не болталась по проекту.
private static class JAXBHibernateAccessor<B, V> extends Accessor<B, V> {
private final Accessor<B, V> accessor;
public JAXBHibernateAccessor(Accessor<B, V> accessor) {
super(accessor.getValueType());
this.accessor = accessor;
}
@Override
public V get(B bean) throws AccessorException {
V value = accessor.get(bean);
// Вот оно! Ради этого весь сыр-бор. Если кому-то простое зануление
// может показаться неправильным, он волен сделать тут все, что
// захочется. Метод Hibernate.isInitialized() c одинаковым поведением
// присутствует и в Hibernate3, и Hibernate4.
return Hibernate.isInitialized(value) ? value : null;
}
@Override
public void set(B bean, V value) throws AccessorException {
accessor.set(bean, value);
}
}
// Определим необходимые методы, используя делегат и inner Accessor.
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Accessor createFieldAccessor(Class bean, Field field, boolean readOnly)
throws JAXBException {
return new JAXBHibernateAccessor(accessorFactory.createFieldAccessor(bean, field, readOnly));
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Accessor createPropertyAccessor(Class bean, Method getter, Method setter)
throws JAXBException {
return new JAXBHibernateAccessor(accessorFactory.createPropertyAccessor(bean, getter, setter));
}
}
Чтобы JAXB начал использовать custom-реализации следует JAXBContext установить специальное свойство «com.sun.xml.bind.XmlAccessorFactory» = true. (оно же JAXBRIContext.XMLACCESSORFACTORY_SUPPORT), которое включает поддержку аннотации @XmlAccessorFactory. В случае использования Spring, сделать это можно не на прямую, а при конфигурировании бина «org.springframework.oxm.jaxb.Jaxb2Marshaller» в свойстве «jaxbContextProperties».
И, наконец, указываем класс нашей реализации при помощи package-level аннотации @XmlAccessorFactory:
...
@XmlAccessorFactory(JAXBHibernateAccessorFactory.class)
package ru.habr.zrd.hls.domain;
import com.sun.xml.bind.XmlAccessorFactory;
...
После выполнения указанных операций обратимся к нашему сервису для получения данных о компании и покупателях:
Все ок — только покупатели и нет поставщиков. Неинициализированная коллекция с поставщиками занулена нашей AccessorFactory, поэтому JAXB не пытается ее сериализовать и LazyInitializationException не возникает. Дальше можно наводить красоту — убрать суррогатные ключи из выдачи и др. Но это уже другая статья.
В конце, как и обещал, ссылка на исхoдный код рабочего примера (на Spring Web MVC) по теме статьи. В нем используется embedded H2, которая конфигурируется сама при запуске проекта, поэтому отдельной СУБД ставить не нужно. Для тех, кто использует Eclipse + STS plugin, в архиве есть отдельная версия, настроенная под Eclipse и STS.
На этом все, надеюсь, статья кому-нибудь окажется полезной.
Автор: IIIkiper