JPA乐观锁规范对客户端提供版本号的更新场景如何规定?
Great question! This digs into some nuanced details of JPA's optimistic locking specification and how different ORM implementations handle edge cases. Let's break down the spec requirements, your two update approaches, and why Hibernate and DataNucleus behave differently.
1. Core JPA Rules for Optimistic Locking
JPA's @Version annotation enforces optimistic locking with one key rule: when updating or deleting a managed entity (one tracked by the EntityManager), the ORM must compare the entity's version value to the version stored in the database. If they don't match, an OptimisticLockException must be thrown.
The critical phrase here is "managed entity"—this refers to an entity loaded into or tracked by the persistence context.
2. Why Your Two Update Behaviors Differ
Let's unpack what's happening under the hood with each approach:
Approach 1: Create a New Object and Use merge()
public MyEntity update(EntityManager em, int id, String name, int version) { MyEntity entity = new MyEntity(); entity.setId(id); entity.setName(name); entity.setVersion(version); return em.merge(entity); }
Here's how merge() works step-by-step:
- It checks if the persistence context already has a managed entity with the matching ID.
- If not, it loads the current state of the entity from the database (converting it to managed status).
- It copies all properties (including your manually set
version) from your detached, new object to the managed entity. - When the transaction commits, Hibernate compares the managed entity's version (which you set) to the database's stored version. If they don't align, it throws
OptimisticLockException—this aligns with JPA's intent, because you're explicitly stating "I expect to update version X of this entity."
Approach 2: Load the Entity and Update Fields Directly
public MyEntity update(EntityManager em, int id, String name, int version) { MyEntity entity = em.find(MyEntity.class, id); entity.setName(name); entity.setVersion(version); return entity; }
In this case, em.find() returns a managed entity loaded directly from the database. When you manually overwrite its version field, you're bypassing the ORM's automatic version management.
Hibernate 5.3 treats this as an explicit user override: it assumes you want to set the version to a specific value, not use that value for optimistic lock validation. So when it generates the update SQL, it doesn't compare your manually set version to the database's version—it just updates the name and sets the version to your provided value. This is an implementation choice, not a bug, because JPA doesn't explicitly define how to handle manual @Version modifications.
3. Why DataNucleus Behaves Differently
DataNucleus 5.1.9 doesn't throw exceptions in either scenario because it interprets manual version overrides the same way across both approaches: it treats your provided version as the new value to store, not as a validation check against the database's current version.
This is another valid implementation choice under JPA's ambiguous wording around manual @Version changes. While it deviates from Hibernate's behavior, it's not strictly a violation of the spec—since the spec doesn't explicitly mandate how to handle users manually modifying the version field.
4. The "Correct" Way to Enforce Optimistic Locking
If you want to strictly follow JPA's optimistic locking intent regardless of ORM, you should explicitly validate the version before updating:
public MyEntity update(EntityManager em, int id, String name, int version) { MyEntity entity = em.find(MyEntity.class, id); // Manually validate the version matches your expectation if (entity.getVersion() != version) { throw new OptimisticLockException("Entity has been updated by another transaction"); } entity.setName(name); // Let the ORM automatically increment the version (no need to set it manually) return entity; }
This approach guarantees that you're only updating the version of the entity you expect, and works consistently across all JPA-compliant ORMs.
内容的提问来源于stack exchange,提问作者Sevas

