kotlinjpaquarkusquarkus-panachehibernate-reactive

How to fetch lazy associations in Hibernate reactive?


I have two entities: Book and Page. Book can have many pages.

@Entity(name = "book")
@RegisterForReflection
internal data class Book (
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    val bookId: Long? = null,

    @OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
    @Fetch(FetchMode.JOIN)
    val pages: MutableSet<Page> = mutableSetOf(),
)
@Entity(name = "page")
@RegisterForReflection
internal data class Page(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    val pageId: Long? = null,

    @ManyToOne(fetch = FetchType.LAZY)
    @Fetch(FetchMode.JOIN)
    @JsonIgnore
    var book: Book? = Book(),
)

Both entities are persisted and created in database fine. But if I try to fetch Book, I get LazyInitializationException. Although I've tried fetching it with Mutiny.fetch or using LEFT JOIN FETCH it always throws the same error. Below the way I persist/fetch:

@ApplicationScoped
internal class BookRepo : PanacheRepository<Book> {
    fun getBook(bookId: Long): Uni<Book> {
        return findById(bookId)
            .call { it -> Mutiny.fetch(it.pages) }
            .map {
                it ?: throw RuntimeException("Not Found")
            }
    }

    fun persistBook(book: Book): Uni<Book> {
        return Panache.withTransaction {
            persist(book)
        }
    }
}

@ApplicationScoped
internal class PageRepo : PanacheRepository<Page> {
    fun persistPage(page: Page, bookId: Long): Uni<Page> {
        return Panache.withTransaction {
            getSession().chain { s ->
                val book = s.getReference(Book::class.java, bookId)
                page.book = book
                persist(page)
            }
        }
    }
}

enter image description here

What am I doing wrong? I've created a repo with this small example, there are sequence of CURL requests to reproduce the error (please check the readme file).


Solution

  • The problem is that the default equals and hashcode for the entities include the associations.

    So, when you run the first select query, if they are called before you've fetched the association, it will cause the error.

    You need to change your entities to something like:

    @Entity(name = "page")
    internal data class Page(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        val pageId: Long? = null,
    
        @ManyToOne(fetch = FetchType.LAZY)
        @Fetch(FetchMode.JOIN)
        @JsonIgnore
        var book: Book? = null,
    ){
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false
    
            other as Page
    
            if (pageId != other.pageId) return false
    
            return true
        }
    
        override fun hashCode(): Int {
            return pageId?.hashCode() ?: 0
        }
    }
    
    package org.acme
    
    import org.hibernate.annotations.Fetch
    import org.hibernate.annotations.FetchMode
    import javax.persistence.*
    
    @Entity(name = "book")
    internal data class Book (
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        val bookId: Long? = null,
    
        @OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
        @Fetch(FetchMode.JOIN)
        var pages: MutableSet<Page> = mutableSetOf(),
    ) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false
    
            other as Book
    
            if (bookId != other.bookId) return false
    
            return true
        }
    
        override fun hashCode(): Int {
            return bookId?.hashCode() ?: 0
        }
    }
    

    Normally, you shouldn't include the identifier, but there are no other fields in this simple example. The Hibernate ORM documentation has a paragraph about how to implement hashcode and equals for entities that should give you a better explanation.

    Another couple of things I've noticed: