Another JPA follower article, this time about a difference between the JPA providers Hibernate (5.4.4) and EclipseLink (2.7.5) concerning the removal of a still referenced entity.
I will use the one-to-many relation of my recent City
- House
example. I'm going to call entityManager.remove()
on a House
that is still referenced in the @OneToMany
collection of its City
. I will not remove the entity from that collection before.
Orphan-removal would be to remove the House
from the collection in City
, and then commit the transaction. But this scenario is the opposite!
I am going to perform the scenario on both Hibernate and EclipseLink JPA providers. Additionally, to exclude database interference, I will also use two different databases. To run it in just one take I can use my recently implemented AbstractJpaTest
.
You can find the JpaUtil
class in my recent article about persist and merge.
Here is the test implementing the scenario:
1 | import static org.junit.Assert.*; |
By extending MultiJpaDatabaseTest
the test will run on both JPA providers, combined with both databases (postgres and H2), so it will be executed 4 times.
The test dynamically declares its persistence classes on line 9. The super-class will take care that no entities of given managed classes are in database when a test execution starts.
On line 26, I create a new City "Washington" with two new houses, "Pentagon" and "White House". The whole graph gets saved on line 27 through cascading persist in a transaction. All the entities are in persistent state now.
On line 30, I fetch the "Pentagon" house from the graph, and remove it in a transaction, without removing it from the "Washington" collection.
This test assumes that it should not be possible to remove a still referenced entity from persistence. Thus it asserts that the persistent list of houses, read in line 35, is of same size as the one in memory, done in line 36.
A Junit assertion error happens on line 36:
java.lang.AssertionError: expected:<2> but was:<1>, on jdbc:h2:tcp://localhost/~/test with org.eclipse.persistence.internal.jpa.EntityManagerImpl
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:647)
at org.junit.Assert.assertEquals(Assert.java:633)
at fri.jpa.configuration.JpaRemoveTest.lambda$7(JpaRemoveTest.java:36)
at fri.jpa.configuration.AbstractJpaTest.executeForAll(AbstractJpaTest.java:99)
at fri.jpa.configuration.JpaRemoveTest.removingAStillReferencedHouseShouldNotWork(JpaRemoveTest.java:24)
....
Suppressed: java.lang.AssertionError: expected:<2> but was:<1>, on jdbc:postgresql://localhost/template1 with org.eclipse.persistence.internal.jpa.EntityManagerImpl
There are two houses in memory, but just one house in persistence.
This means that EclipseLink allows to remove a still referenced entity. It does not throw an exception on entityManager.remove()
, and it leaves the list of houses in "Washington" city inconsistently with what is in database.
Following is the logging output of the Hibernate run with postgres database:
==========================================
Executing removingAStillReferencedHouseShouldNotWork on jdbc:postgresql://localhost/template1 with org.hibernate.internal.SessionImpl
------------------------------------------
Hibernate: select nextval ('hibernate_sequence')
Hibernate: select nextval ('hibernate_sequence')
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into City (name, id) values (?, ?)
Hibernate: insert into House (city_id, name, id) values (?, ?, ?)
Hibernate: insert into House (city_id, name, id) values (?, ?, ?)
Hibernate: select house0_.id as id1_1_, house0_.city_id as city_id3_1_, house0_.name as name2_1_ from House house0_
------------------------------------------
Finished removingAStillReferencedHouseShouldNotWork on jdbc:postgresql://localhost/template1 with org.hibernate.internal.SessionImpl
==========================================
The test succeeded. Hibernate did not throw an exception, and, if you check the log, you see that no SQL DELETE statement was launched. That means Hibernate did not remove the "Pentagon" entity in persistence.
Was the entityManager.remove()
call silently ignored, or is the "Pentagon" house detached now? A subsequent entityManager.persist()
call on the "Washington" city would throw an exception if it was detached. But it does not, I tried it out. Hibernate simply ignores the entityManager.remove()
call.
I would consider both JPA providers to be wrong. I would expect JPA to not allow the persistent removal of a still referenced entity, and entityManager.remove()
to throw an exception in such a scenario.
Such things won't change quickly. It won't be easy to change the JPA provider of your application. Best is to have a 100 % unit test coverage of your DAO layer before you try it.
ɔ⃝ Fritz Ritzberger, 2020-02-23