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)
}
}
}
}
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).
CodePudding user response:
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:
- I don't think you need to add
@RegisterForReflection
. That's only helpful in specific circumstances and this shouldn't be one of them. - Many-to-one associations are usually initialized with null. That's because you can check if the association exists using a null check. I suspect it may also cause errors because you are associating a new unmanaged object.