For AI agents: a documentation index is available at https://www.mongodb.com/docs/llms.txt — markdown versions of all pages are available by appending .md to any URL path.
See how MongoDB 9.0 delivers up to 2x higher throughput.
MongoDB Branding Shape
Register now >
Docs Menu

Prevent Conflicting Updates with Optimistic Locking

In this guide, you can learn how to use the MongoDB Extension for Hibernate ORM to perform optimistic locking. Optimistic locking allows concurrent sessions to read the same entity, then detects a conflict when more than one session tries to write to that entity.

When you turn on optimistic locking, the Hibernate ORM extension generates a MongoDB Query Language (MQL) statement for each update or delete operation. The statement filters on the version value that the session read, in addition to the primary key. If another session modified the document first, the filter matches no document, and the Hibernate ORM throws an OptimisticLockException.

The Hibernate ORM extension does not support pessimistic locking. Pessimistic locking locks an entity when a session reads it, which blocks other sessions from modifying the entity until the session releases the lock.

To turn on optimistic locking for an entity, add a field annotated with @Version to the entity class.

Define a getter for the field if your application reads the version value, but don't define a setter. The Hibernate ORM assigns the value itself, and setting the field in your application code might cause the Hibernate ORM extension to generate a filter that doesn't match the stored document.

The examples in this guide use the following Product entity, which maps to the products collection. Don't initialize the version field, because the Hibernate ORM assigns its initial value when you first persist the entity:

@Entity
@Table(name = "products")
public class Product {
@Id
@ObjectIdGenerator
private ObjectId id;
private String name;
private int quantity;
@Version
private Long version;
public Product() {
}
public Product(String name, int quantity) {
this.name = name;
this.quantity = quantity;
}
public ObjectId getId() {
return id;
}
public Long getVersion() {
return version;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

The Hibernate ORM extension supports the following types for a @Version field:

  • int and Integer

  • long and Long

  • Instant

A numeric version field starts at 0 and increments by 1 on each write. An Instant version field stores the time of the write.

When you persist a new entity, the Hibernate ORM sets the version field to 0. When you then modify the entity and commit the transaction, the Hibernate ORM extension generates an update statement that filters on both the primary key and the version value that the session read. The statement also sets the new version value.

The following example inserts a new Product entity, then updates its quantity field to increment the version:

var sf = HibernateUtil.getSessionFactory();
try (Session session = sf.openSession()) {
// Insert a new product, which sets the version to 0
session.beginTransaction();
var product = new Product("notebook", 100);
session.persist(product);
session.getTransaction().commit();
System.out.println("Version after insert: " + product.getVersion());
// Update the same product, which increments the version to 1
session.beginTransaction();
product.setQuantity(75);
session.getTransaction().commit();
System.out.println("Version after update: " + product.getVersion());
}
sf.close();

The update in the preceding example generates a statement similar to the following MQL:

{
"update": "products",
"updates": [
{
"q": { "$and": [ { "_id": { "$eq": "..." } }, { "version": { "$eq": 0 } } ] },
"u": { "$set": { "quantity": 75, "version": 1 } }
}
]
}

Delete operations use the same filter. Because the version value is part of the filter, a write succeeds only if the document still holds the version that the session read.

If another session modifies a document after your session reads it, the version value in your filter no longer matches the stored value, and the Hibernate ORM throws an OptimisticLockException. To recover, catch the exception and either reload the entity and retry the write, or return an error that notifies your application's user of the conflict.

The following example uses two sessions to read the same document. The second session commits first, so the commit in the first session throws an OptimisticLockException:

var sf = HibernateUtil.getSessionFactory();
ObjectId productId;
// Insert a new product for two sessions to modify
try (Session session = sf.openSession()) {
session.beginTransaction();
var product = new Product("notebook", 100);
session.persist(product);
session.getTransaction().commit();
productId = product.getId();
}
try (Session sessionA = sf.openSession(); Session sessionB = sf.openSession()) {
// Both sessions load the product at version 0
var productA = sessionA.find(Product.class, productId);
var productB = sessionB.find(Product.class, productId);
// Session B commits first and increments the version to 1
sessionB.beginTransaction();
productB.setQuantity(75);
sessionB.getTransaction().commit();
// Session A commits second, but still holds version 0
try {
sessionA.beginTransaction();
productA.setQuantity(50);
sessionA.getTransaction().commit();
} catch (OptimisticLockException e) {
System.out.println("Update failed: Another session modified this document");
// Reload the entity and retry, or report the conflict to the user
}
}
sf.close();

Note

Depending on how your application loads the entity, the Hibernate ORM might throw a StaleObjectStateException instead. This exception is the Hibernate ORM native equivalent of OptimisticLockException.

To increment the version of an entity that your session did not modify, pass the entity and the LockMode.OPTIMISTIC_FORCE_INCREMENT lock mode to the lock() method on your Session. Use this lock mode when a change to related data must invalidate other sessions' copies of the entity.

The following example increments the version of a Product document:

var sf = HibernateUtil.getSessionFactory();
try (Session session = sf.openSession()) {
session.beginTransaction();
var product = session.createQuery("from Product where name = :n", Product.class)
.setParameter("n", "notebook")
.setMaxResults(1)
.getSingleResult();
session.lock(product, LockMode.OPTIMISTIC_FORCE_INCREMENT);
session.getTransaction().commit();
}
sf.close();

If another session incremented the version first, the commit throws an OptimisticLockException.

You can also detect conflicts without adding a version field. To do so, annotate your entity with @OptimisticLocking and specify one of the following types:

  • OptimisticLockType.DIRTY: The Hibernate ORM extension filters on the previous values of only the fields that the session modified.

  • OptimisticLockType.ALL: The Hibernate ORM extension filters on the previous values of all the entity's fields.

Both types require the @DynamicUpdate annotation.

The following example uses the DIRTY type:

@Entity
@Table(name = "products")
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
public class Product {
@Id
@ObjectIdGenerator
private ObjectId id;
private String name;
private int quantity;
// Constructors, getters, and setters
}

To exclude a field from optimistic locking in a versioned entity, annotate the field with @OptimisticLock(excluded = true). Changes to an excluded field do not increment the version:

@Entity
@Table(name = "products")
public class Product {
@Id
@ObjectIdGenerator
private ObjectId id;
@OptimisticLock(excluded = true)
private String name;
private int quantity;
@Version
private Long version;
// Constructors, getters, and setters
}

Optimistic locking has the following limitations:

  • The Hibernate ORM extension does not support pessimistic locking. To learn more about locking support, see the Transactions and Concurrency section of the Feature Compatibility page.

  • You cannot pass an entity that has a @Version field to the StatelessSession.upsert() method. The Hibernate ORM extension throws an UnsupportedFeatureException for this operation.

To learn more about optimistic locking, see Optimistic Locking in the Hibernate ORM documentation.

To learn more about running write operations in a transaction, see the Transactions and Sessions guide.