Hibernate's native Session historically offered save(), update() and saveOrUpdate() alongside JPA's persist() and merge(). How do those legacy operations differ in behaviour, and what is their status in modern Hibernate?
answer
- update = re-attach *your* instance, no SELECT
- merge = copy onto managed twin, returns it
- saveOrUpdate = unsaved-value guess, breaks on assigned ids
- NonUniqueObjectException from update
- deprecated in Hibernate 6, removed in 7
basics
~20 ssave() inserts and returns the generated id, update() re-attaches a detached instance and forces an UPDATE, saveOrUpdate() picks between them by inspecting the id or version. All three are deprecated in Hibernate 6 and gone in Hibernate 7; use persist and merge.
solid answer
~50 sThe native trio predates JPA: - **`save(entity)`** — like persist but returns the generated identifier and tends to obtain it eagerly, so it can insert outside a transaction and does not respect JPA's deferred-INSERT semantics as strictly. - **`update(entity)`** — truly **re-attaches** the instance you passed: it becomes managed and an UPDATE is scheduled, even when nothing changed (unless `@SelectBeforeUpdate` or dynamic update mitigates it). If the context already holds another instance for that id it throws `NonUniqueObjectException`. - **`saveOrUpdate(entity)`** — decides insert vs update using the 'unsaved-value' heuristic on the identifier or `@Version` field. The key contrast: `update` attaches your object (no SELECT, no copy, no return value), `merge` copies onto a managed instance and returns it. `update` is cheaper but brittle; `merge` is portable and conflict-free. Hibernate 6 deprecated all three, and Hibernate 7 removed them. New code uses `persist`/`merge`, plus `Session.lock(entity, LockMode.NONE)` when you specifically want the old re-attach behaviour.
code
java · 8 lines// legacy (removed in Hibernate 7)
session.update(detachedOrder); // detachedOrder itself becomes managed
// modern portable
Order managed = em.merge(detachedOrder);
// modern re-attach without a state copy
session.lock(detachedOrder, LockMode.NONE);go deeper
Know that the legacy trio exists in old code and that persist/merge are the modern equivalents; do not be surprised by save() in a tutorial.
Explain attach-versus-copy precisely, the missing SELECT behind update's unconditional UPDATE, and that Hibernate 6 deprecated and 7 removed these methods.
Discuss migrating a legacy codebase: which update() calls can become merge, where the extra SELECT hurts, when Session.lock is the faithful replacement, and how full-column UPDATEs interact with auditing and replication.
Position it as API-evolution risk management — vendor-specific surface area that eventually disappears, and how you fence provider APIs so a major-version removal is a contained change.
## Why two families of methods exist Hibernate shipped years before JPA. Its native API grew `save`, `update`, `saveOrUpdate`, `delete` and `saveOrUpdateCopy`. When JPA standardised the model, the same provider had to expose `persist`, `merge`, `remove`. `Session` kept both sets for a long time, which is why legacy codebases mix them freely and why the distinction is a common interview probe. ## The three legacy operations **save(Object): Serializable.** Inserts a transient instance and returns the identifier. The practical differences from `persist` are: it returns the id; it will generate the identifier immediately, which historically meant it could execute the INSERT outside an active transaction; and it does not throw for an entity that is already persistent in the way persist does. Code that depends on `Long id = (Long) session.save(x);` is trivially rewritten as `em.persist(x); Long id = x.getId();`. **update(Object): void.** This is the one with genuinely distinct semantics. It takes a *detached* instance and makes **that instance** managed again — a real re-attach. Consequences: - **No SELECT.** Hibernate has no snapshot, so it cannot tell what changed and simply schedules a full UPDATE of every column at flush. `@DynamicUpdate` or `@SelectBeforeUpdate` change that, the latter by adding the SELECT back. - **No return value** — your reference is the managed one. - **`NonUniqueObjectException`** if the persistence context already holds a different instance with the same identifier, because attaching would break the one-instance-per-identity rule. - Calling it on a transient instance is an error, and on an already-managed one it is a no-op. **saveOrUpdate(Object): void.** Routes to save or update by asking 'does this look unsaved?'. The check uses the identifier (null, or a configured `unsaved-value`) or, if the entity is versioned, a null/zero version. This heuristic is exactly what fails for entities with **assigned** identifiers — a natural-key entity always looks saved, so Hibernate tries an UPDATE against a row that does not exist and either silently affects zero rows or throws a stale-state error. ## update vs merge, side by side | | Session.update | EntityManager.merge | |---|---|---| | Your instance becomes managed | yes | no | | Return value | void | managed copy | | Loads the row first | no | yes (usually) | | Two instances with same id | throws | fine | | Portable JPA | no | yes | | UPDATE emitted | unconditionally | only if state actually differs | The performance argument for `update` — 'it skips the SELECT' — is real, especially in batch re-attach loops. The correctness argument against it is stronger: any code path that already touched the entity in the same context turns into a runtime exception, and blind full-column UPDATEs are noisy in audit triggers and replication, and can clobber columns your detached instance never loaded. ## Modern status Hibernate 6.0 deprecated `save`, `update`, `saveOrUpdate` (and the `delete`/`refresh` overloads that mirrored them) in favour of the JPA operations. Hibernate 7.0 removed them from `Session`. The supported replacements: - `save` → `persist` (read the id off the entity). - `saveOrUpdate` → `merge`. - `update` → `merge`, or `session.lock(entity, LockMode.NONE)` when you specifically want a re-attach without a state copy — `lock` reassociates the given instance, and with `LockMode.READ`/optimistic modes it also verifies the version. - Bulk insert-or-update work moved toward `StatelessSession`, which in Hibernate 6.5+ exposes `upsert`. A good answer names the semantic difference (attach vs copy), the two failure modes (`NonUniqueObjectException`, assigned-id heuristics) and the deprecation/removal timeline. A weak answer says they are 'the same thing with different names'.
- Why can Session.update() throw NonUniqueObjectException while merge never does?update re-attaches the exact instance you pass. If the persistence context already holds a different Java object for that identifier, attaching would put two managed instances behind one row, so Hibernate refuses. merge instead copies your state onto whichever instance is already managed, so the conflict cannot arise.
- Where does saveOrUpdate's insert-or-update guess go wrong?It infers 'new' from a null or unsaved-value identifier, or a null/zero version. Entities with assigned identifiers — natural keys, client-supplied ids — always look already-saved, so Hibernate issues an UPDATE for a row that does not exist and reports zero rows affected or a stale state exception instead of inserting.
saying these in an interview costs you the question
- Saying update() and merge() are the same call with different names
- Claiming update() issues a SELECT to work out what changed
- Recommending saveOrUpdate for new code in Hibernate 6/7
- Believing save() and persist() differ only in the return type, ignoring identifier-generation timing