JPA Doesn't Call Persist But Merge Operation On New Entities
Author
Marcus HeldWhen working with JPA I prefer generating the primary key on the application and not in the database (check out this post from James Brundege
). Additionally, I also prefer optimistic locking and in order to use it you need to specify a @Version
field in your Entity. But you need to be careful how to initialize these two fields. In this post I’ll talk about an error when you assign a default value for both fields in JPA which leads the EntityManager
to never call persist
but the merge
operation instead when creating a new entity.
Let’s start with a simple definition of two entities with a one-to-many relationship:
@Entity
class ExampleEntity(
@Id var id: UUID = UUID.randomUUID(),
@Version var version: Long = 0L,
@OneToMany var composite: List<Composite>
)
@Entity
class Composite(
@Id var id: UUID = UUID.randomUUID(),
@Version var version: Long = 0L
)
When trying to save a ExampleEntity
with repository.save(ExampleEntity(composite = listOf(Composite(), Composite())))
we get a EntityNotFoundException
. This is because in a single transaction we want to persist a ExampleEntity
and all the Composite
objects. But in our definition we didn’t instruct JPA to cascade the persist operation. So - we can achieve that by adding it to the one-to-many relationship: @OneToMany(cascade = [CascadeType.PERSIST])
. Save it. Run it. And … It still crashes!?
And here comes the behavior I wasn’t aware of. After looking into the SQL that got executed I noticed that Hibernate executed a select
on the composite table. So for some reasons Hibernate expected that the composite entity already exists instead of creating it.
The reason why Hibernate is doing that is because as soon as an Entity has a non-null primary key, and a non-null version JPA defines the Entity as existent and fetches it from the database.
So the lesson is easy. Don’t - ever - initialize, touch or modify the version field! A look into the JPA specification (Chapter 3.4.2) also clearly states: “An entity may access the state of its version field or property or export a method for use by the application to access the version, but must not modify the version value”
So, this is the correct implementation:
@Entity
class ExampleEntity(
@Id var id: UUID = UUID.randomUUID(),
@Version var version: Long? = null,
@OneToMany(cascade = [CascadeType.PERSIST]) var composite: List<Composite>
)
@Entity
class Composite(
@Id var id: UUID = UUID.randomUUID(),
@Version var version: Long? = null
)
You learn something new every day ;-)