Статьи

Четыре решения для LazyInitializationException — Часть 1

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

Чтобы увидеть ошибку LazyInitializationException и обработать ее, мы будем использовать приложение JSF 2 с EJB 3.

Темы поста:

  • Понимая проблему, почему возникает исключение LazyInitializationException?
  • Загрузить коллекцию по аннотации
  • Загрузка коллекции открытым сеансом в представлении (транзакция в представлении)
  • Загрузка коллекции с помощью Stateful EJB с PersistenceContextType.EXTENDED
  • Загрузить коллекцию с помощью Join Query
  • EclipseLink и ленивая коллекция инициализации

В конце этого поста вы найдете исходный код для загрузки.

Внимание : в этом посте мы найдем простой для чтения код, который не применяет шаблоны проектирования. Эта статья посвящена демонстрации решений для исключения LazyInitializationException.

Решения, которые вы найдете здесь, работают для веб-технологий, таких как JSP с Struts, JSP с VRaptor, JSP с сервлетом, JSF с чем-либо еще.

Модельные классы

В посте сегодня мы будем использовать класс Person и Dog:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.model;
 
import javax.persistence.*;
 
@Entity
public class Dog {
 
 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 private int id;
 
 private String name;
 
 public Dog() {
 
 }
 
 public Dog(String name) {
  this.name = name;
 }
 
 //get and set
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.model;
 
import java.util.*;
 
import javax.persistence.*;
 
@Entity
public class Person {
 
 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 private int id;
 
 private String name;
 
 @OneToMany
 @JoinTable(name = 'person_has_lazy_dogs')
 private List<Dog> lazyDogs;
 
 public Person() {
 
 }
 
 public Person(String name) {
  this.name = name;
 }
 
 // get and set
}

Обратите внимание, что с этими двумя классами мы сможем создать исключение LazyInitializationException. У нас есть класс Person со списком собак.

Мы также будем использовать класс для обработки действий с базой данных (EJB DAO) и ManagedBean, чтобы помочь нам создать ошибку и обработать ее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.ejb;
 
import java.util.List;
 
import javax.ejb.*;
import javax.persistence.*;
 
import com.model.*;
 
@Stateless
public class SystemDAO {
 
 @PersistenceContext(unitName = 'LazyPU')
 private EntityManager entityManager;
 
 private void saveDogs(List<Dog> dogs) {
  for (Dog dog : dogs) {
   entityManager.persist(dog);
  }
 }
 
 public void savePerson(Person person) {
   saveDogs(person.getLazyDogs());
   saveDogs(person.getEagerDogs());
   entityManager.persist(person);
 }
 
 // you could use the entityManager.find() method also
 public Person findByName(String name) {
  Query query = entityManager.createQuery('select p from Person p where name = :name');
  query.setParameter('name', name);
 
  Person result = null;
  try {
   result = (Person) query.getSingleResult();
  } catch (NoResultException e) {
   // no result found
  }
 
  return result;
 }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.mb;
 
import javax.ejb.EJB;
import javax.faces.bean.*;
 
import com.ejb.SystemDAO;
import com.model.*;
 
@ManagedBean
@RequestScoped
public class DataMB {
 
 @EJB
 private SystemDAO systemDAO;
 
 private Person person;
 
