Loading...

JPA Doesn't Call Persist But Merge Operation On New Entities

JPA
January 7, 2021
3 minutes to read
Share this post:

When 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 ;-)

Have you heard of Marcus' Backend Newsletter?

New ideas. Twice a week!
Top