如何解决JPA-Hibernate中嵌套集合的LazyInitializationException问题?
必要背景信息
作为Spring学习项目,我开发了一个用于存储题目、生成随机测验的小工具。数据结构层级为:科目包含多个主题,主题包含多个题目,题目包含多个答案。
问题说明
反复遇到LazyInitializationException异常。
已尝试方案
- 将几乎所有集合类型改为
Set - 曾考虑开启
enable_lazy_load_no_trans属性,但知道这是需要避免的反模式,所以未采用
实体类定义(仅展示字段)
Subject:
@Entity @Table(name = Resources.TABLE_SUBJECTS) public class Subject implements DomainObject { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = Resources.ID_SUBJECT) private int subjectId; @Column(name="subject_name", nullable = false) private String name; @OneToMany( mappedBy = Resources.ENTITY_SUBJECT, fetch = FetchType.EAGER ) private Set<Topic> topics; }
Topic:
@Entity @Table(name = Resources.TABLE_TOPICS) public class Topic implements DomainObject { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "topic_id") private int topicId; @Column(name = "name") private String name; @OneToMany( mappedBy = Resources.ENTITY_TOPIC, orphanRemoval = true, cascade = CascadeType.MERGE, fetch = FetchType.EAGER ) private Set<Question> questions; @ManyToOne( fetch = FetchType.LAZY ) private Subject subject; }
Question:
@Entity @Table(name = Resources.TABLE_QUESTIONS) public class Question implements DomainObject { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = Resources.ID_QUESTION) private int questionId; @Column(name = "statement") private String statement; @OneToMany( mappedBy = Resources.ENTITY_QUESTION, orphanRemoval = true, cascade = CascadeType.MERGE ) private Set<Answer> answers; @ManyToOne( fetch = FetchType.LAZY ) private Topic topic; }
Answer:
@Entity @Table(name = Resources.TABLE_ANSWERS) public class Answer implements DomainObject { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = Resources.ID_ANSWER) private int answerId; @Column(name = "answer_text", nullable = false) private String text; @Column(name = "is_correct", nullable = false) private Boolean isCorrect; @ManyToOne( fetch = FetchType.LAZY ) private Question question; }
数据访问层实现
我通过继承JpaRepository实现CRUD操作,尝试了以下自定义查询但无效:
public interface SubjectRepository extends JpaRepository<Subject, Integer> { @Query Optional<Subject> findByName(String name); @Query(value = "SELECT DISTINCT s FROM Subject s " + "LEFT JOIN FETCH s.topics AS t " + "JOIN FETCH t.questions AS q " + "JOIN FETCH q.answers as a") List<Subject> getSubjects(); }
异常栈信息
Caused by: org.hibernate.LazyInitializationException: could not initialize proxy [org.callisto.quizmaker.domain.Subject#1] - no Session at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final] at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final] at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final] at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final] at org.callisto.quizmaker.domain.Subject$HibernateProxy$B8rwBfBD.getTopics(Unknown Source) ~[main/:na] at org.callisto.quizmaker.service.QuizMakerService.activeSubjectHasTopics(QuizMakerService.java:122) ~[main/:na] at org.callisto.quizmaker.QuizMaker.checkIfActiveSubjectHasTopics(QuizMaker.java:307) ~[main/:na] at org.callisto.quizmaker.QuizMaker.createNewQuestion(QuizMaker.java:117) ~[main/:na] at org.callisto.quizmaker.QuizMaker.prepareMainMenu(QuizMaker.java:88) ~[main/:na] at org.callisto.quizmaker.QuizMaker.run(QuizMaker.java:65) ~[main/:na] at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:769) ~[spring-boot-2.6.3.jar:2.6.3]
触发场景
调用以下代码时触发异常:
boolean output = service.activeSubjectHasTopics();
对应的Service方法:
public boolean activeSubjectHasTopics() { if (activeSubject == null) { throw new NullPointerException(Resources.EXCEPTION_SUBJECT_NULL); } return !activeSubject.getTopics().isEmpty(); }
调用上下文代码:
private void createNewQuestion(View view, QuizMakerService service) { int subjectId = chooseOrAddSubject(view, service); service.setActiveSubject(subjectId); if (checkIfActiveSubjectHasTopics(view, service)) { chooseOrAddTopic(view, service, subjectId); } do { createQuestion(view, service); createAnswers(view, service); } while(view.askToCreateAnotherQuestion()); service.saveDataToFile(); prepareMainMenu(view, service); } private boolean checkIfActiveSubjectHasTopics(View view, QuizMakerService service) { boolean output = service.activeSubjectHasTopics(); if (!output) { view.printNoTopicsWarning(service.getActiveSubjectName()); String topicName = readTopicName(view); createNewTopic(service, topicName); } return output; }
解决方案
1. 修复实体加载逻辑
异常核心原因是activeSubject是Hibernate代理对象,获取它时的Session已关闭,后续调用getTopics()无法初始化代理。
可以在Repository中新增带关联加载的查询方法:
@Query("SELECT s FROM Subject s LEFT JOIN FETCH s.topics WHERE s.subjectId = :id") Optional<Subject> findByIdWithTopics(@Param("id") Integer id);
然后在setActiveSubject方法中调用该方法获取Subject,确保topics集合已被主动加载,避免延迟加载触发异常。
2. 调整事务边界
将需要访问实体关联集合的业务流程包裹在事务中,比如在setActiveSubject或activeSubjectHasTopics方法上添加@Transactional注解,确保从获取实体到访问关联数据的整个过程处于同一个Session内。
如果是多步骤的业务逻辑(比如createNewQuestion整个流程),也可以在对应的Service方法上添加@Transactional,覆盖完整的业务操作周期。
3. 避免长期持有代理对象
不要将Hibernate代理对象长期保存在Service的成员变量中(如activeSubject),Session关闭后代理将无法初始化。建议每次需要访问时从Repository重新获取,或者确保获取时已加载所有需要的关联数据。
内容的提问来源于stack exchange,提问作者Emiliano De Santis

