问题描述
在数据更新操作中,对实体的version字段进行修改,明显低于数据库version,但是却可以正常保存,并忽略了用户设置的version,直接递增了最新的version。
解决方法
- 将本次操作的查询与保存分事务处理,不要放在一个事务中。
- 事务保持不变,不用查询出来的实体进行set操作,new一个实体,将查出的实体与请求提交的实体合并,然后保存。
详细经过
如更新一个实体的名称字段
@Transaction
void update(long id, String name, int version){
T entity = repository.findById(id);//{id: 1, name:"11", version: 10}
entity.setName(name);
entity.setVersion(version);//{id: 1, name:"22", version: 8}
repository.save(entity);
//保存成功,保存后的实体:{id: 1, name:"22", version: 11}
}
update(1,"22",8);
低版本的实体成功保存, jpa跳过了乐观锁检测。
产生原因
JPA/Hibernate中的实体有四种状态:
org.hibernate.event.internal.EntityState{
PERSISTENT,//托管状态
TRANSIENT,//瞬时状态
DETACHED,//游离状态
DELETED;//删除状态
}
当处于一个事务中时, 查询出的实体将一直处于PERSISTENT状态,对PERSISTENT状态下的实体进行修改将不会检测乐观锁,手动setVersion也会被忽略。
所以要让实体不再处于PERSISTENT状态,就可以得到解决。
当查询与保存不在同一事务时,实体由于查询事务的结束,将变成DETACHED状态,这对JPA来说,该实体是不可信的,所以对实体保存时,JPA将通过主键再次查询数据库,进行数据比对,这也包括乐观锁的检测。
同样new一个实体时,也不会处于PERSISTENT状态, 这也可以迫使JPA进行乐观锁检测。
JPA/Hibernate的具体代码实现:
org.hibernate.event.internal.DefaultMergeEventListener{
public void onMerge(MergeEvent event, Map copiedAlready){
//获取实体上下文,比对得到实体状态
...
switch(entityState) {
case DETACHED:
this.entityIsDetached(event, copyCache);
break;
case TRANSIENT:
this.entityIsTransient(event, copyCache);
break;
case PERSISTENT:
this.entityIsPersistent(event, copyCache);
break;
}
}
protected void entityIsDetached(MergeEvent event, Map copyCache) {
...
if (this.isVersionChanged(entity, source, persister, target)) {
StatisticsImplementor statistics = source.getFactory().getStatistics();
if (statistics.isStatisticsEnabled()) {
statistics.optimisticFailure(entityName);
}
throw new StaleObjectStateException(entityName, id);
}
...
}
protected void entityIsPersistent(MergeEvent event, Map copyCache) {
LOG.trace("Ignoring persistent instance");
Object entity = event.getEntity();
EventSource source = event.getSession();
EntityPersister persister = source.getEntityPersister(event.getEntityName(), entity);
((MergeContext)copyCache).put(entity, entity, true);
this.cascadeOnMerge(source, persister, entity, copyCache);
this.copyValues(persister, entity, entity, source, copyCache);
event.setResult(entity);
}
}