Hibernate save vs saveOrUpdate vs update: Hibernate 7 Guide

Hibernate save(), update() and saveOrUpdate() were deprecated in Hibernate 6 and removed in Hibernate 7. We run the old methods on Hibernate 6.6, then show how persist() and merge() replace them for new, detached and managed entities.

Hibernate save(), update() and saveOrUpdate() were the Session methods that inserted a new entity, reattached a detached entity, or chose between the two by looking at the id. Hibernate 6.0 deprecated all three, and Hibernate 7.0 removed them, so code that calls them no longer compiles. Their replacements are the two standard Jakarta Persistence methods, persist() for a new object and merge() for an object that already has a row.

We meet these methods when we upgrade an older app to Hibernate 7, where every save() or saveOrUpdate() call turns into a compile error.

The following example shows the Hibernate 7 calls for a Plant entity, with the old method names in the comments.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String species;
private String potSize;
private double price;
em.persist(basil);                         // was save(basil)
                                           // insert into Plant (name,potSize,price,species,id) values (?,?,?,?,default)
Long id = basil.getId();                   // id = 2, the value save() used to return

Plant copy = em.merge(repotted);           // was update(repotted)
                                           // select p1_0.id,...,p1_0.species from Plant p1_0 where p1_0.id=?
                                           // commit: update Plant set name=?,potSize=?,price=?,species=? where id=?
boolean same = copy == repotted;           // false, keep using copy

Plant saved = PlantStore.save(em, aloe);   // was saveOrUpdate(aloe): persist() if the id is null, else merge()

Notice that merge() returns a different object than the one we pass, so we keep using copy.

Next, we run the old methods on Hibernate 6.6 to see what they did. After that, we run persist() and merge() on Hibernate 7.4 for the same cases.

1. What Did save(), update() and saveOrUpdate() Do?

Each method was made for an object in a particular state. Hibernate knows three entity states that matter here.

  • A new (transient) object was created with new Plant(…), so it has no id and no row.
  • A managed object is tracked by an open Session. Hibernate saves its changes at commit by comparing it with the values it loaded, which is called dirty checking.
  • A detached object has an id and a row, but the Session that loaded it is closed. A plant that came back from a web form is a typical example.

The method save() was for new objects, update() for detached ones, and saveOrUpdate() for both. Running the old Session methods on Hibernate 6.6.58 with an IDENTITY id shows how each one treats a new or a detached object.

save()update()saveOrUpdate()
Meant forA new objectA detached objectA new or a detached object
ReturnsThe generated id, as Object (here a Long)voidvoid
The object we passBecomes managedBecomes managedBecomes managed
SQLINSERT at the callUPDATE at commit, even when nothing changedINSERT at the call, or UPDATE at commit
Id is nullInsertsTransientObjectExceptionInserts
Detached objectInserts a second rowUpdates the rowUpdates the row
Id that has no rowIgnores the id and insertsOptimisticLockException at commitOptimisticLockException at commit
Cascades alongCascadeType.SAVE_UPDATECascadeType.SAVE_UPDATECascadeType.SAVE_UPDATE

The INSERT runs at the call because the id comes from an IDENTITY column, so Hibernate must insert the row to learn it. The method saveOrUpdate() decides by the id, where null means new and anything else means detached.

1.1. The Old Calls on Hibernate 6.6

The nursery table starts with one plant, Monstera with id 1. In each call, monstera is a detached copy of that row, loaded in an earlier session.

Object id = session.save(basil);      // insert into Plant (name,potSize,price,species,id) values (?,?,?,?,default)
                                      // id = 2, a Long

session.update(monstera);             // no SQL, monstera is managed again
                                      // commit: update Plant set name=?,potSize=?,price=?,species=? where id=?

session.saveOrUpdate(fern);           // id is null: insert into Plant ...
session.saveOrUpdate(monstera);       // id is set: no SQL, commit: update Plant ...

The update() call sends an UPDATE even though we changed nothing. The method update() never reads the row, so Hibernate cannot know whether the values differ and writes all columns.

1.2. save() on a Detached Plant Inserts a Second Row

The method save() treats every object as new. With a generated id, it ignores the id the object already has, inserts a new row and overwrites the id field with the new value.

Object id = session.save(monstera);   // insert into Plant (name,potSize,price,species,id) values (?,?,?,?,default)
                                      // id = 2, monstera.getId() = 2

After the call, the table holds the same plant twice. In a nursery catalog, an admin who edits the price of Monstera in a form ends up with two Monstera entries.

Before and after tables for session.save(monstera) on Hibernate 6.6: before, the Plant table has row 1 Monstera medium 25.0 and the Java object monstera has id 1 and is detached; save runs an insert with a default id and ignores id 1; after, the table has rows 1 and 2 with the same Monstera data and the object has id 2 and is managed; a note says that in Hibernate 7 persist(monstera) throws EntityExistsException and merge(monstera) copies the fields onto row 1
The method save() turned an edit into a duplicate row. In Hibernate 7 the same mistake with persist() throws an exception instead.

A plant whose id has no row at all, such as 99, gets the same treatment, so save() inserts it with the next id.

1.3. Errors From update() and saveOrUpdate()

The method update() attaches the object we pass, so it fails whenever Hibernate cannot attach it. Three cases come up in practice.

session.update(new Plant("Fern", ...));   // id is null: TransientObjectException

Plant loaded = session.get(Plant.class, 1L);
session.update(monstera);                 // a second object with id 1: NonUniqueObjectException

session.update(cactus);                   // id 99, no row: no SQL at the call
                                          // commit: OptimisticLockException
org.hibernate.TransientObjectException: The given object has a null identifier: com.howtodoinjava.hibernate.saveorupdate.legacy.Plant

org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session: [com.howtodoinjava.hibernate.saveorupdate.legacy.Plant#1]

jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.howtodoinjava.hibernate.saveorupdate.legacy.Plant#99]

The method saveOrUpdate() throws the last two in the same situations. The NonUniqueObjectException exists because a Session holds at most one object per id in its persistence context, the set of entities it tracks. The OptimisticLockException appears because the UPDATE changed zero rows.

2. Saving Plants With persist() and merge() in Hibernate 7

Hibernate 7.0 removed all forms of the three methods from Session. The Session interface in hibernate-core 7.4.11 has persist() and merge() and none of the old names, so an old call fails at compile time.

OldCall.java:7: error: cannot find symbol
    session.save(basil);
           ^
  symbol:   method save(Plant)
  location: variable session of type Session

Hibernate 7.0 also removed CascadeType.SAVE_UPDATE from org.hibernate.annotations, and its replacements are the JPA cascade types PERSIST and MERGE.

2.1. The Plant Nursery Example

The following example is a plant nursery app that runs on Hibernate 7.4.11, Java 25 and an in-memory H2 database. The Plant entity from the intro has an @Id with an IDENTITY generator and four plain columns. We create the EntityManagerFactory without a persistence.xml.

EntityManagerFactory emf = new HibernatePersistenceConfiguration("plant-nursery")
    .managedClasses(Plant.class)
    .jdbcUrl("jdbc:h2:mem:nursery;DB_CLOSE_DELAY=-1")
    .jdbcCredentials("sa", "")
    .schemaToolingAction(Action.CREATE_DROP)
    .createEntityManagerFactory();

Each case starts with one row, Monstera (medium pot, 25.0) with id 1. The comments show the SQL that Hibernate sent at the call and at commit.

2.2. Saving a New Plant

The method persist() replaces save(). It returns nothing, and the object we pass becomes managed and gets its id.

Plant basil = new Plant("Basil", "Ocimum basilicum", "small", 4.00);
em.persist(basil);                      // insert into Plant (name,potSize,price,species,id) values (?,?,?,?,default)
Long id = basil.getId();                // 2
boolean managed = em.contains(basil);   // true

The method merge() accepts a new object too, but it inserts a copy. The object we pass keeps a null id.

Plant fern = new Plant("Fern", "Nephrolepis exaltata", "medium", 9.00);
Plant copy = em.merge(fern);           // insert into Plant (name,potSize,price,species,id) values (?,?,?,?,default)
Long copyId = copy.getId();            // 2
Long fernId = fern.getId();            // null
boolean managed = em.contains(fern);   // false

For a new object we call persist(), so the object in our variable is the one Hibernate saves.

2.3. Saving a Detached Plant

The method merge() replaces update(). For example, a customer changed the pot size of Monstera in a form, and repotted is the detached plant with that change.

repotted.setPotSize("large");
Plant copy = em.merge(repotted);                   // select p1_0.id,p1_0.name,p1_0.potSize,p1_0.price,p1_0.species from Plant p1_0 where p1_0.id=?
boolean same = copy == repotted;                   // false
boolean repottedManaged = em.contains(repotted);   // false
boolean copyManaged = em.contains(copy);           // true
                                                   // commit: update Plant set name=?,potSize=?,price=?,species=? where id=?

The method merge() loads row 1, copies the fields of repotted onto the loaded object and returns it. Dirty checking then compares the copy with the row. When we merge a detached plant that has no change, the SELECT runs and the UPDATE does not, while the old update() wrote the row anyway.

Passing the detached plant to persist() fails at the call, before any SQL.

em.persist(monstera);   // EntityExistsException
jakarta.persistence.EntityExistsException: Detached entity passed to persist: com.howtodoinjava.hibernate.saveorupdate.Plant

This exception is the Hibernate 7 answer to the duplicate row from section 1.2. Jakarta Persistence allows persist() to reject a detached object, and Hibernate does so as soon as the id of a generated-id entity is not null.

2.4. Saving a Plant That Is Already Managed

A plant loaded with find() in the same transaction needs no save call at all. Its setters are enough, and Hibernate ignores both methods.

Plant monstera = em.find(Plant.class, 1L);   // select ...
monstera.setPrice(27.00);
em.persist(monstera);                        // ignored, no SQL
Plant merged = em.merge(monstera);           // returns monstera itself, no SQL
                                             // commit: update Plant set name=?,potSize=?,price=?,species=? where id=?

When the transaction already holds row 1 and we merge a detached copy of it, merge() copies the fields onto the loaded object and returns it. There is no SELECT, and no NonUniqueObjectException as with update().

Plant loaded = em.find(Plant.class, 1L);
priced.setPrice(30.00);              // priced: detached copy of row 1
Plant copy = em.merge(priced);       // no SQL
boolean same = copy == loaded;       // true
double price = loaded.getPrice();    // 30.0
                                     // commit: update Plant set name=?,potSize=?,price=?,species=? where id=?

2.5. Saving a Plant With an Id That Has No Row

A cactus with the id 99 set by hand looks detached to Hibernate, because its id is not null. Neither method inserts it.

em.persist(cactus);                // EntityExistsException: Detached entity passed to persist: ...Plant

Plant merged = em.merge(cactus);   // select ... where p1_0.id=?  (sent twice)
                                   // OptimisticLockException at the call
jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another transaction for entity [com.howtodoinjava.hibernate.saveorupdate.Plant with id '99']

The old save() inserted this plant under a new id, and update() failed only at commit. With a generated id, a new plant must reach Hibernate with a null id, for example by not copying the id from a request body into a new entity.

3. persist() vs merge()

The two methods differ in which object ends up managed. The method persist() puts our object into the persistence context, whereas merge() leaves our object outside and returns another one.

Two panels: on the left, em.persist(basil) takes the new object basil with id null, sends the insert, and the same object basil, now managed with id 2, sits in the persistence context; it returns void and later changes to basil are saved; on the right, em.merge(repotted) loads row 1 with a select, copies the fields of the detached repotted object onto a managed copy with id 1 and pot size large, and returns that copy; repotted stays detached and changes to it are lost
The method persist() makes our object managed, whereas merge() returns a managed copy and leaves our object detached.

Which object ends up managed explains every other row of the comparison.

persist()merge()
ReturnsvoidThe managed instance
New plant (id null)Inserts this objectInserts a copy; our object keeps id null
Detached plantEntityExistsExceptionSELECT, then UPDATE at commit if a field changed
Managed plantIgnoredReturns the same object, no SQL
Id with no rowEntityExistsExceptionOptimisticLockException
Cascades alongCascadeType.PERSISTCascadeType.MERGE
Replacessave()update()

After merge(), we continue with the returned object. A change to the argument after the call is lost, while a change to the copy is saved.

emf.runInTransaction(em -> {
  Plant copy = em.merge(plant);
  plant.setPrice(99.00);          // lost: plant is not managed
  copy.setPotSize("large");       // saved at commit
});
// row 1: price = 25.0, potSize = large

4. Migrating From save(), update() and saveOrUpdate()

Most old calls have a one-line replacement. The one to watch is update(), because the variable has to take the returned object.

Hibernate 5 and 6Hibernate 7
Object id = session.save(plant);session.persist(plant); then plant.getId()
session.update(plant);plant = session.merge(plant);
session.saveOrUpdate(plant);persist() for a new plant, merge() for a detached one
session.update(plant) on a plant loaded in the same sessionNothing; dirty checking saves it
session.delete(plant);session.remove(plant);
@Cascade(CascadeType.SAVE_UPDATE)cascade = {CascadeType.PERSIST, CascadeType.MERGE}

The method session.delete() was removed in the same release, and remove() replaces it. When code holds a SessionFactory and Session instead of an EntityManager, the same persist() and merge() methods are on Session.

Which call to write depends on the state of the plant, and the same two questions decide every case.

Decision flow for a plant to save: if plant.getId() is null, call em.persist(plant), which replaces save(); otherwise, if the plant was loaded in this transaction, no call is needed because the setters and dirty checking update it at commit; otherwise the plant is detached, for example from a form, so call copy = em.merge(plant), which replaces update(), and keep using copy; a note says saveOrUpdate() made the first decision for us, its replacement is a method that calls persist() when the id is null and merge() otherwise, as Spring Data JPA save() does, and an id set by hand for a new plant fails in merge() if the row is missing
Ask whether the plant is new, then whether it is managed. A saveOrUpdate() call becomes the first question written in code.

The method saveOrUpdate() has no single replacement, so we write the first question as a small method. It follows the same rule as Spring Data’s save().

public static Plant save(EntityManager em, Plant plant) {
  if (plant.getId() == null) {
    em.persist(plant);
    return plant;
  }
  return em.merge(plant);
}
Plant saved = PlantStore.save(em, aloe);   // id is null: insert into Plant ..., saved == aloe
aloe.setPrice(8.00);                       // aloe is detached after that transaction
saved = PlantStore.save(em, aloe);         // id is set: select ... where p1_0.id=?, saved != aloe
                                           // commit: update Plant set name=?,potSize=?,price=?,species=? where id=?

The method returns the managed object in both branches, so callers always continue with the result.

The complete project on GitHub contains the Hibernate 7 cases with 13 JUnit tests. Its hibernate6-legacy folder is a separate Maven project on Hibernate 6.6.58 that runs the old methods from section 1 with 15 tests (mvn -q compile exec:java, mvn test in each folder).

5. save(), update() and saveOrUpdate() FAQs

5.1. What Is the Difference Between save() and persist()?

Both inserted a new object, and persist() is the one that still exists. The other differences come from their origin, because save() was Hibernate’s own method, whereas persist() comes from Jakarta Persistence.

save() (removed in 7.0)persist()
ReturnsThe idvoid; read plant.getId()
Detached objectInserts a duplicate rowEntityExistsException
Id with no rowInserts under a new idEntityExistsException
Defined byHibernate SessionEntityManager and Session

With an IDENTITY id, both send the INSERT at the call.

5.2. What Is the Difference Between update() and merge()?

The method update() attached the object we passed, whereas merge() attaches a copy and returns it.

update() (removed in 7.0)merge()
ReturnsvoidThe managed copy
Our object afterwardsManagedStill detached
SQL for an unchanged plantUPDATE at commitSELECT only
Same id already loadedNonUniqueObjectExceptionCopies onto the loaded object
New object (id null)TransientObjectExceptionInserts a copy
Id with no rowOptimisticLockException at commitOptimisticLockException at the call

The common bug after migrating is a line like session.merge(plant); that ignores the result, followed by more changes to plant, so those changes are lost.

5.3. What Replaces saveOrUpdate() in Spring Data JPA?

The method CrudRepository.save() plays that role. The Spring Data JPA 4.1 implementation in SimpleJpaRepository calls persist() for a new entity and merge() for any other.

if (entityInformation.isNew(entity)) {
  entityManager.persist(entity);
  return entity;
}
return entityManager.merge(entity);

By default an entity is new when its non-primitive @Version field is null, or, without such a field, when its id is null. Always use the entity that save() returns, because for an existing entity it is the copy from merge().

5.4. Does merge() Always Run a SELECT?

No, merge() reads the row only when the persistence context does not hold that id yet.

SituationSQL at the merge() call
Detached plant, id 1 not loadedOne SELECT
Detached plant, id 1 already loaded with find()None
Managed plantNone
New plant (id null)INSERT (with IDENTITY)

6. Conclusion

The methods save(), update() and saveOrUpdate() are gone in Hibernate 7, and the replacements behave more safely. The method persist() saves a new object and rejects a detached one instead of inserting a duplicate row. Its counterpart merge() saves a detached object by copying it onto a managed instance, which we must use afterwards, and a managed entity needs no call at all. For saveOrUpdate(), a small method that picks persist() or merge() by the id does the same job, as Spring Data’s save() does.

7. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Dear Sir,

    While submitting the web form entitymanager.merge() is inserting data instead of updating.Why is it happening.

    Regards
    Vinay

  2. In Clusterred environment, we have 2 nodes. I have a job which runs on clustered env. Situation is , when this cron job started, using its status whether it is PENDING or InProgress , node will pick that job and start it.
    Node picks up the job on basis of status ie. if Status is PENDING then job will start by respective node. So here to avoid , job to be picked up by both the node, before start the job we, updated the status from Pending to In Progress.

    In current code, we first update Status=IN Progress and start the process but this change doesn’t get committed to DB till whole transaction gets completed and in between that 2nd node picks this job again and starts working on it.

    This situation puts job into deadlock.

    Is there any way in Hibernate Transaction where i can commit status before complete the transaction so other node can understand that this job is already in progress with first node.?

    • Hi Deepak, I am not aware of any such technique in hibernate. Though I have faced similar problem in one of projects, but we were using iBatis there so the solution does not apply here.

  3.  
    
    Session session = sessionFactory.openSession();
    Transaction tx = session.beginTransaction();
       
    for ( int i=0; i<100000; i++ ) {
        Customer customer = new Customer(.....);
        session.save(customer);
        if ( i % 20 == 0 ) { //20, same as the JDBC batch size
            //flush a batch of inserts and release memory:
            session.flush();
            session.clear();
        }
    }
       
    tx.commit();
    session.close();  
    

    In this example, for every 20 records data will be flushed i.e. data will be persisted in the database, if any record thrown error while processing any record after flush what will happen? as per the transaction concept whole activity i.e. saving 1000000 records should be rollback , but we already saved first 20 records in the database. How Transaction concept will work this scenario? please explain me in understanding session.flush vs tx.commit when we use together .

    • Well, you are right in your understanding. Ultimately, it all boils down to fact how you want to handle the error.

      I will put the code inside for-loop in a try-catch block, if any error is received I will try again to run that 20 statements, if succeeded – move ahead to next set of 20; if not then simply log them somewhere and continue processing next set of 20 records.

    • Hi Satish,

      You can also used try catch block. As Lokesh suggest. If you want to continue you can do something in catch block( mean get the index number where it stop and start again).

      If you want to stop execution and you want those 20 record should not be in database you can rollback the transaction in catch block.

      Thanks,
      Bhargav

  4. 2.SaveOrUpdate() calls either save() or update() on the basis of identifier exists or not. e.g if identifier does not exist, save() will be called or else update() will be called

    I am confused as ID is given when we save so it should not be vice- versa.
    i.e if identifier does not exist, update() will be called or else save() will be called ?

  5. I didn’t understand the contents of “Suggestion For Production Code” section. In this section you are telling to pass VO object instead of calling session.save() or session.saveOrUpdate(). What is the meaning of VO object here. we generally use saveOrUpdate() method. What’s wrong in using it explicitly. What’s the benefit of using VO object???

    • Probably in your application, VO and entities are same objects. In that case, your argument is valid.
      If both are different objects, then I “suggested” the way. How you guys do it?

      • We use AbstractDao layer or baseDao you can say. And we write DAO classes extending this abstract dao class. So all I understand that it removes boiler plate code. And after all we define these generic methods like saveOrupdate(), delete, findById(). So that we don’t have to write these methods in every DAO class. But I don’t know what you mean here using VO object. What is this term VO. Could you please elaborate it.

        • VO = View Objects; which you pass to your view layer. These are generally separate than DTO (or entities in this case) objects. You fetch data onto DTO, copy to VO and then return VO to controller. Controller passes VO to view later. Generally happen like this.

          Keeping DTO and VO separate helps in evolving view layer and data layer independently.

          • Yes, I think you are telling that sometime DTO objects are exactly as same as entity class and sometime some of the fields of DTO are different from entity class. For example One DTO or VO may contain data from two entity classes and we process it in service layer I mean transfer the values to entity object so that we can save or update in database. I don’t know if I understood the same what you meant or not. But what ever you are telling sounds good. Could you please provide some link or example where I can see this scenario implemented. I mean I can practically understand the benefit of using VO object. Thanks :-)

          • You got it right. Please note that what I call View Object, is called Domain Object [DO] as well by many others. If you search for “DO vs DTO”, you will find many good links.

            In short, most notifiable difference between them is that DO can have some view related business logic i.e. formatting field or validating it; whereas DTOs contain only setter and getter methods (well mostly).

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.