What is the Difference Between persist() and merge()
In JPA and Hibernate, persist() and merge() are core methods of the EntityManager interface used to bring entity objects under the management of the PersistenceContext. However,...
In JPA and Hibernate, persist() and merge() are core methods of the EntityManager interface used to bring entity objects under the management of the PersistenceContext. However, they are designed for fundamentally different entity lifecycle states, exhibit distinct memory semantics, and trigger entirely different sequences of SQL operations.
🟢 Junior Level
Core Differences: Executive Summary
persist(entity): Designed exclusively for new entities in theTransientstate (objects that do not yet exist in the database). It transitions the passed Java instance directly into theManagedstate in-place and returnsvoid.merge(entity): Designed for entities in theDetachedstate (objects previously persisted whose persistence context has closed). It locates or loads an existing entity from the database, copies the state from the detached object onto the managed instance, and returns a reference to that new managed instance.
PERSIST VS MERGE: HEAP MEMORY BEHAVIOR
1. INVOCATION OF PERSIST:
[ user (Transient) ] ──persist(user)──> [ user (Managed) ]
(The exact same Java instance becomes managed in-place; returns void)
2. INVOCATION OF MERGE:
[ detachedUser ] ──merge(detachedUser)──> [ managedUser (Managed) ]
│
(detachedUser REMAINS Detached! ▼
All subsequent modifications must be made to managedUser!)
Comparative Analysis
| Feature | entityManager.persist(entity) |
entityManager.merge(entity) |
|---|---|---|
| Target Lifecycle State | Transient (New entity) |
Detached (Detached entity) |
| Return Value | void |
Reference to the Managed instance ($T$) |
| Passed Object Status | Becomes Managed |
Remains Detached |
| Generated SQL | Only SQL INSERT (during flush) |
SQL SELECT, then UPDATE or INSERT |
| Behavior if ID Exists in L1 | Throws EntityExistsException |
Updates the existing managed instance |
Practical Code Examples
// 1. Using persist():
User newUser = new User();
newUser.setName("Maxim");
entityManager.persist(newUser);
// newUser is now MANAGED. You can mutate its fields directly:
newUser.setName("Maxim Updated"); // Dirty checking automatically persists this on flush!
// 2. Using merge():
User detachedUser = fetchFromExternalRestService(); // Entity with ID=1, outside session
detachedUser.setName("New Name");
// ❌ COMMON JUNIOR BUG:
// entityManager.merge(detachedUser);
// detachedUser.setEmail("test@example.com"); // NEVER SAVED TO DATABASE!
// ✅ PRODUCTION STANDARD:
User managedUser = entityManager.merge(detachedUser);
managedUser.setEmail("test@example.com"); // Successfully persisted by Dirty Checking!
🟡 Middle Level
Internal Execution Algorithm of merge()
When an application invokes entityManager.merge(detachedEntity):
- Hibernate extracts the primary key (
id) from the passed object. - It inspects the First-Level Cache (
entitiesByKey) for a managed entity with that ID. - If the entity is absent from the active persistence context, Hibernate executes an SQL
SELECTagainst the database. - If the database record is found:
- Hibernate retrieves or instantiates the
Managedentity instance. - It copies all property values from
detachedEntityonto thisManagedinstance. - It schedules an SQL
UPDATEin theActionQueue(if dirty checking detects state changes).
- Hibernate retrieves or instantiates the
- If the database record is not found (or the ID was
null):- Hibernate creates a new
Managedentity instance. - It copies property values from
detachedEntity. - It schedules an SQL
INSERTin theActionQueue.
- Hibernate creates a new
- Finally, it returns the reference to the
Managedinstance.
Why Calling merge() for New Entities is an Anti-Pattern
Developers often default to using merge() universally because “it handles both new and existing entities”:
User newUser = new User();
newUser.setName("Oleg");
entityManager.merge(newUser); // Technically succeeds, BUT...
The Hidden Overhead:
- If the ID generation strategy relies on manual assignment or is not null, Hibernate fires an unnecessary penalty SQL
SELECTto confirm whether the row exists in the database. - It allocates an extra duplicate object instance on the JVM heap.
- For new entities, always invoke
persist().
Cascading Rules: CascadeType.PERSIST vs CascadeType.MERGE
When entities manage relationships:
@OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST)
private List<OrderItem> items = new ArrayList<>();
- Invoking
entityManager.persist(order)cascades to all elements initems. - However, calling
entityManager.merge(order)will not cascade toitemsunlessCascadeType.MERGE(orALL) is explicitly included. The child elements will remain detached, and modifications made to them will be silently lost.
🔴 Senior Level
Under the Hood of Spring Data JPA: How repository.save() Operates
Many developers believe Spring Data JPA’s save() executes a specialized internal operation. In reality, SimpleJpaRepository implements a conditional routing mechanism:
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
The “Assigned ID” Penalty Problem:
How does Spring Data determine entityInformation.isNew(entity)?
- Default Strategy: If the
@Idproperty isnull(or0for numeric primitives), the entity is recognized as new (isNew() == true), and Spring callspersist(). - The Assigned ID Dilemma: When using manually assigned identifiers (business UUIDs, national IDs, or composite keys), the primary key is already populated before saving (
id != null). - Spring Data misinterprets the entity as
Detached(isNew() == false)! - Instead of issuing a fast direct SQL
INSERT, Spring invokesentityManager.merge(entity). - Hibernate executes a parasitic preliminary SQL
SELECT, discovers no row exists, and only then issues the SQLINSERT.
Architectural Solution:
The entity must implement org.springframework.data.domain.Persistable<ID>:
@Entity
public class Payment implements Persistable<String> {
@Id
private String transactionUuid;
@Transient
private boolean isNewEntity = true;
@Override
public String getId() { return transactionUuid; }
@Override
public boolean isNew() { return isNewEntity; }
@PostPersist
@PostLoad
void markNotNew() { this.isNewEntity = false; }
}
With Persistable implemented, Spring calls persist() directly, eliminating the redundant database round-trip.
Cascading merge() Pitfalls with Uninitialized Lazy Collections
Consider the following scenario:
- In Transaction 1, a
Departmententity is loaded with an uninitialized lazy collectionemployees(FetchType.LAZY). - The transaction closes without accessing
employees; the collection remains an uninitializedPersistentBagproxy in the detachedDepartment. - In Transaction 2,
entityManager.merge(detachedDept)is invoked. - Internal Behavior: Hibernate’s
DefaultMergeEventListenerinspects collection states. Ifemployeesis configured withCascadeType.MERGEorALLand was left untouched:- Hibernate maintains the uninitialized proxy state in the managed copy.
- However, if the developer replaced the collection on the detached instance (
detachedDept.setEmployees(new ArrayList<>())), Hibernate interprets this as the explicit removal of all previous employees (Orphan Removal) and issues SQLDELETEstatements against all child records in the database!
4 Tricky Questions
1. What happens if User(id = 1) is already loaded in the active session (Managed), and we invoke entityManager.persist(user2) with the same id = 1? What if we invoke entityManager.merge(user2)?
Answer:
- For
persist(user2): Hibernate inspectsentitiesByKey. Upon finding that an entity with that type and primary key already exists in thePersistenceContext, it immediately throws:jakarta.persistence.EntityExistsException(orNonUniqueObjectException). - For
merge(user2): No exception occurs. Hibernate retrieves the existingUser(id = 1)instance from the L1 cache, copies all property values fromuser2into it, and returns the managed reference. Zero SQLSELECTqueries are executed because the entity is already in memory.
2. Why does persist() return void while merge() returns an object of type T?
Answer:
persist(e)mutates the lifecycle state of the passed Java instance in-place. The object referenceeyou already possess becomes managed in thePersistenceContext. Returning it would be redundant.merge(e)cannot make the passed instanceemanaged because an instance representing that database row might already exist in the persistence context (which would break the Identity Map pattern:instance1 != instance2). Thus,merge()must return the specific reference attached to the session while leaving the original parameterein theDetachedstate.
3. We pass an entity in the Removed state to persist(), and to merge(). What happens in each case?
Answer:
persist(removedEntity): The JPA specification explicitly permits un-removing an entity. The entity transitions back to theManagedstate, and its scheduledEntityDeleteActionin theActionQueueis canceled.merge(removedEntity): The JPA specification strictly forbids merging a removed entity. InvokingentityManager.merge(removedEntity)throwsjava.lang.IllegalArgumentException.
4. What happens if you pass a Transient entity with a null ID to merge(), assuming its primary key strategy is GenerationType.SEQUENCE?
Answer:
merge()identifies the entity as new becauseid == null.- Hibernate queries the database sequence for the next identifier (
SELECT nextval(...)). - It instantiates a brand new entity object, assigns the generated ID, copies all properties, and places the new instance into the
Managedstate. - The original entity instance passed as an argument retains
id == nulland remains in theTransientstate. - The method returns the newly constructed managed entity with the populated ID.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“
persist()transitions a new (Transient) entity toManagedin-place, returnsvoid, and schedules an SQLINSERTwithout reading the database. Conversely,merge()transitions a detached entity toManagedby executing an SQLSELECT, copying property values onto a managed instance, and returning that managed reference while the input parameter remainsDetached. In Spring Data JPA,repository.save()usespersist()for new entities (id == null) andmerge()for detached entities. For entities with manually assigned identifiers, you must implementPersistable<ID>to prevent Spring Data from executing redundant, penaltySELECTqueries before every insert.”
Key Production Metrics & Health Checks
- SELECT-before-INSERT Ratio: A high volume of
SELECTqueries immediately precedingINSERTstatements indicates improper use ofmerge()or missingPersistable<ID>implementations on assigned-key entities. save()Execution Duration: Significant latency duringsave()suggests deep object graph traversals and cascading validations duringmerge().
Red Flags to Avoid
- ❌ “Calling
merge()makes the original input object managed.” — The input instance remains detached; you must use the returned reference. - ❌ “Always use
merge()because it is universal and handles both INSERT and UPDATE.” — This introduces redundantSELECTqueries and doubles heap memory consumption. - ❌ “Calling
persist()immediately executes an SQL INSERT over the network.” — Except forGenerationType.IDENTITY, SQL statements are deferred untilflush(). - ❌ “You can safely pass a detached entity to
persist().” — Passing a detached entity topersist()throwsEntityExistsExceptionorPersistentObjectException.
Related Topics
- Describe the Entity Lifecycle in Hibernate — Lifecycle states
- What Are the Transient, Persistent, Detached, Removed States — State transitions
- What is Dirty Checking in Hibernate — Change detection on managed entities
- How Does the Flush Mechanism Work in Hibernate — ActionQueue execution
- What Does the refresh() Method Do — State reload