巧的是,在Prevayler的实现中,强调的也是事务:它要求开发者设计可以序列化的“事务对象”,利用Java的序列化机制实现持久,而所有对象的目前状态则保持在内存中。
三年以前我开始看Prevayler时,觉得它的这种“事务对象”不好理解,别扭,不是我喜欢的东西。在读了彩色UML建模之后,我理解了这种持久设计。下面以银行账户转账的例子来说明我的理解(注意,这个例子是虚构的,不存在于任何真实的系统中。如有雷同,纯属巧合):
业务模型
关于这个业务模型,有两点说明:
- 按照“5Ws and an H”的方式设计。MoneyTransferring本身记录了转账的金额和时间,说明了这是个什么事(What),何时发生(When)。MeansDesc是转账方式描述,可能是通过ATM机、柜台、电话银行等方式完成这次转账,说明了发生方式(How)。Terminal说明这次交易在哪里发生(Where)。Staff和Customer说明了参与这次转账的人(Who)。转账的理由(Why)则因为没有很合适的分类方法,可能作为MoneyTransferring的一个字符串属性。
- 所有的类,都可以作为分类统计的依据。例如,我们可以根据一个账户的MoneyTransferring事务和存入、取出事务,确定这个账户的活跃程度。或根据MeansDesc来确定用户对转账方式的偏好。“5Ws and an H”都可以在未来成为查询和统计的条件。
持久设计
持久层设计为一个接口,其中的一个方法实现转账事务:
PersistentLayer.transferMoney(MoneyTransfering mt) ;
如果以关系数据库的方式来实现持久,这个事务将包含3条SQL语句:一条插入(INSERT)和两条更新(UPDATE)。更新必然是用原来的账户余额加上或减去转账的金额。数据库事务将保证这个操作的ACID。如果转出账户余额不足,且数据库设置了约束,余额字段不能为负,这个事务将失败。
但是,我们在开发过程中还可以临时使用内存的持久方式,实现一个“不持久的”持久层。这个持久层实现只是简单地将MoneyTransfering对象插入到一个全局的List中,并假定业务层已经将相应的两个账户做了改动。
这两种实现在对业务层的假定方面有一个细微的区别。第一种SQL实现只关注传入的MoneyTransfering对象拖着的两个Account对象的ID,而不关心它们的余额。第二种内存实现则假定业务层已经处理好了它们的余额。如果我们只用内存实现来做一些不严肃的测试,这样也未偿不可,但严格来说,我们对持久层接口的契约理解上,出现了偏差。
所以,正确的做法是,业务层不负责改动账户余额,全部交由持久层实现来完成。这样内存持久实现将做3件事,一次插入和两次更新。这下和数据库持久的语义一致了。
之所以费这么大劲讨论内存持久对PersistentLayer接口语义的支持,是因为有Prevayler的存在。它把“内存+对象序列化”做为一种正式的持久方式,用于生产系统中。如果我们写一个PersistentLayerPrevaylerImpl,那么这个实现对这个事务的处理是:将MoneyTransfering对象插入全局的List,更新两个账户余额,将MoneyTransfering对象通过序列化机制持久到硬盘。内存里保存的是最新的数据快照,持久存储上存放了所有数据变动的历史。这和HSQL是一样的道理。
您可能听说过DB4O,或者听说过BerkeleyDB,还可能听说过Tokyo Cabinet。在云计算时代,我们有很多的持久机制可以选择。为什么不把业务代码设计得灵活一些,可以适应不同的持久机制呢?多维护一个PersistentLayer接口的开销并不算大。
小结
- 我们用“5Ws and an H”的方式对业务事件建模。
- “5Ws and an H”成为业务数据查询和统计的条件。
- 通过PersistentLayer接口来统一持久层语义。
- 根据不同目的和环境,采用不同的持久层实现。
- 云计算时代的持久不同于C/S时代的持久。