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.
Scenario
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!
Test Basis
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.
Source Code
Here is the test implementing the scenario:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import static org.junit.Assert.*; import java.util.*; import org.junit.Test; public class JpaRemoveTest extends MultiJpaDatabaseTest { private static final List<Class<?>> managedClasses = new ArrayList<>(); static { managedClasses.add(City.class); managedClasses.add(House.class); // This order is needed! // Hibernate would not delete a House that is still // referenced by a city through its @OneToMany Collection! } @Override protected List<Class<?>> getManagedClasses() { return managedClasses; } /** This works in Hibernate, but not in EclipseLink. */ @Test public void removingAStillReferencedHouseShouldNotWork() { executeForAll("removingAStillReferencedHouseShouldNotWork", (entityManager) -> { // build test data final City washington = newCity("Washington", new String[] { "Pentagon", "White House" }); JpaUtil.transactional(entityManager, entityManager::persist, washington); // perform test action final House pentagon = washington.getHouses().iterator().next(); JpaUtil.transactional(entityManager, entityManager::remove, pentagon); // assert result assertEquals(2, washington.getHouses().size()); final List<House> persistentHouses = JpaUtil.findAll(House.class, entityManager); assertEquals(2, persistentHouses.size()); }); } } |
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.
Result
EclipseLink
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.
Hibernate
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.
Conclusion
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.
- What Hibernate does, ignoring the developer's intent, is not OK.
- What EclipseLink does, leaving the in-memory graph out-of-sync with the database, is not OK either.
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.
Keine Kommentare:
Kommentar veröffentlichen