Blog-Archiv

Dienstag, 4. Februar 2020

Difference between JPA Persist and Merge

The two JPA methods persist() and merge() can both persist and update objects. So what is the actual difference between these two?

In one sentence:

  • persist() works directly on its parameter, while merge() persists a clone of its parameter and returns that clone.

Here is a more specific description:

merge()
deep-copies its parameter object, saves that clone with all related cascaded objects, or updates when already persistent, and finally returns the clone, which is a persistent object.
merge() does not write an ID into the original object in case ID is a @GeneratedValue, although the returned clone has an ID. Further the original object is not a persistent object after the merge() call.
merge() can save also detached objects.
persist()
writes an ID into its parameter object in case ID is a @GeneratedValue. That original object is a persistent object after the persist() call. Related cascaded objects will be persisted, too.
persist() does not return or clone anything. If it finds a non-persistent and non-cascaded object in relations, it throws an exception.
persist() fails when receiving a detached object.

Reality is even more complex. I used JPA 2.2. It looks like that Hibernate 5.4.4 and EclipseLink 2.7.5 differ in their behaviour concerning merge().

Example Entities

The story is:

Repairs should be recorded. A vehicle repair is done by a workshop. Optionally an agent has mediated the repair. When a vehicle is deleted, all its repairs will be deleted too (UML composition), but not the shops and agents that performed the repairs.

So we have required relations between repair and vehicle, and repair and workshop, and an optional relation between repair and agent. In Java, the vehicle holds a collection of repairs, but there is no such collection in database. The backlink from repair to vehicle is present in both Java and database.

Here are the JPA implementations of the entities (click to expand source code):

Workshop.java
import javax.persistence.*;

@Entity
public class Workshop
{
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    public Long getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
Vehicle.java (Please refer to my recent article for source code of BacklinkSettingSet.java)
import java.util.*;
import javax.persistence.*;
import fri.jpa.util.BacklinkSettingSet;

@Entity
public class Vehicle
{
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    /** The houses of this city. */
    @OneToMany(mappedBy = "vehicle", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Repair> repairs = new HashSet<>();
    
    public Long getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public Set<Repair> getRepairs() {
        return new BacklinkSettingSet<Repair,Vehicle>(
                repairs,
                this,
                (element, owner) -> element.setVehicle(owner));
    }
}
Repair.java
import javax.persistence.*;

@Entity
public class Repair
{
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne  // optional = true by default
    private Agent agent;
    
    @ManyToOne(optional = false)
    private Workshop workshop;
    
    @ManyToOne(optional = false)
    private Vehicle vehicle;
    
    public Long getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public Agent getAgent() {
        return agent;
    }
    /** Public because no backlink exists. */
    public void setAgent(Agent agent) {
        this.agent = agent;
    }
    
    public Workshop getWorkshop() {
        return workshop;
    }
    /** Public because no backlink exists. */
    public void setWorkshop(Workshop workshop) {
        this.workshop = workshop;
    }
    
    public Vehicle getVehicle() {
        return vehicle;
    }
    /** Package-visible because used by BacklinkSettingSet only. */
    void setVehicle(Vehicle vehicle) {
        this.vehicle = vehicle;
    }
}
Agent.java
import javax.persistence.*;

@Entity
public class Agent
{
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    public Long getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

The optional relation to Agent class will not be used in this article, but in the follower about what happens when unsaved objects are in a relation and persist() or merge() gets called.

Utilities

To write short and concise code I created JPA database utilities. I needed two transactional calls, one without return for persist(), one with return for merge(). The ready-made Java functional interface for void functions with one parameter is Consumer, for functions with one parameter and a return object there is Function. See how a Consumer can be turned into a Function to avoid code duplications:

JpaUtil.java
 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;

public final class JpaUtil
{
    /**
     * @param entityManager required, for getting a transaction.
     * @param entityManagerFunction required, the persistence-function to call.
     * @param parameter optional, the parameter to pass to given function.
     */
    public static <P> void transactional(
            EntityManager entityManager, 
            Consumer<P> entityManagerFunction, 
            P parameter)
    {
        transactionalResult(
                entityManager,
                p -> { entityManagerFunction.accept(p); return null; },
                parameter);
    }
    
    /**
     * @param entityManager required, for getting a transaction.
     * @param entityManagerFunction required, the persistence-function to call.
     * @param parameter optional, the parameter to pass to given function.
     * @return the return value of the called function.
     */
    public static <R,P> R transactionalResult(
            EntityManager entityManager, 
            Function<P,R> entityManagerFunction, 
            P parameter)
    {
        final EntityTransaction transaction = entityManager.getTransaction();
        try {
            transaction.begin();
            final R returnValue = entityManagerFunction.apply(parameter);
            transaction.commit();
            return returnValue;
        }
        catch (Throwable th)    {
            if (transaction.isActive())
                transaction.rollback();
            throw th;
        }
    }
    