 public Person getPerson() {
  return systemDAO.findByName('Mark M.');
 }
}

Почему происходит исключение LazyInitializationException?

Класс Person имеет список собак. Самый простой и толстый способ отобразить данные о человеке — использовать метод entityManager.find () и выполнить итерацию по коллекции на странице (xhtml).

Все, что мы хотим, это чтобы код ниже делал это …

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<b> // you could use the entityManager.find() method also
 public Person findByName(String name) {
  Query query = entityManager.createQuery('select p from Person p where name = :name');
  query.setParameter('name', name);
 
  Person result = null;
  try {
   result = (Person) query.getSingleResult();
  } catch (NoResultException e) {
   // no result found
  }
 
  return result;
 }</b>
1
2
3
<b> public Person getPerson() {
  return systemDAO.findByName('Mark M.');
 }</b>
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<b><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'
<h:head>
 
</h:head>
<h:body>
 <h:form>
  <h:dataTable var='dog' value='#{dataMB.personByQuery.lazyDogs}'>
   <h:column>
    <f:facet name='header'>
     Dog name
    </f:facet>
    #{dog.name}
   </h:column>
  </h:dataTable>
 </h:form>
</h:body>
</html></b>

Обратите внимание, что в приведенном выше коде все, что мы хотим сделать, — это найти человека в базе данных и показать его собак пользователю. Если вы попытаетесь получить доступ к странице с кодом выше, вы увидите исключение ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<b>[javax.enterprise.resource.webcontainer.jsf.application] (http–127.0.0.1-8080-2) Error Rendering View[/getLazyException.xhtml]: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.model.Person.lazyDogs, no session or session was closed                 
 
 
at org.hibernate.collection.internal.AbstractPersistentCollection.
throwLazyInitializationException(AbstractPersistentCollection.java:393)
[hibernate-core-4.0.1.Final.jar:4.0.1.Final]
 
 
at org.hibernate.collection.internal.AbstractPersistentCollection.
throwLazyInitializationExceptionIfNotConnected
(AbstractPersistentCollection.java:385) [
hibernate-core-4.0.1.Final.jar:4.0.1.Final]
 
 
 
at org.hibernate.collection.internal.AbstractPersistentCollection.
readSize(AbstractPersistentCollection.java:125) [hibernate-core-4.0.1.Final.jar:4.0.1.Final]
</b>

Чтобы лучше понять эту ошибку, давайте посмотрим, как JPA / Hibernate обрабатывает отношения.

Каждый раз, когда мы делаем запрос в базе данных, JPA будет предоставлять всю информацию этого класса. Исключением из этого правила является случай, когда мы говорим о списке (коллекции). Представьте, что у нас есть объект объявления со списком из 70000 писем, которые получат это объявление. Если вы хотите просто отобразить имя объявления для пользователя на экране, представьте себе работу, которую выполнила бы JPA, если бы 70 000 электронных писем были загружены с этим именем.

JPA создал технологию с именем Lazy Loading для атрибутов классов. Мы можем определить Lazy Loading следующим образом: «Желаемая информация будет загружена (из базы данных) только тогда, когда она необходима».

Обратите внимание, что в приведенном выше коде запрос к базе данных вернет объект Person. Когда вы получаете доступ к коллекции lazyDogs, контейнер заметит, что коллекция lazyDogs является атрибутом lazy, и он «попросит» JPA загрузить эту коллекцию из базы данных.

В момент выполнения запроса ( который принесет коллекцию lazyDogs ) произойдет исключение. Когда JPA / Hibernate пытается получить доступ к базе данных, чтобы получить эту ленивую информацию, JPA заметит, что нет открытой коллекции. Вот почему происходит исключение, отсутствие открытого подключения к базе данных.

Все отношения, заканчивающиеся @Many, будут загружаться по умолчанию с отложенной загрузкой: @OneToMany и @ManyToMany. Все отношения, заканчивающиеся @One, будут загружены по умолчанию: @ManyToOne и @OneToOne. Если вы хотите установить базовое поле (например, имя строки) с отложенной загрузкой, просто сделайте: @Basic (fetch = FetchType.LAZY).

Каждое основное поле (например, String, int, double), которое мы можем найти внутри класса, будет загружено с нетерпением, если разработчик не установит его как ленивое.

Любопытный вопрос о значениях по умолчанию состоит в том, что вы можете найти каждую реализацию JPA (EclipseLink, Hibernate, OpenJPA) с различным поведением для одной и той же аннотации. Мы поговорим об этом позже.

Загрузить коллекцию по аннотации

Самый простой и самый толстый способ создать ленивый список при загрузке объекта — это аннотации. Но это не всегда будет лучший подход .

В приведенном ниже коде мы увидим, как быстро загрузить коллекцию аннотацией:

1
2
3
<b>@OneToMany(fetch = FetchType.EAGER)
@JoinTable(name = 'person_has_eager_dogs')
private List<Dog> eagerDogs;</b>
1
2
3
4
5
6
7
8
<b><h:dataTable var='dog' value='#{dataMB.person.eagerDogs}'>
 <h:column>
  <f:facet name='header'>
   Dog name
  </f:facet>
  #{dog.name}
 </h:column>
</h:dataTable></b>

Плюсы и минусы этого подхода:

Pros

Cons

Легко настроить

Если класс имеет несколько коллекций, это не повлияет на производительность сервера.

Список всегда будет идти с загруженным объектом

Если вы хотите отобразить только базовый атрибут класса, такой как имя или возраст, все коллекции, настроенные как EAGER, будут загружены с именем и возрастом

Такой подход будет хорошей альтернативой, если в коллекции EAGER всего несколько предметов. Если у человека будет только 2, 3 собаки, ваша система сможет справиться с этим очень легко. Если в дальнейшем запуск коллекции Persons собак действительно вырастет, это не скажется на производительности сервера.

Этот подход может быть применен к JSE и JEE.

Загрузка коллекции открытым сеансом в представлении (транзакция в представлении)

Open Session in View (или Transaction in View) — это шаблон проектирования, в котором соединение с базой данных будет оставаться открытым до конца пользовательского запроса. Когда приложение обращается к отложенной коллекции, Hibernate / JPA выполнит запрос к базе данных без проблем, исключений не будет.

Этот шаблон проектирования, при применении к веб-приложениям, использует класс, который реализует фильтр, этот класс будет получать все пользовательские запросы. Этот шаблон проектирования очень прост в применении, и есть два основных действия: открыть соединение с базой данных и закрыть соединение с базой данных.

Вам нужно будет отредактировать « web.xml » и добавить настройки фильтра. Посмотрите ниже, как будет выглядеть наш код:

1
2
3
4
5
6
7
8
<b> <filter>
  <filter-name>ConnectionFilter</filter-name>
  <filter-class>com.filter.ConnectionFilter</filter-class>
 </filter>
 <filter-mapping>
  <filter-name>ConnectionFilter</filter-name>
  <url-pattern>/faces/*</url-pattern>
 </filter-mapping></b>
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<b>package com.filter;
 
import java.io.IOException;
 
import javax.annotation.Resource;
import javax.servlet.*;
import javax.transaction.UserTransaction;
 
public class ConnectionFilter implements Filter {
 
 @Override
 public void destroy() {
 
 }
 
 @Resource
 private UserTransaction utx;
 
 @Override
 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  try {
   utx.begin();
   chain.doFilter(request, response);
   utx.commit();
  } catch (Exception e) {
   e.printStackTrace();
  }
 
 }
 
 @Override
 public void init(FilterConfig arg0) throws ServletException {
 
 }
}</b>
1
2
3
4
5
6
7
8
<b><h:dataTable var='dog' value='#{dataMB.person.lazyDogs}'>
 <h:column>
  <f:facet name='header'>
   Dog name
  </f:facet>
  #{dog.name}
 </h:column>
</h:dataTable></b>

Плюсы и минусы этого подхода:

Pros

Cons

Модельные классы редактировать не нужно

Все транзакции должны быть обработаны в классе фильтра

Разработчик должен быть очень осторожен с ошибками транзакций базы данных. ManagedBean / Servlet может отправить сообщение об успехе, но когда база данных фиксирует транзакцию, может произойти ошибка

Эффект N + 1 может произойти (более подробно ниже)

Основной проблемой этого подхода является эффект N + 1. Когда метод возвращает человека на страницу пользователя, страница перебирает коллекцию собак. Когда страница обращается к ленивой коллекции, будет запущен новый запрос к базе данных, чтобы вывести список ленивых собак. Представьте, есть ли у Собаки коллекция собак, собак детей. Для загрузки списка дочерних собак будет запущен другой запрос к базе данных. Но если у детей есть другие дети, JPA снова запустит новый запрос к базе данных … и вот оно …

Это главная проблема этого подхода. Запрос может создавать почти бесконечное число других запросов.

Этот подход может быть применен к JSE и JEE.

Перейдите ко второй части этого урока.

Ссылка: четыре решения для LazyInitializationException от нашего партнера JCG Хеберта Коэльо в блоге uaiHebert .