Blog-Archiv

Sonntag, 23. Februar 2020

Removing a Still Referenced JPA Entity

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: