Quarkus(Panache)中Hibernate Reactive的LazyInitializationException问题
背景与问题
我有三个实体:Interview、Question和Answer。Interview与Question为多对多关系,Question与Answer为一对多关系。
所有持久化操作正常,Interview和Question的创建与查询也正常。但创建Answer后,调用findById获取Question或Interview时出现如下错误(此为获取Question时的错误):
org.hibernate.HibernateException: java.util.concurrent.CompletionException: org.hibernate.LazyInitializationException: HR000056: Collection cannot be initialized: com.my.company.question.Question.answers - Fetch the collection using 'Mutiny.fetch', 'Stage.fetch', or 'fetch join' in HQL
此前获取Interview时也出现相同错误,但通过FetchMode.JOIN解决了问题。但FetchMode.JOIN对Question的answers集合无效,我尝试手动编写带left join fetch的HQL,结果仍相同。请问我哪里操作有误?
仓库实现代码
@ApplicationScoped internal class InterviewRepository : PanacheRepository<Interview> { fun persistInterview(interview: Interview): Uni<Interview> { return Panache.withTransaction { persist(interview) } } fun getInterview(interviewId: Long): Uni<Interview> { return findById(interviewId) } } // Question和Answer的仓库实现类似
实体代码
Interview实体
@Entity(name = "interview") @RegisterForReflection internal data class Interview ( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") val interviewId: Long? = null, @Column(name = "client_id", nullable = false) val clientId: Long = Long.MIN_VALUE, @ManyToMany(mappedBy = "interviews", fetch = FetchType.LAZY) @Fetch(FetchMode.JOIN) val questions: MutableSet<Question> = mutableSetOf(), )
Question实体
@Entity(name = "question") @RegisterForReflection internal data class Question ( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") val questionId: Long? = null, @Column(name = "client_id", nullable = true) val clientId: Long? = null, @OneToMany(mappedBy = "question", fetch = FetchType.LAZY) @OnDelete(action = CASCADE) @Fetch(FetchMode.JOIN) val answers: MutableSet<Answer> = mutableSetOf(), @ManyToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY) @JoinTable( name = INTERVIEW_QUESTION_TABLE, joinColumns = [JoinColumn(name = "question_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "interview_id", referencedColumnName = "id")] ) @Fetch(FetchMode.JOIN) @JsonIgnore val interviews: MutableList<Interview> = mutableListOf(), )
Answer实体
@Entity(name = "answer") @RegisterForReflection internal data class Answer ( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") val answerId: Long? = null, @Column(name = "question_id", nullable = false) val questionId: Long = Long.MIN_VALUE, @ManyToOne(fetch = FetchType.LAZY) @Fetch(FetchMode.JOIN) @JoinColumn( name="question_id", foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT), nullable = false, insertable = false, updatable = false, ) @JsonIgnore val question: Question = Question(), )
问题分析与解决办法
1. FetchMode.JOIN在findById中的局限性
Panache的findById是单实体查询,Hibernate的@Fetch(FetchMode.JOIN)虽然能处理多对多关联,但对于一对多集合(比如Question.answers),单实体加载时不会自动触发JOIN查询——这个注解主要在JPQL/HQL或批量加载场景生效,findById会直接加载主实体,忽略集合的JOIN配置。
2. 修正自定义HQL查询
如果用HQL的LEFT JOIN FETCH,必须通过Panache的find方法执行,不能用findById,示例代码:
@ApplicationScoped internal class QuestionRepository : PanacheRepository<Question> { fun getQuestionWithAnswers(questionId: Long): Uni<Question> { return find("SELECT q FROM question q LEFT JOIN FETCH q.answers WHERE q.questionId = ?1", questionId) .singleResult() } }
确保HQL中明确指定LEFT JOIN FETCH q.answers,并且通过参数传递ID,这样查询时会一次性加载Question和关联的Answers。
3. 实体映射的潜在问题
- Answer的关联维护:创建Answer时,不要只设置
questionId字段,要同时设置question关联属性(比如answer.question = existingQuestion),否则Hibernate无法正确维护关联关系,后续加载集合会出现异常。 - Kotlin数据类的冲突:Hibernate懒加载依赖代理类,而Kotlin data class自动生成的
equals/hashCode方法可能干扰代理逻辑。可以尝试把Question、Answer改为普通class(去掉data关键字),保留@RegisterForReflection注解。
4. 显式加载集合(兼容findById)
如果必须使用findById,可以在获取实体后,在会话/事务范围内用Mutiny.fetch显式加载集合:
fun getQuestionWithAnswers(questionId: Long): Uni<Question> { return Panache.withTransaction { findById(questionId) .onItem().invoke { question -> Mutiny.fetch(question.answers) } } }
这种方式会在事务内触发集合加载,避免会话关闭后访问懒加载集合的异常。
5. 检查会话/事务范围
访问懒加载集合时,必须处于Hibernate会话或Quarkus事务范围内。如果查询实体后立即返回,再在外部访问answers集合,会话已经关闭,必然抛出异常。要确保集合访问逻辑在Panache.withTransaction块内,或者在查询时就加载好集合。
内容的提问来源于stack exchange,提问作者E. Dn

