persist makes a new entity managed; merge takes a detached entity and copies its state onto a managed instance. They behave differently, and saveOrUpdate is a Hibernate-only method that Spring Data's save() effectively replaces. The confusion is worth untangling because it explains subtle "my update didn't persist" bugs.
Detached entities — the setup
A detached entity was once managed but its persistence context has closed (e.g., it came back from a previous transaction, was deserialized from an HTTP request, or you called clear()). It has a PK but is no longer tracked — mutating it does nothing until you reattach it.
persist
persist(entity) makes a transient (new) entity managed. It expects no existing row; the entity becomes managed and is INSERTed at flush. The same instance you passed in is now managed (the call returns void).
User u = new User("a@b.com");
em.persist(u); // u is now managed; INSERT at flush
Calling persist on a detached entity throws EntityExistsException (or fails at flush).
merge
merge(entity) is for detached entities. It does not attach your instance. Instead it loads (or finds in the context) the managed entity with that PK, copies your state onto it, and returns that managed copy. Your original argument stays detached.
User detached = ...; // from a previous tx, with id=5
detached.setName("New");
User managed = em.merge(detached); // SELECT id=5, copy fields, return managed
managed.setName("Newer"); // affects the row
detached.setName("Ignored"); // detached → no effect
The trap: merge returns the managed instance — keep working with the return value, not the argument.
Spring Data's save()
JpaRepository.save() hides this: if the entity is new (no PK, or per Persistable.isNew()), it calls persist; otherwise it calls merge. That's convenient but means save() on a detached entity does a merge — a SELECT plus copy, and a blanket overwrite of every column.
public <S> S save(S e) { return isNew(e) ? em.persist(e),e : em.merge(e); } // conceptually
saveOrUpdate
saveOrUpdate is Hibernate-native (Session, not JPA). Unlike merge it reattaches the very instance you pass (no copy, no new instance returned). It can throw NonUniqueObjectException if another managed instance with that PK is already in the context. Modern code should prefer JPA merge / Spring's save.