3

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(),
)
E. Dn
  • 940
  • 1
  • 9
  • 21

1 Answers1

5

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.

Davide D'Alto
  • 7,421
  • 2
  • 16
  • 30
  • I tried both options you've suggested, but it's getting the same error as soon as Interview has got some related Questions (i.e. before I insert a question it works fine, but after insertion and fetch again - throws the error above). I created a demo on my GitHub, please take a look if possible (I've put a readme with how-to reproduce): https://github.com/DnEgorWeb/ogram-interviews – E. Dn Nov 08 '22 at 15:23
  • Thanks for the reproducer. That's really helpful. But can you create a test case that I can run, please? I'm not familiar with postman and it would take some time for me to figure out how to reproduce the error. A sequence a curl requests to the rest API would be good as well. – Davide D'Alto Nov 08 '22 at 15:32
  • Thank you for checking it out! I've just updated the repo with created test case. – E. Dn Nov 08 '22 at 17:22
  • Thank, I gave it a look and I think it might be a bug. I need to create a simpler test case (one that only uses Hibernate Reactive) to make sure of it. – Davide D'Alto Nov 08 '22 at 21:54
  • OK, I've forked a project and made some changes. Now it seems to work: https://github.com/DavideD/ogram-interviews/commits/master – Davide D'Alto Nov 09 '22 at 21:29
  • I've sent a pr to your repository with the fixes that should make everything work. Ultimately, I think the answer to your question was correct, but there are some other things to fix in the project – Davide D'Alto Nov 09 '22 at 21:38
  • Hello Davide, I can't thank you enough for making the fixes and comments. I will definitely check out the links you've provided on the matter. For some reason now when persisting an answer it fails with the error (tests are fine though): Cannot add or update a child row: a foreign key constraint fails (`interviews`.`answer`, CONSTRAINT `FK8frr4bcabmmeyyu60qt7iiblo` FOREIGN KEY (`question_id`) REFERENCES `question` (`id`) ON DELETE CASCADE)". I'm trying to figure out why – E. Dn Nov 10 '22 at 08:23
  • 1
    I'm not sure about this but It usually depends on the order you are deleting entities. I would try to remove the related association before deleting the entity involved. It's probably better to ask these kind of questions on the Hibernate user topic on Zulip: https://hibernate.zulipchat.com/#narrow/stream/132096-hibernate-user – Davide D'Alto Nov 10 '22 at 09:20
  • I've tried different ways, but still have this issue with fetching (one-to-many case, many-to-many works fine). I've created much smaller and simpler demo, if you still have time (and patience...) please have a look: https://stackoverflow.com/questions/74391462/how-to-fetch-lazy-associations-in-hibernate-reactive – E. Dn Nov 10 '22 at 15:27
  • Mmmh... I've started to have a look at it and I don't see anything wrong. I don't know why you have the lazy initialization error. I can't see any mistake. I will have to create a project without kotlin and check if the same error happens again. – Davide D'Alto Nov 10 '22 at 17:48
  • If I were you, I would create an issue at https://github.com/quarkusio/quarkus/issues - I don't think SO is suited for these kind of issues – Davide D'Alto Nov 10 '22 at 18:47