Overview
在本指南中,您可以学习;了解如何使用MongoDB Extension for Hibernate ORM 来执行乐观锁。乐观锁允许并发会话读取同一实体,然后在多个会话尝试写入该实体时检测到冲突。
当您打开乐观锁时,Hibernate ORM 扩展会为每个更新或删除操作生成MongoDB查询语言 (MQL)声明。除了主键之外,该声明还筛选会话读取的版本值。如果另一个会话首先修改了该文档,则过滤无法匹配任何文档,并且 Hibernate ORM 会抛出 OptimisticLockException。
Hibernate ORM 扩展不支持悲观锁。悲观锁会在会话读取实体时锁定该实体,这会阻止其他会话修改该实体,直到该会话释放锁。
添加版本字段
要为实体打开乐观锁,请在实体类中添加一个带有 @Version 注释的字段。
如果应用程序读取版本值,请为字段定义 getter,但不要定义 setter。 Hibernate ORM 自行分配该值,在应用程序代码中设置该字段可能会导致 Hibernate ORM 扩展生成与存储的文档不匹配的过滤。
本指南中的示例使用以下 Product 实体,该实体映射到 products集合。不要初始化 version字段,因为 Hibernate ORM 会在您首次持久化实体时为其分配初始值:
public class Product { private ObjectId id; private String name; private int quantity; 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; } }
Hibernate ORM 扩展支持以下 @Version字段类型:
int和Integerlong和LongInstant
数字版本字段从 0 开始,并在每次写入时按 1 递增。 Instant 版本字段存储写入时间。
更新版本化实体
当您持久保存新实体时,Hibernate ORM 会将版本字段设置为 0。当您修改实体并提交ACID 事务时,Hibernate ORM 扩展会生成一条更新声明,该语句会筛选会话读取的主键和版本值。该声明还设置新的版本值。
以下示例插入一个新的 Product 实体,然后更新其 quantity字段以递增版本:
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();
前面示例中的更新生成类似于以下MQL 的声明:
{ "update": "products", "updates": [ { "q": { "$and": [ { "_id": { "$eq": "..." } }, { "version": { "$eq": 0 } } ] }, "u": { "$set": { "quantity": 75, "version": 1 } } } ] }
删除操作使用相同的过滤。由于版本值是过滤的一部分,因此仅当文档仍包含会话读取的版本时,写入才会成功。
处理版本冲突
如果另一个会话在您的会话读取文档后对其进行了修改,则过滤中的版本值将不再与存储的值匹配,并且 Hibernate ORM 会抛出 OptimisticLockException。要恢复,请捕获异常并重新加载实体并重试写入,或者返回错误以通知应用程序的用户存在冲突。
以下示例使用两个会话来读取同一文档。第二个会话首先提交,因此第一个会话中的提交会抛出 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();
注意
根据应用程序加载实体的方式,Hibernate ORM 可能会抛出 StaleObjectStateException。此异常在 Hibernate ORM原生相当于 OptimisticLockException。
在不更改数据的情况下增加版本
要递增会话未修改的实体的版本,请将该实体和 LockMode.OPTIMISTIC_FORCE_INCREMENT锁模式传递给 Session 上的 lock() 方法。当对相关数据的更改必须使其他会话的实体副本失效时,请使用此锁模式。
以下示例递增 Product文档的版本:
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();
如果另一个会话首先递增了版本,则提交会抛出 OptimisticLockException。
不带版本字段的锁
您还可以在不添加版本字段的情况下检测冲突。为此,请使用 @OptimisticLocking 注释您的实体并指定以下类型之一:
OptimisticLockType.DIRTY:Hibernate ORM 扩展仅筛选会话修改的字段的先前值。OptimisticLockType.ALL:Hibernate ORM 扩展会筛选所有实体字段的先前值。
这两种类型都需要 @DynamicUpdate 注解。
以下示例使用 DIRTY 类型:
public class Product { private ObjectId id; private String name; private int quantity; // Constructors, getters, and setters }
要将字段从版本化实体中的乐观锁定中排除,请使用 @OptimisticLock(excluded = true) 注释该字段。对已排除字段的更改不会递增版本:
public class Product { private ObjectId id; private String name; private int quantity; private Long version; // Constructors, getters, and setters }
限制
乐观锁具有以下限制:
不能将具有
@Version字段的实体传递给StatelessSession.upsert()方法。 Hibernate ORM 扩展会为此操作抛出一个UnsupportedFeatureException。