JAXB vs. org.hibernate.LazyInitializationException

в 12:04, , рубрики: hibernate, java, JAXB, Песочница, метки: , ,

Статья будет полезна всем, кому интересно узнать способ устранения ошибки 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, и никакого штатного средства решения проблемы не предоставили:

JAXB vs. org.hibernate.LazyInitializationException

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

JAXB vs. org.hibernate.LazyInitializationException

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

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;
...

После выполнения указанных операций обратимся к нашему сервису для получения данных о компании и покупателях:

JAXB vs. org.hibernate.LazyInitializationException

Все ок — только покупатели и нет поставщиков. Неинициализированная коллекция с поставщиками занулена нашей AccessorFactory, поэтому JAXB не пытается ее сериализовать и LazyInitializationException не возникает. Дальше можно наводить красоту — убрать суррогатные ключи из выдачи и др. Но это уже другая статья.

В конце, как и обещал, ссылка на исхoдный код рабочего примера (на Spring Web MVC) по теме статьи. В нем используется embedded H2, которая конфигурируется сама при запуске проекта, поэтому отдельной СУБД ставить не нужно. Для тех, кто использует Eclipse + STS plugin, в архиве есть отдельная версия, настроенная под Eclipse и STS.

На этом все, надеюсь, статья кому-нибудь окажется полезной.

Автор: IIIkiper


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js