Issue
I have three entities: Interview, Question and Answer. Interview can have many Questions (and vice versa, many-to-many), Question can have many Answers (one-to-many).
I persist and fetch entities in the following manner:
@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)
}
}
// same repos for Question and Answer
All persistence operations work fine, interviews and questions are created fine and then both fetched fine as well. But when I create Answer (also fine) and then try to findById Question or Interview entities I get the following error (this one for getting 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"
It used to show the same error for findById (interview), but FetchMode.JOIN solved the issue. For some reason FetchMode.JOIN is ignored for fetching answer (?). Instead of using findById I also tried manually writing HQL using left join fetch, but got the same result. What am I missing here?
Interview Entity:
@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:
@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:
@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(),
)
Solution
In Hibernate Reactive, lazy collections must be fetched explicitly. If yo you try to access a lazy association without fetching it first, you will see that error.
Here's how you can fetch an association with Panache:
import org.hibernate.reactive.mutiny.Mutiny;
fun getInterview(interviewId: Long): Uni<Interview> {
return findById(interviewId)
.call(interview -> Mutiny.fetch(interview.questions))
}
Note that fetching associations this way will cause an extra query to the database. Depending on the use case, it might be more efficient to load the association eagerly with a join fetch in a query. This should also work:
fun getInterview(interviewId: Long): Uni<Interview> {
return find("from Interview i left join fetch i.questions where i.id = :id", Parameters.with("id", interviewId))
}
In this case, Hibernate Reactive will load everything with a single query.
Also, when using Hibernate, the developer is responsible to keep bidirectional associations in sync on both sides.
Answered By - Davide D'Alto
Answer Checked By - Clifford M. (JavaFixing Volunteer)