Hibernate实现贯穿三层的乐观锁
无论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写入对象作为一个整体进行并发控制。考虑以下步骤:
?
?
以上步骤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);}?
?
抽象类
?
@MappedSuperclasspublic 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,不知道在后面的版本中是否会修复此问题。
?
?
?