    /**
     * @param <T> the class of the objects in returned result list.
     * @param persistenceClass the database table to be queried.
     * @param em JPA database access.
     * @return all records from given table.
     */
    public static <T> List<T> findAll(Class<T> persistenceClass, EntityManager em) {
        return em
            .createQuery(
                    "select x from "+persistenceClass.getName()+" x", 
                    persistenceClass)
            .getResultList();
    }

    private JpaUtil() {} // do not instantiate
}

We will see instantly how this can be called. For full source code of JpaUtil please refer to my recent article (click to expand it).

Unit Test

Skeleton

The following unit test class contains just helpers to build test data and assert results. Below you will find @Test methods to be placed into this class. Please refer to my recent article for how to build a JPA unit test.

PersistAndMergeTest.java
 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import static org.junit.Assert.*;
import java.util.*;
import javax.persistence.EntityManager;
import org.junit.*;

public class PersistAndMergeTest
{
    // put test methods here, provide entityManager

    /** Asserts exactly one repair with one vehicle at one shop. */
    private Repair assertPersistedState(EntityManager entityManager, Workshop shop, Vehicle vehicle, Repair repair) {
        final List<Workshop> shops = JpaUtil.findAll(Workshop.class, entityManager);
        assertEquals(1, shops.size());
        final Workshop persistentShop = shops.get(0);
        assertEquals(shop.getName(), persistentShop.getName());
        
        final List<Vehicle> vehicles = JpaUtil.findAll(Vehicle.class, entityManager);
        assertEquals(1, vehicles.size());
        final Vehicle persistentVehicle = vehicles.get(0);
        assertEquals(vehicle.getName(), persistentVehicle.getName());
        
        assertEquals(1, persistentVehicle.getRepairs().size());
        final Repair persistentRepair = persistentVehicle.getRepairs().iterator().next();
        assertEquals(repair.getName(), persistentRepair.getName());
        assertEquals(persistentVehicle, persistentRepair.getVehicle());
        assertEquals(persistentShop, persistentRepair.getWorkshop());
        
        return persistentRepair;
    }
    
    private Workshop newWorkshop(String name) {
        final Workshop workshop = new Workshop();
        workshop.setName(name);
        return workshop;
    }
    
    private Vehicle newVehicle(String name) {
        final Vehicle vehicle = new Vehicle();
        vehicle.setName(name);
        return vehicle;
    }

    private Agent newAgent(String name) {
        final Agent agent = new Agent();
        agent.setName(name);
        return agent;
    }
    
    private Repair newRepair(String name, Vehicle vehicle, Workshop workshop) {
        final Repair repair = new Repair();
        repair.setName(name);
        repair.setWorkshop(workshop);
        vehicle.getRepairs().add(repair);
        return repair;
    }
}

Tests

Following methods test just one thing, and should be self-explanatory by their names. This first example is about @GeneratedValue id, and that merge() creates a clone.

    @Test
    public void persistWritesIdIntoParameter() {
        final Workshop shop = newWorkshop("Thelma's Car Repair");
        assertNull(shop.getId());
        JpaUtil.transactional(
                entityManager, entityManager::persist, shop);
        assertNotNull(shop.getId());
    }
    
    @Test
    public void mergeWritesNoIdIntoParameterButIntoReturnedClone() {
        final Workshop shop = newWorkshop("Louise's Truck Shop");
        assertNull(shop.getId());
        final Workshop mergedShop = JpaUtil.transactionalResult(
                entityManager, entityManager::merge, shop);
        assertTrue(shop != mergedShop);
        assertNull(shop.getId());
        assertNotNull(mergedShop.getId());
    }

The next examples show repeated calls on one object. The persist() method would not duplicate its parameter, but merge() would if you pass the original object to it again.

    @Test
    public void doublePersistShouldCreateJustOnePersistentObject() {
        final Workshop shop = newWorkshop("Thelma's Car Repair");
        JpaUtil.transactional(
                entityManager, entityManager::persist, shop);
        JpaUtil.transactional(
                entityManager, entityManager::persist, shop);
        assertEquals(1, JpaUtil.findAll(Workshop.class, entityManager).size());
    }
    
