📜 ⬆️ ⬇️

JAXB vs. org.hibernate.LazyInitializationException

The article will be useful to anyone who is interested in finding out how to resolve the LazyInitializationException error when JAXB serializes objects created with Hibernate.
At the end of the article there is a link to the source code of the project implementing the proposed solution - using custom AccessorFactory.

For comparison, it is considered how a similar problem is solved in the popular JSON serializer - Jackson.

1. And what is the problem?
In our abstract project, in the base under the control of a certain relational DBMS, data on companies, their suppliers and customers are stored in three tables:

image
')
Suppose you want to develop two REST services: the first returns data about the company and its suppliers, the second about the company and its customers:
(Notes: the company about which you need to provide data will be further defined for simplicity in the database by ID = 0, content-type by extension: /HLS/rest/company/suppliers.xml - get data about suppliers in XML.
HLS - context path of the test application: hibernate lazy serialization. I did not invent anything smarter.

The customer wanted to receive data in XML and JSON. For reasons X, Y, Z, the project team decided to use ORM in the form of Hibernate for data access, JAXB for generating XML, Jackson for generating JSON.

Everything, we start to encode:

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 

The code for Customer.java Supplier.java will not be given, there is nothing special.
In package-info.java, we define two fetch profiles:

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

It is easy to see that the companyWithSuppliers will pull out of the supplier database, and leave customers uninitialized. The second profile will do the opposite.
In DAO we will set the desired fetch profile depending on which service is called:

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

We will understand for a start with JSON. An attempt to serialize an object returned by the CompanyDAO.getCompany () method with a standard ObjectMapper Jackson will fail:

image

Sad but quite expected. Session closed, Hibernate proxy, which wrapped collection suppliers, can not pull data from the database. It would be great if Jackson would handle such uninitialized fields in a special way ...

And there is such a solution: jackson-module-hibernate - “add-on module for Jackson JSON processor handles Hibernate <...> datatypes; and specifically aspects of lazy-loading ”. What you need! Let's correct 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, .    . } } 

And serialize the result of the work of CompanyDAO.getCompany () to our new mapper:

image

Great, everything worked - in the final JSON only buyers and no suppliers - the uninitialized collection is simply vanished. Among the shortcomings, it is worth noting the lack of support for Hibernate4, but judging by the information on GitHub, this feature is in the process of development. Go to JAXB.

The JAXB developers thought too globally to worry that their brainchild was not friendly with some kind of Hibernate lazy-loading, and they did not provide any regular solution to the problem:



What to do? The project is almost failed.



And Google said:

2. LazyInitializationException: Common Problem Solving Methods

  1. Do not create lazy collections - use FetchMode.JOIN (FetchType.EAGER).
    No, this option is not suitable - both collections (suppliers and customers) will have to be made non-lazy. Then it turns out that it does not matter which service to call: ... / suppliers.xml or ... / customers.xml - the resulting XML will contain data about both suppliers and customers immediately.
  2. Do not mess with lazy collections - use @XmlTransient (of course, in cases where it is generally advisable to talk about the application of this annotation).
    No, this option is not suitable - both collections (suppliers and customers) will have to be labeled as @XmlTransient. Then it turns out that it does not matter which service to call: ... / suppliers.xml or ... / customers.xml - the resulting XML will not contain data about either the buyers or the suppliers.
  3. Do not let the session close using X, Y, Z tricks (for example, HibernateInterceptor or OpenSessionInViewFilter for Spring and Hibernate3).
    No, this option is not suitable. Unnecessary data will be pulled out from an unclosed session and we get a semblance of clause 1
  4. Use DTO - intermediate layer between DAO and? (in our case? - serializer), where solve the situation.
    It is possible, but you have to write your DTO for each specific case. And in general, the use of DTO should be better justified, because it is a kind of anti-pattern, because causes duplication of data.
  5. Go through the object graph “manually” or using the XYZ tool (for example, Hibernate lazy chopper, if you use Spring) and deal with lazy collections after receiving the object from the DAO.
    This option is not bad and claims to be universal, but in the case of serialization, one problem remains - you will have to go through the object graph twice: the first time to eliminate lazy collections, the second time it will be done by the serializer during serialization.

We come to the idea that ideally a serializer should cut off uninitialized collections by itself, as Jackson does.

3. Custom JAXB AccessorFactory
Among other things, Google issued 2 links, to which it came in the least:
forum.hibernate and blogs.oracle .
It scared away from these articles the lack of a solution suitable for Ctrl + C / Ctrl + V and the excessive overload with all sorts of unnecessary. So I had to creatively modify the contents of the articles and rework. The result is shown below.
So, from the sources mentioned, it is clear what we need to do:
  1. Write your implementation of AccessorFactory (a class of this type is used by JAXB to access the fields / properties of an object while marshalling / unmarshalling)
  2. Tell JAXB to use the AccessorFactory custom implementation.
  3. Tell JAXB where this implementation is located.

Let's go point by point:

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

In order for JAXB to start using custom implementations, JAXBContext should be set to the special property “com.sun.xml.bind.XmlAccessorFactory” = true. (also known as JAXBRIContext.XMLACCESSORFACTORY_SUPPORT), which includes support for the @XmlAccessorFactory annotation. In the case of using Spring, this can be done not directly, but by configuring the "org.springframework.oxm.jaxb.Jaxb2Marshaller" bean in the "jaxbContextProperties" property.

And finally, we specify the class of our implementation using the package-level annotation @XmlAccessorFactory:

 ... @XmlAccessorFactory(JAXBHibernateAccessorFactory.class) package ru.habr.zrd.hls.domain; import com.sun.xml.bind.XmlAccessorFactory; ... 

After performing these operations, we turn to our service to obtain information about the company and customers:



Everything is OK - only buyers and no suppliers. An uninitialized collection with suppliers is set to our AccessorFactory, so JAXB does not try to serialize it and LazyInitializationException does not occur. Then you can direct the beauty - remove the surrogate keys from the issue, etc. But this is another article.

At the end, as promised, a link to the source code of the working example (on Spring Web MVC) on the topic of the article. It uses embedded H2, which is configured by itself when the project is launched, so there is no need to install a separate DBMS. For those who use the Eclipse + STS plugin, the archive has a separate version configured for Eclipse and STS.

That's all, I hope, the article will be useful to someone.

Source: https://habr.com/ru/post/143171/


All Articles