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 themerge()
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 thepersist()
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()
.
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.
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 | import java.util.List; |
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).
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 | import static org.junit.Assert.*; |
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.
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.
ɔ⃝ Fritz Ritzberger, 2020-02-04