    @Test
    public void doubleMergeWillCreateTwoPersistentObjects() {
        final Workshop shop = newWorkshop("Louise's Truck Shop");
        JpaUtil.transactional(
                entityManager, entityManager::merge, shop);
        JpaUtil.transactional(
                entityManager, entityManager::merge, shop);
        assertEquals(2, JpaUtil.findAll(Workshop.class, entityManager).size());
    }

The next two tests show that both merge() and persist() can do both persist unsaved objects and update persistent objects.

    @Test
    public void persistCanUpdate() {
        final String SHOP_NAME = "Thelma's Car Repair";
        final Workshop shop = newWorkshop(SHOP_NAME);
        JpaUtil.transactional(
                entityManager, entityManager::persist, shop);
        
        assertEquals(SHOP_NAME, entityManager.find(Workshop.class, shop.getId()).getName());
        
        final String UPDATED_NAME = "Louise's Truck Shop";
        shop.setName(UPDATED_NAME);
        JpaUtil.transactional(
                entityManager, entityManager::persist, shop);
        
        assertEquals(UPDATED_NAME, entityManager.find(Workshop.class, shop.getId()).getName());
    }
    
    @Test
    public void mergeCanUpdate() {
        final String SHOP_NAME = "Louise's Truck Shop";
        final Workshop shop = newWorkshop(SHOP_NAME);
        final Workshop mergedShop = JpaUtil.transactionalResult(
                entityManager, entityManager::merge, shop);
        
        assertEquals(SHOP_NAME, entityManager.find(Workshop.class, mergedShop.getId()).getName());
        
        final String UPDATED_NAME = "Thelma's Car Repair";
        mergedShop.setName(UPDATED_NAME);
        JpaUtil.transactional(
                entityManager, entityManager::merge, mergedShop);
        
        assertEquals(UPDATED_NAME, entityManager.find(Workshop.class, mergedShop.getId()).getName());
    }

Following examples save a graph of objects in both ways. Mind that the Workshop must be already persistent to be able to persist a Vehicle with cascaded Repair. Mind also that, with merge(), the mergedShop must be linked to the Repair, else the shop would be either saved a second time (EclipseLink), or an exception would be thrown (Hibernate).

    @Test
    public void persistGraph() {
        final Workshop shop = newWorkshop("John's Repair Shop");
        JpaUtil.transactional(entityManager, entityManager::persist, shop);
        
        final Vehicle vehicle = newVehicle("Bentley");
        final Repair repair = newRepair("Breaks", vehicle, shop);
        JpaUtil.transactional(entityManager, entityManager::persist, vehicle);
        
        final Repair persistentRepair = assertPersistedState(entityManager, shop, vehicle, repair);
        
        assertTrue(repair == persistentRepair);
        assertTrue(shop == persistentRepair.getWorkshop());
        assertTrue(vehicle == persistentRepair.getVehicle());
    }

    @Test
    public void mergeGraph() {
        final Workshop shop = newWorkshop("Jeff's Workshop");
        final Workshop mergedShop = JpaUtil.transactionalResult(entityManager, entityManager::merge, shop);
        
        final Vehicle vehicle = newVehicle("Ferrari");
        final Repair repair = newRepair("Tires", vehicle, mergedShop); // use merged shop, not original one!
        final Vehicle mergedVehicle = JpaUtil.transactionalResult(entityManager, entityManager::merge, vehicle);
        
        final Repair persistentRepair = assertPersistedState(entityManager, shop, vehicle, repair);
        
        assertTrue(repair != persistentRepair);
        assertTrue(shop != persistentRepair.getWorkshop());
        assertTrue(vehicle != persistentRepair.getVehicle());

        assertTrue(mergedShop == persistentRepair.getWorkshop());
        assertTrue(mergedVehicle == persistentRepair.getVehicle());
    }

The merge example shows that none of the objects returned from merge() is identical with the parameters that were given to the call.

Conclusion

Use persist() when you want JPA to work directly on your objects. Use merge() when you don't want JPA to touch your objects, but persist them, even when they are detached.

If you decide to use merge(), make sure that you always use the object returned from that call, else you will have to cope with data duplications and exceptions. It is for sure the more error-prone method, because people forget to use the returned object instead of the parameter object.

One pitfall I saw recently: a test created a new object, called merge() to persist it, and finally deleted the object, but deleted the original transient object instead of the merged one. The test failed because the persistent object was still present after. Surprisingly JPA remove() doesn't throw an exception when receiving a transient object. The problem is that merge() doesn't do what we'd intuitively expect.

In my next article I will try to show how persist() and merge() behave when there is a non-persistent and non-cascaded object in a saved graph. Here the JPA specification seems to be unclear, because Hibernate and EclipseLink do different things then.




Keine Kommentare: