Как решить проблему "не удалось лениво инициализировать коллекцию ролей" в исключении Hibernate
У меня есть эта проблема:
org.hibernate.LazyInitializationException: не удалось лениво инициализировать коллекцию ролей: mvc3.model.Topic.comments, ни один сеанс или сеанс не был закрыт
Вот модель:
@Entity
@Table(name = "T_TOPIC")
public class Topic {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private int id;
@ManyToOne
@JoinColumn(name="USER_ID")
private User author;
@Enumerated(EnumType.STRING)
private Tag topicTag;
private String name;
private String text;
@OneToMany(mappedBy = "topic", cascade = CascadeType.ALL)
private Collection<Comment> comments = new LinkedHashSet<Comment>();
...
public Collection<Comment> getComments() {
return comments;
}
}
Контроллер, который вызывает модель, выглядит следующим образом:
@Controller
@RequestMapping(value = "/topic")
public class TopicController {
@Autowired
private TopicService service;
private static final Logger logger = LoggerFactory.getLogger(TopicController.class);
@RequestMapping(value = "/details/{topicId}", method = RequestMethod.GET)
public ModelAndView details(@PathVariable(value="topicId") int id)
{
Topic topicById = service.findTopicByID(id);
Collection<Comment> commentList = topicById.getComments();
Hashtable modelData = new Hashtable();
modelData.put("topic", topicById);
modelData.put("commentList", commentList);
return new ModelAndView("/topic/details", modelData);
}
}
Страница jsp выглядит следующим образом:
<%@page import="com.epam.mvc3.helpers.Utils"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
<title>View Topic</title>
</head>
<body>
<ul>
<c:forEach items="${commentList}" var="item">
<jsp:useBean id="item" type="mvc3.model.Comment"/>
<li>${item.getText()}</li>
</c:forEach>
</ul>
</body>
</html>
Исключение повышается при просмотре jsp. В строке с циклом c:forEach
35 ответов
Если вы знаете, что вы хотите увидеть все Comment
каждый раз, когда вы получаете Topic
затем измените ваше отображение поля для comments
чтобы:
@OneToMany(fetch = FetchType.EAGER, mappedBy = "topic", cascade = CascadeType.ALL)
private Collection<Comment> comments = new LinkedHashSet<Comment>();
Коллекции загружаются по умолчанию, посмотрите на это, если хотите узнать больше.
Исходя из моего опыта, у меня есть следующие методы для решения известной исключительной ситуации LazyInitializationException:
(1) Используйте Hibernate.initialize
Hibernate.initialize(topics.getComments());
(2) Используйте JOIN FETCH
Вы можете использовать синтаксис JOIN FETCH в вашем JPQL для явного извлечения дочерней коллекции. Это как EAGER выборка.
(3) Используйте OpenSessionInViewFilter
LazyInitializationException часто возникают в слое представления. Если вы используете Spring Framework, вы можете использовать OpenSessionInViewFilter. Тем не менее, я не предлагаю вам сделать это. Это может привести к проблемам с производительностью, если не использовать правильно.
Я знаю, что это старый вопрос, но я хочу помочь. Вы можете поместить транзакционную аннотацию на нужный вам сервисный метод, в этом случае findTopicByID(id) должен иметь
@Transactional(propagation=Propagation.REQUIRED, readOnly=true, noRollbackFor=Exception.class)
больше информации об этой аннотации можно найти здесь
О других решениях:
fetch = FetchType.EAGER
не является хорошей практикой, его следует использовать ТОЛЬКО в случае необходимости.
Hibernate.initialize(topics.getComments());
Инициализатор hibernate связывает ваши классы с технологией hibernate. Если вы стремитесь быть гибкими, это не лучший способ.
Надеюсь, поможет
Происхождение вашей проблемы:
По умолчанию Hibernate лениво загружает коллекции (отношения), что означает, что когда вы используете collection
в вашем коде (здесь comments
поле в Topic
класс) Hibernate получает это из базы данных, теперь проблема в том, что вы получаете коллекцию в вашем контроллере (где сессия JPA закрыта). Это строка кода, которая вызывает исключение (где вы загружаете comments
коллекция):
Collection<Comment> commentList = topicById.getComments();
Вы получаете коллекцию "comments" (topic.getComments()) в вашем контроллере (где JPA session
закончилась) и это вызывает исключение. Кроме того, если у вас есть comments
коллекция в вашем файле JSP, как это (вместо того, чтобы получить его в вашем контроллере):
<c:forEach items="topic.comments" var="item">
//some code
</c:forEach>
У вас все равно будет то же исключение по той же причине.
Решение проблемы:
Потому что вы можете иметь только две коллекции с FetchType.Eager
(нетерпеливо извлеченная коллекция) в классе Entity, и поскольку отложенная загрузка более эффективна, чем энергичная загрузка, я думаю, что этот способ решения вашей проблемы лучше, чем просто изменение FetchType
стремиться:
Если вы хотите инициализировать коллекцию Lazy, а также сделать эту работу, лучше добавить этот фрагмент кода в ваш web.xml
:
<filter>
<filter-name>SpringOpenEntityManagerInViewFilter</filter-name>
<filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SpringOpenEntityManagerInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Этот код делает то, что он увеличит длину вашего JPA session
или, как сказано в документации, он используется "to allow for lazy loading in web views despite the original transactions already being completed."
таким образом, сессия JPA будет открыта немного дольше, и поэтому вы можете лениво загружать коллекции в ваши файлы jsp и классы контроллеров.
@Controller
@RequestMapping(value = "/topic")
@Transactional
я решаю эту проблему, добавив @Transactional
я думаю, что это может сделать сессию открытой
Как я объяснил в этой статье, лучший способ справиться с LazyInitializationException
чтобы получить его во время запроса, например:
select t
from Topic t
left join fetch t.comments
Вы должны ВСЕГДА избегать следующих анти-паттернов:
- создание ассоциаций EAGER
- используя OSIV (Open Session in View)
- включение
hibernate.enable_lazy_load_no_trans
Свойство конфигурации Hibernate
Поэтому убедитесь, что ваш FetchType.LAZY
ассоциации инициализируются во время запроса или в исходном @Transactional
использование области Hibernate.initialize
для вторичных коллекций.
Проблема вызвана доступом к атрибуту с закрытым сеансом гибернации. У вас нет спящего режима транзакции в контроллере.
Возможные решения:
Выполните всю эту логику на уровне обслуживания(с @Transactional), а не в контроллере. Там должно быть правильное место для этого, это часть логики приложения, а не контроллера (в данном случае интерфейс для загрузки модели). Все операции на уровне сервиса должны быть транзакционными. т.е.: переместите эту строку в метод TopicService.findTopicByID:
Коллекция commentList = topicById.getComments();
Используйте "нетерпеливый" вместо "ленивый". Теперь вы не используете 'ленивый'.. это не настоящее решение, если вы хотите использовать ленивый, работает как временный (очень временный) обходной путь.
- используйте @Transactional в контроллере. Это не должно использоваться здесь, вы смешиваете сервисный слой с презентацией, это не очень хороший дизайн.
- использовать OpenSessionInViewFilter, сообщается о многих недостатках, возможна нестабильность.
В общем, лучшим решением является 1.
Причина в том, что когда вы используете ленивую загрузку, сессия закрывается.
Есть два решения.
Не используйте ленивый груз.
Задавать
lazy=false
в XML или Set@OneToMany(fetch = FetchType.EAGER)
В аннотации.Используйте ленивый груз.
Задавать
lazy=true
в XML или Set@OneToMany(fetch = FetchType.LAZY)
В аннотации.и добавить
OpenSessionInViewFilter filter
в вашемweb.xml
Подробнее см. Мой пост.
Чтобы лениво загрузить коллекцию, должен быть активный сеанс. В веб-приложении есть два способа сделать это. Вы можете использовать шаблон Open Session In View, где вы используете перехватчик, чтобы открыть сеанс в начале запроса и закрыть его в конце. Существует риск того, что вам потребуется надежная обработка исключений, или вы можете связать все свои сеансы, и ваше приложение может зависнуть.
Другой способ справиться с этим - собрать все данные, которые вам нужны в вашем контроллере, закрыть сеанс, а затем вставить данные в вашу модель. Я лично предпочитаю такой подход, так как он кажется немного ближе к духу паттерна MVC. Также, если вы получаете ошибку из базы данных таким образом, вы можете справиться с ней намного лучше, чем если бы это происходило в вашем представлении представления. Ваш друг в этом сценарии - Hibernate.initialize(myTopic.getComments ()). Вам также придется заново присоединить объект к сеансу, поскольку вы создаете новую транзакцию с каждым запросом. Для этого используйте session.lock(myTopic,LockMode.NONE).
Одним из лучших решений является добавление следующего в файл application.properties:spring.jpa.properties.hibernate.enable_lazy_load_no_trans = true
Я получил эту ошибку при втором выполнении генерации токена JWT. Не в первом.
Линия
user.getUsersRole().stream().forEachOrdered((ur) -> roles.add(ur.getRoleId()));
выдал ошибку.
// MyUserDetails.java
@Service
public class MyUserDetails implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) {
// ...
/* ERROR
/* org.hibernate.LazyInitializationException: failed to
/* lazily initialize a collection of role:
/* com.organizator.backend.model.User.usersRole,
/* could not initialize proxy - no Session */
user.getUsersRole().stream().forEachOrdered((ur) ->
roles.add(ur.getRoleId()));
Добавление
@Transacctional
на уровне класса решил это:
// MyUserDetails.java
// ...
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional // <-- added
public class MyUserDetails implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) {
// ...
/* No Error */
user.getUsersRole().stream().forEachOrdered((ur) ->
roles.add(ur.getRoleId()));
Если вы пытаетесь установить связь между сущностью и коллекцией или списком Java-объектов (например, типа Long), она бы хотела что-то вроде этого:
@ElementCollection(fetch = FetchType.EAGER)
public List<Long> ids;
Две вещи, которые вам нужны fetch = FetchType.LAZY
.
@Transactional
а также
Hibernate.initialize(topicById.getComments());
@ Транзакционная аннотация на контроллере отсутствует
@Controller
@RequestMapping("/")
@Transactional
public class UserController {
}
Есть несколько решений этой проблемы с отложенной инициализацией -
1) Измените тип выборки ассоциации с LAZY на EAGER, но это не очень хорошая практика, потому что это снизит производительность.
2) Используйте FetchType.LAZY для связанного объекта, а также используйте аннотацию транзакций в методе уровня сервиса, чтобы этот сеанс оставался открытым, и когда вы вызовете topicById.getComments(), загрузится дочерний объект (комментарии).
3) Также попробуйте использовать объект DTO вместо объекта на уровне контроллера. В вашем случае сеанс закрывается на уровне контроллера. ТАК лучше преобразовать объект в DTO на уровне обслуживания.
Я узнал, что декларирование @PersistenceContext
как EXTENDED
также решает эту проблему:
@PersistenceContext(type = PersistenceContextType.EXTENDED)
Для решения проблемы в моем случае просто не хватало этой строки
<tx:annotation-driven transaction-manager="myTxManager" />
в файле контекста приложения.
@Transactional
аннотация по методу не была принята во внимание.
Надеюсь, что ответ поможет кому-то
Проблема вызвана тем, что код обращается к ленивому отношению JPA, когда "соединение" с базой данных закрыто (контекст персистентности - это правильное имя в терминах Hibernate/JPA).
Простым способом решения этой проблемы в Spring Boot является определение уровня сервиса и использование @Transactional
аннотация. Эта аннотация в методе создает транзакцию, которая распространяется на уровень репозитория и сохраняет открытый контекст сохранения до завершения метода. Если вы обращаетесь к коллекции внутри транзакционного метода, Hibernate/JPA будет извлекать данные из базы данных.
В вашем случае вам просто нужно аннотировать @Transactional
метод findTopicByID(id)
в твоем TopicService
и принудительно выполнить выборку коллекции в этом методе (например, задав ее размер):
@Transactional(readOnly = true)
public Topic findTopicById(Long id) {
Topic topic = TopicRepository.findById(id).orElse(null);
topic.getComments().size();
return topic;
}
Ваш список загружается медленно, поэтому список не был загружен. звонка для попадания в список недостаточно. используйте в Hibernate.initialize для инициации списка. Если работа не выполняется, запустите элемент списка и вызовите Hibernate.initialize для каждого. это должно быть до того, как вы вернетесь из области транзакции. посмотрите на этот пост
ищи -
Node n = // .. get the node
Hibernate.initialize(n); // initializes 'parent' similar to getParent.
Hibernate.initialize(n.getChildren()); // pass the lazy collection into the session
Это старый вопрос, но приведенная ниже информация может помочь людям, ищущим ответ на него.
Ответ @VladMihalcea полезен. Вы не должны полагаться на
FetchType.EAGER
, вместо этого вы должны загружать комментарии в объект, когда это необходимо.
Если вы явно не определяете свои запросы, чтобы вы могли указать
join fetch
, затем используя
@NamedEntityGraph
и
@EntityGraph
Вы можете переопределитьFetchType.LAZY
(
@OneToMany
ассоциации используют LAZY по умолчанию) во время выполнения и загружают комментарии одновременно с
Topic
только при необходимости . Это означает, что вы ограничиваете загрузку комментариев только теми методами (запросами), которые действительно этого требуют. Граф сущности, как это определяет JPA :
Граф сущностей можно использовать с методом find или в качестве подсказки запроса для переопределения или расширения семантики FetchType.
Вы можете использовать его на основе примера JPA здесь . В качестве альтернативы, если вы используете Spring Data JPA, вы можете использовать его на основе примера, предоставленного Spring.
Это была проблема, с которой я недавно столкнулся и которую решил с помощью
<f:attribute name="collectionType" value="java.util.ArrayList" />
Более подробное описание здесь и спасло мне этот день.
Чтобы избавиться от исключения ленивой инициализации, вы не должны вызывать ленивую коллекцию при работе с отсоединенным объектом.
На мой взгляд, лучший подход - использовать DTO, а не entity. В этом случае вы можете явно указать поля, которые хотите использовать. Как обычно, этого достаточно. Не нужно беспокоиться, что что-то вроде ДжексонаObjectMapper
, или hashCode
сгенерированный Lombok вызовет ваши методы неявно.
Для некоторых конкретных случаев вы можете использовать @EntityGrpaph
аннотации, которые позволяют сделать eager
загрузить, даже если у вас есть fetchType=lazy
в вашем объекте.
Используя спящий режим @Transactional
аннотация, если вы получаете объект из базы данных с ленивыми извлеченными атрибутами, вы можете просто получить их, извлекая эти атрибуты следующим образом:
@Transactional
public void checkTicketSalePresence(UUID ticketUuid, UUID saleUuid) {
Optional<Ticket> savedTicketOpt = ticketRepository.findById(ticketUuid);
savedTicketOpt.ifPresent(ticket -> {
Optional<Sale> saleOpt = ticket.getSales().stream().filter(sale -> sale.getUuid() == saleUuid).findFirst();
assertThat(saleOpt).isPresent();
});
}
Здесь, в транзакции, управляемой прокси Hibernate, факт вызова ticket.getSales()
выполните другой запрос, чтобы получить данные о продажах, потому что вы явно задали этот вопрос
Не лучшее решение, но для тех, кто сталкивается LazyInitializationException
особенно на Serialization
это поможет. Здесь вы проверите лениво инициализированные свойства и настройкуnull
к тем. Для этого создайте следующий класс
public class RepositoryUtil {
public static final boolean isCollectionInitialized(Collection<?> collection) {
if (collection instanceof PersistentCollection)
return ((PersistentCollection) collection).wasInitialized();
else
return true;
}
}
Внутри вашего класса Entity, у которого есть лениво инициализированные свойства, добавьте метод, как показано ниже. Добавьте в этот метод все ваши свойства ленивой загрузки.
public void checkLazyIntialzation() {
if (!RepositoryUtil.isCollectionInitialized(yourlazyproperty)) {
yourlazyproperty= null;
}
Назовите это checkLazyIntialzation()
после всех мест, куда вы загружаете данные.
YourEntity obj= entityManager.find(YourEntity.class,1L);
obj.checkLazyIntialzation();
В моем случае следующий код был проблемой:
entityManager.detach(topicById);
topicById.getComments() // exception thrown
Потому что он отсоединился от базы данных, и Hibernate больше не извлекал список из поля, когда это было необходимо. Поэтому я инициализирую его перед отсоединением:
Hibernate.initialize(topicById.getComments());
entityManager.detach(topicById);
topicById.getComments() // works like a charm
Для тех, кто работает с критериями, я обнаружил, что
criteria.setFetchMode("lazily_fetched_member", FetchMode.EAGER);
сделал все, что мне было нужно.
Режим начальной выборки для коллекций установлен на FetchMode.LAZY, чтобы обеспечить производительность, но когда мне нужны данные, я просто добавляю эту строку и наслаждаюсь полностью заполненными объектами.
В моем cae у меня было отображение ч / б A и B как
А имеет
@OneToMany(mappedBy = "a", cascade = CascadeType.ALL)
Set<B> bs;
на уровне DAO метод должен быть аннотирован @Transactional
если вы не аннотировали отображение с помощью Fetch Type - Eager
Коллекция comments
в вашем модельном классе Topic
загружается лениво, что является поведением по умолчанию, если вы не комментируете его fetch = FetchType.EAGER
в частности.
Скорее всего, ваш findTopicByID
служба использует сеанс Hibernate без сохранения состояния. Сеанс без сохранения состояния не имеет кеша первого уровня, т. Е. Отсутствует контекст постоянства. Позже, когда вы попытаетесь повторить comments
, Hibernate бросит исключение.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: mvc3.model.Topic.comments, no session or session was closed
Решение может быть:
аннотировать
comments
сfetch = FetchType.EAGER
@OneToMany(fetch = FetchType.EAGER, mappedBy = "topic", cascade = CascadeType.ALL) private Collection<Comment> comments = new LinkedHashSet<Comment>();
Если вы все еще хотите, чтобы комментарии загружались лениво, используйте сеансы с сохранением состояния в Hibernate, чтобы вы могли получать комментарии позже по требованию.
Причина в том, что вы пытаетесь получить commentList на своем контроллере после закрытия сеанса внутри службы.
topicById.getComments();
Выше будет загружать commentList, только если ваш сеанс гибернации активен, который, я думаю, вы закрыли в своем сервисе.
Таким образом, вы должны получить commentList перед закрытием сессии.
Еще один способ сделать это, вы можете использовать TransactionTemplate, чтобы обернуть ленивую выборку. подобно
Collection<Comment> commentList = this.transactionTemplate.execute
(status -> topicById.getComments());