persist, save, merge, saveOrUpdate in Hibernate 7: The Five Decisions That Cause Duplicate Inserts
Here is the table that every Hibernate tutorial promises and none of them finish: MethodEntity state: TransientEntity state: ManagedEntity state: Detachedpersist()Schedules INSERT; ID assigned at flush (SEQUENCE) or immediately (IDENTITY)No-op — entity already trackedThrows PersistentObjectExceptionsave()Schedules INSERT; returns ID immediately (may fire INSERT for IDENTITY)Returns existing ID; entity already trackedTreats as new: ignores existing ID, generates new one — duplicate insertmerge()Copies state to new managed entity; returns the managed copyReturns same instance (identity)Issues SELECT to load current DB state; merges over it; returns managed copyupdate()Throws TransientPropertyValueExceptionNo-op if already in session; throws NonUniqueObjectException if different instance same IDRe-attaches; schedules UPDATE; throws NonUniqueObjectException if conflicting instance existssaveOrUpdate()Calls save() pathNo-op if already in sessionCalls update() path; same NonUniqueObjectException risk Most of the bugs that come to production code review — duplicate inserts, silent data loss, unexpected exceptions — trace to a mismatch between what the developer expected from the column above and what the table actually says. The rest of this post is a catalog of the five most common mismatches, with code showing exactly what goes wrong and why.