无论Hibernate还是Toplink,都支持乐观锁机制。在Toplink中实现贯穿3层的乐观锁很容易,但Hibernate缺省不支持三层环境下的乐观锁,为了实现这个功能,我费了一番功夫。
所谓乐观锁,是指在实体上增加一个字段 version (Hibernate目前只支持int,Toplink可以是long),提交实体时,采用这样的update语句
update T set a=xx, version=1 where id=1 and version=0
根据返回结果中修改记录的条数来判断是否修改了版本正确的对象,如果条数为0标示此id的对象版本已被修改,抛出异常。
Hibernate的实现只考虑到了两层(Server端)的乐观锁,只能作到Server端读出到写入对象这段间隔内的并发控制。实际上,我们需要并发控制的间隔往往更长。在三层系统内,我们尝尝要将Client读取对象到Server写入对象作为一个整体进行并发控制。考虑以下步骤:
- Client 请求对象 id=1
- Server 读取数据库,返回[id=100,value='abc',version=1]
- Client 进行展示,用户在界面上将 'abc' 改为 'xyz'
- Client 传回数据,要求将[id=100, version=1]的对象值修改为'xyz'
- Server 开启事务,读取数据库中 id=100的对象,将value设为'xyz'
- Server 提交事务
以上步骤1~6应该作为一个整体,由于用户是根据[version=1]的对象状态作出将 value改为'xyz'的操作,所以从用户看到数据到最后提交的过程中,这个对象都不能有修改,不然可能在用户不知情的状况下覆盖掉别人的修改。
在Toplink中要实现以上的并发控制,只需要在步骤5中读出id=100的对象后, setVersion(1),最后的SQL中version取值就是1
但在Hibernate中,setVersion是不起作用的。Hibernate最后生成的SQL语句中where version= xx 中的取值是对象从session中被读出来的初始值,而不是对象对象被提交时的值。所以如果读出对象时version=2,即使手工serVersion(1),最后的SQL中version取值还是2,也就不会抛异常。
以上,Hibernate的实现只能防止步骤5、6之间的并发修改,不能防止1~4之间数据被修改。我们知道,前四个步骤的间隔远大于后两个步骤,如果不能解决这个问题,Hibernate的乐观锁的实用价值就不大了。
奇怪的是,这应该是个很普遍的问题,但在网上搜到的问题和方案却不多,stackoverflow上给了个方法也不管用(后面会提到),只能自己想办法。
首先想到的是Hijack源码,把生成SQL时的语句改成取最新的Version,这应该是最根本的方法。但公司对jar包管理较严,所以不可行。
其次是stackoverflow上的方法,写一个HibernateIntecetor,在onFlushDirty中判断新旧version不一样则手工抛异常,这个方法也不可行,因为Hibernate自己更新对象时也会去修改objectVersion并触发onFlushDirty,同样的问题也存在于setVersion(int version)方法中,如果在setVerison里进行判断,由于我们的mapping annotation配置在property上,初始化对象时会调用这个setter把version从0改成数据库里的值,一样会抛出不该抛的异常。
总结一下,既然不能修改最后拿version的方法(修改源码),只能在修改version时进行判断(interceptor,setter);但Hibernate进行的修改不应该抛异常,只对所有手工进行的不一致的修改抛异常,该怎么办呢?
我的解决方法是对version修改给出两个函数,一个是暴露出去的resetVersion,供手工修改,在这个方法类进行判断并抛异常;另一个是内部的setter,供Hibernate内部使用,这个setter不会进行判断。为了防止内部setter被误调用,将其设为default access
接口
public interface OptimisticLockSupported {
String PROPERTY_OBJECT_VERSION = "objectVersion";
int getObjectVersion();
/**
* Public Setter of objectVersion, child classes should throw
* StaleObjectStateException if objectVersion is changed.
*/
void resetObjectVersion(int objectVersion);
}
抽象类
@MappedSuperclass
public abstract class AbstractOptimisticLockSupportedEntity implements OptimisticLockSupported, Serializable {
/**
*
*/
private static final long serialVersionUID = 128597519171260732L;
private int objectVersion;
@Version
public int getObjectVersion() {
return objectVersion;
}
/**
* Setter of obejctVersion, for Hibernate only.
*
* @param objectVersion
*/
void setObjectVersion(int objectVersion) {
this.objectVersion = objectVersion;
}
/**
* child classes should override this function and throw
* StaleObjectStateException if objectVersion is changed. Do not implement
* here because constructor of UnsupportedOperationException need id which
* does not exist in this class.
*/
public void resetObjectVersion(int objectVersion) {
throw new UnsupportedOperationException();
}
}
再下一层的AbstractIdEntity中重载resetObjectVersion
/**
* Implement {@code resetObjectVersion(int objectVersion)} here because
* constructor of {@code StaleObjectStateException} need id which does not
* exist in {@code OptimisticLockSupported}.
* <p>
* In the implementation, any change of objectVersion will throw
* StaleObjectStateException, below is an example:
* <p>
* id (say 100) and objectVersion (say 1) are passed from client to server,
* as a process in server side, you may
* <ol>
* <li>Find entity (say user) by id (100)
* <li>Reset object version of user to 1 by invoke
* {@code user.resetObjectVersion(1L);}
* </ol>
* If the current object version of user is 1, everything will be OK. But if
* it is 2, a StaleObjectStateException will be thrown to indicate that user
* has been modified since it was sent to client.
*
*/
@Override
public void resetObjectVersion(int objectVersion) {
if (getObjectVersion() != objectVersion) {
throw new StaleObjectStateException(getClass().getName(), getId());
}
setObjectVersion(objectVersion);
}
这样作,算是解决了此问题,但相比Toplink,整体上很不优雅,不是一个total solution。
我用的Hibernate是3.3.2.GA,不知道在后面的版本中是否会修复此问题。