This is a quick summary of the issues around EntityRepository.save() that seem to exist since 1.4.2.

I'll create test cases and JIRA issues as time permits, but I think these notes may be helpful at this stage to find out whether or not incompatibilities experienced by other users have the same root cause.

According to Javadoc [1], this is what save() does:

"Persist (new entity) or merge the given entity. The distinction on calling either method is done based on the primary key field being null or not. If this results in wrong behavior for a specific case, consider using the EntityManagerDelegate which offers both persist and merge."

As far as I can see, the description accurately describes the behaviour in 1.4.1.

The behaviour was changed in 1.4.2 in an incompatible way without adapting the documentation. Since 1.4.2, if the primary key is not null, DeltaSpike also runs a database query to check whether an entity with the given key exists. If so, it calls merge(), otherwise persist().

So there are now quite a few cases when save() calls persist() where it would have called merge() in 1.4.1.

Some use cases:

Use case 1:

// assume separate transactions.
foo = save(foo);
remove(foo);
foo = save(foo);

Result in 1.4.1:
foo is persistent. The second save() is a merge().

Result in 1.4.2:
Exception. The second save() is a persist(), since the key no longer exists. But then Hibernate complains it cannot persist a detached entity.

Use case 2:

Assume a one-to-many association Blog -> Comment, both with auto-generated identities of type long.

public Blog createBlog(List<Comment> comments) {
    Blog = new Blog();
    blog.setComments(comments);
    blog = blogRepository.save(blog);
    blog.getComments().add(new Comment());
}

Now someone calls createBlog(Arrays.asList(c1, c2)).

In 1.4.1 this call succeeds. In 1.4.2, there is an UnsupportedOperationException since blog.getComments() is immutable.

In 1.4.1, the identity member is 0. 0 is not null, so save() performs a merge(). merge() creates a new blog instance with a new mutable comments list (at least with Hibernate (PersistentBag)).

In 1.4.2, there is no existing blog entity with identity 0, so save() calls persist() and returns the original blog instance. Its comments member is the result of Arrays.asList() which is immutable.

Sometimes it is useful to question the things you think you know, like "an entity with an identity value of 0 or null cannot be persistent". I searched the JPA 2.1 spec and did not find a word about zero identities. In fact, Eclipselink has an option to *enable* zero identities (eclipselink.allow-zero-id).

The exception from Hibernate is also misleading. Just because an entity passed to persist() has a non-null and non-zero key, it does not have to be detached, it may well be a new transient entity.

I never really liked the fact that save() blurs the distinction between persist() and merge(), and by its very name it encourages users to always call save() after changing an entity state which is usually unnecessary and sometimes even wrong - so far, I've seen each and every new user of DeltaSpike Data doing that.

At the moment, I don't see a way to specify and implement save() in a way that is logically consistent *and* portable across JPA providers. (I'll be happy to be proved false.)

So for the next release we should do one of the following:

1) Adapt Javadoc to the current behaviour and document the incompatibilities.

2) Leave Javadoc unchanged and restore compatibility.

3) Specify a new expected behaviour and adapt both Javadoc and implementation.

4) Add a big fat warning to save() Javadoc and move persist() and merge() from EntityManagerDelegate to EntityRepository.

[1] http://deltaspike.apache.org/javadoc/1.4.2/org/apache/deltaspike/data/api/EntityRepository.html#save-E-

Regards,
Harald

Reply via email to