The best way to handle the LazyInitializationException
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
The LazyInitializationException is undoubtedly one of the most common exceptions you can get when using Hibernate. This article is going to summarize the best and the worst ways of handling lazy associations.
Fetching 101
With JPA, not only you can fetch entities from the database, but you can also fetch entity associations as well. For this reason, JPA defines two FetchType strategies:
The problem with EAGER fetching
EAGER fetching means that associations are always retrieved along with their parent entity. In reality, EAGER fetching is very bad from a performance perspective because it’s very difficult to come up with a global fetch policy that applies to every business use case you might have in your enterprise application.
Once you have an EAGER association, there is no way you can make it LAZY . This way, the association will always be fetched even if the user does not necessarily need it for a particular use case. Even worse, if you forget to specify that an EAGER association needs to be JOIN FETCH-ed by a JPQL query, Hibernate is going to issue a secondary select for every uninitialized association, leading to N+1 query problems.
Unfortunately, JPA 1.0 decided that @ManyToOne and @OneToOne should default to FetchType.EAGER , so now you have to explicitly mark these two associations as FetchType.LAZY :
@ManyToOne(fetch = FetchType.LAZY) private Post post;
LAZY fetching
For this reason, it’s better to use LAZY associations. A LAZY association is exposed via a Proxy, which allows the data access layer to load the association on demand. Unfortunately, LAZY associations can lead to LazyInitializationException .
For our next example, we are going to use the following entities:
When executing the following logic:
List comments = null; EntityManager entityManager = null; EntityTransaction transaction = null; try < entityManager = entityManagerFactory() .createEntityManager(); transaction = entityManager.getTransaction(); transaction.begin(); comments = entityManager.createQuery( "select pc " + "from PostComment pc " + "where pc.review = :review", PostComment.class) .setParameter("review", review) .getResultList(); transaction.commit(); >catch (Throwable e) < if (transaction != null && transaction.isActive()) transaction.rollback(); throw e; >finally < if (entityManager != null) < entityManager.close(); >> try < for(PostComment comment : comments) < LOGGER.info( "The post title is '<>‘», comment.getPost().getTitle() ); > > catch (LazyInitializationException expected)
Hibernate is going to throw a LazyInitializationException because the PostComment entity did not fetch the Post association while the EntityManager was still opened, and the Post relationship was marked with FetchType.LAZY :
@ManyToOne(fetch = FetchType.LAZY) private Post post;
How NOT to handle LazyInitializationException
Unfortunately, there are also bad ways of handling the LazyInitializationException like:
These two Anti-Patterns are very inefficient from a database perspective, so you should never use them in your enterprise application.
JOIN FETCH to the rescue
Entities are only needed when the current running application-level transaction needs to modify the entities that are being fetched. Because of the automatic dirty checking mechanism, Hibernate makes it very easy to translate entity state transitions into SQL statements.
Considering that we need to modify the PostComment entities, and we also need the Post entities as well, we just need to use the JOIN FETCH directive like in the following query:
comments = entityManager.createQuery( "select pc " + "from PostComment pc " + "join fetch pc.post " + "where pc.review = :review", PostComment.class) .setParameter("review", review) .getResultList();
The JOIN FETCH directive instructs Hibernate to issue an INNER JOIN so that Post entities are fetched along with the PostComment records:
SELECT pc.id AS id1_1_0_ , p.id AS id1_0_1_ , pc.post_id AS post_id3_1_0_ , pc.review AS review2_1_0_ , p.title AS title2_0_1_ FROM post_comment pc INNER JOIN post p ON pc.post_id = p.id WHERE pc.review = 'Excellent!'
That’s it! It’s as simple as that!
DTO projection to the rescue
Now, we are not done yet. What if you don’t even want entities in the first place. If you don’t need to modify the data that’s being read, why would you want to fetch an entity in the first place? A DTO projection allows you to fetch fewer columns and you won’t risk any LazyInitializationException .
For instance, we can have the following DTO class:
public class PostCommentDTO < private final Long id; private final String review; private final String title; public PostCommentDTO( Long id, String review, String title) < this.id = id; this.review = review; this.title = title; >public Long getId() < return id; >public String getReview() < return review; >public String getTitle() < return title; >>
If the business logic only needs a projection, DTOs are much more suitable than entities. The previous query can be rewritten as follows:
List comments = doInJPA(entityManager -> < return entityManager.createQuery( "select new " + " com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentDTO(" + " pc.id, pc.review, p.title" + " ) " + "from PostComment pc " + "join pc.post p " + "where pc.review = :review", PostCommentDTO.class) .setParameter("review", review) .getResultList(); >); for(PostCommentDTO comment : comments) < LOGGER.info("The post title is '<>'", comment.getTitle()); >
And Hibernate can execute a SQL query which only needs to select three columns instead of five:
SELECT pc.id AS col_0_0_ , pc.review AS col_1_0_ , p.title AS col_2_0_ FROM post_comment pc INNER JOIN post p ON pc.post_id = p.id WHERE pc.review = 'Excellent!'
Not only that we got rid of the LazyInitializationException , but the SQL query is even more efficient. Cool, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
![]()
![]()
![]()
Conclusion
LazyInitializationException is a code smell because it might hide the fact that entities are used instead of DTO projections. Sometimes, fetching entities is the right choice, in which case, a JOIN FETCH directive is the simplest and the best way to initialize the LAZY Hibernate proxies.
JAXB vs. org.hibernate.LazyInitializationException
Статья будет полезна всем, кому интересно узнать способ устранения ошибки LazyInitializationException при JAXB сериализации объектов, созданных при помощи Hibernate.
В конце статьи имеется ссылка на исходный код проекта, реализующего предложенное решение — использование custom AccessorFactory.
Для сравнения рассмотрено, как аналогичная проблема решена в популярном JSON-сериализаторе — Jackson.
1. А в чем, собственно, проблема?
На нашем абстрактном проекте в базе под управлением некой реляционной СУБД в трех таблицах хранятся данные о компаниях, их поставщиках и покупателях:
- GET /HLS/rest/company/suppliers HTTP/1.1
Accept: some_content_type - GET /HLS/rest/company/customers HTTP/1.1
Accept: some_content_type
Заказчик пожелал получать данные в 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 Setsuppliers; @OneToMany @JoinColumn(name = "ID_COMPANY") @XmlElementWrapper // Обернем коллекцию дополнительным тегом @XmlElement(name = "customer") private Set 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 в зависимости от того, какой сервис вызван:
Разберемся для начала с 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 дважды: первый раз для устранения ленивых коллекций, второй раз это сделает сериализатор при сериализации.
3. Custom JAXB AccessorFactory
- Написать свою реализацию 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 JAXBHibernateAccessorextends Accessor < private final Accessoraccessor; public JAXBHibernateAccessor(Accessor 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() @Override public Accessor createFieldAccessor(Class bean, Field field, boolean readOnly) throws JAXBException < return new JAXBHibernateAccessor(accessorFactory.createFieldAccessor(bean, field, readOnly)); >@SuppressWarnings() @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.
На этом все, надеюсь, статья кому-нибудь окажется полезной.