Как стать автором
Обновить

JAXB vs. org.hibernate.LazyInitializationException

Время на прочтение7 мин
Количество просмотров5K
Статья будет полезна всем, кому интересно узнать способ устранения ошибки LazyInitializationException при JAXB сериализации объектов, созданных при помощи Hibernate.
В конце статьи имеется ссылка на исходный код проекта, реализующего предложенное решение — использование custom AccessorFactory.

Для сравнения рассмотрено, как аналогичная проблема решена в популярном JSON-сериализаторе — Jackson.

1. А в чем, собственно, проблема?
На нашем абстрактном проекте в базе под управлением некой реляционной СУБД в трех таблицах хранятся данные о компаниях, их поставщиках и покупателях:

image

Допустим, требуется разработать два 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 потерпит неудачу:

image

Печально, но вполне ожидаемо. Сессия закрылась, 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:

image

Отлично, все заработало — в итоговом JSON только покупатели и нет поставщиков — неинициализированная коллекция просто занулена. Из недостатков стоит отметить отсутствие поддержки для Hibernate4, но судя по информации на GitHub, эта фича в процессе разработки. Переходим к JAXB.

Разработчики JAXB мыслили слишком глобально, чтобы переживать, что их детище не дружит с каким-то там Hibernate lazy-loading, и никакого штатного средства решения проблемы не предоставили:



Что делать? Проект почти провален.



И сказал Гугл:

2. LazyInitializationException: общие методы решения проблемы

  1. Не создавайте ленивые коллекции — используйте FetchMode.JOIN (FetchType.EAGER).
    Нет, этот вариант не подходит — обе коллекции (suppliers и customers) придется сделать неленивыми. Тогда получится, что неважно, какой сервис вызывать: .../suppliers.xml или .../customers.xml — полученный XML будет содержать данные и о поставщиках, и о покупателях сразу.
  2. Не связывайтесь с ленивыми коллекциями — используйте @XmlTransient (конечно, в случаях, где вообще целесообразно говорить о применении этой аннотации).
    Нет, этот вариант не подходит — обе коллекции (suppliers и customers) придется маркировать, как @XmlTransient. Тогда получится, что неважно, какой сервис вызывать: .../suppliers.xml или .../customers.xml — полученный XML не будет содержать данных ни о покупателях, ни о поставщиках.
  3. Не давайте сессии закрыться, используя приемы X, Y, Z. (к примеру HibernateInterceptor или OpenSessionInViewFilter — для Spring и Hibernate3).
    Нет, этот вариант не подходит. Из незакрытой сессии вытянутся ненужные данные и мы получаем подобие пункта 1.
  4. Используйте DTO — промежуточный слой между DAO и? (в нашем случае? — сериализатор), где разрулите ситуацию.
    Можно, но придется писать свое DTO для каждого конкретного случая. И вообще, использование DTO должно быть получше обосновано, ведь это своего рода антипаттерн, т.к. вызывает дублирование данных.
  5. Пройдитесь по object graph «вручную» или с помощью средства XYZ (например, Hibernate lazy chopper, если используете Spring) и разберитесь с ленивыми коллекциями после получения объекта из DAO.
    Этот вариант неплох и претендует на звание универсального, но в случае с сериализацией остается одна проблема — придется пройтись по object graph дважды: первый раз для устранения ленивых коллекций, второй раз это сделает сериализатор при сериализации.

Мы подходим к мысли, что в идеале сериализатор должен сам отсекать неинициализированные коллекции — так, так как это делает Jackson.

3. Custom JAXB AccessorFactory
Помимо прочего, гугл выдал 2 ссылки, до которых дело дошло в последнюю очередь:
forum.hibernate и blogs.oracle.
Отпугивало от этих статей отсутствие решения, пригодного для Ctrl+C/Ctrl+V и излишняя перегруженость всякими ненужностями. Так что пришлось содержимое статей творчески доработать и переработать. Результат представлен ниже.
Итак, из упомянутых источников ясно, что нам нужно сделать:
  1. Написать свою реализацию AccessorFactory (класс этого типа используется JAXB для доступа к fields/properties объекта при marshalling/unmarshalling)
  2. Сказать JAXB, что он должен использовать custom-реализации AccessorFactory.
  3. Сказать 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.

На этом все, надеюсь, статья кому-нибудь окажется полезной.
Теги:
Хабы:
Всего голосов 26: ↑24 и ↓2+22
Комментарии43

Публикации

Истории

Работа

Java разработчик
367 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань