db4o hack之dynabean(一)

问题:公司的一个遗留系统具有运行期修改domain字段定义的特性,背后的实现原理是数据库中有"Table"表,“Field”表以及对应的实体表等,需要修改domain字段定义的时候程序会把信息存入Table表和Field表,同时执行alter语句动态修改对应的实体表结构,并存入对应数据。而程序代码中并不含对应的java class,数据都是在运行期“组装”起来以XML格式输出。这种设计看起来似乎没什么问题,但在实际的维护中很让人不爽。经常出现的情况是,美国同事(非软件开发从业者)发邮件来要求某个页面添加一些字段,我收到邮件后以管理员身份登录系统,执行一系列繁琐的操作...往往要消耗我数小时的宝贵时间。自从07年接手这个系统以来,我已经忍受了整整3年了!实在忍受不了了,决定重新开发,尽早脱离苦海。于是上次我回邮件说:

Sam写道
It’s tedious for me, hard for you to do it. Create database tables -> Add fields and configure fields -> Attach tables to modules -> Set up modules access control -> Populate data -> Add fields that reference the tables created at the very beginning.(If the fields are study specific, there’s one more step of configuration)… Do you really want to know the how-to? I’m sure you’ll say NO! :-):-)

Before I make a silver bullet, I have to do it this way.
BTW, as CTMS is not well-designed, I’m developing a brand new CTMS(which is much more advanced and is what I call the silver bullet). With that silver bullet, such tasks can be finished in 1 minute with a couple of lines of code!
 

2周前正式开始开发新系统。最初的设想是,严格遵循DRY原则,数据模型的定义只存在于Domain Class中,采用0管理的面向对象数据库db4o - db4o内置了对domain class的重构支持。

 

可选的方案:

1. 每次需要更改domain class时,直接在IDE中修改,把编译后的classes上传到生产环境服务器,然后重新启动应用。

2. 利用JRebel,让它在运行期做到domain classes的热切换。

 

方案1的缺点是每次都得重启,已登录的用户需要重新登录,不太适合生产环境(尽管那个遗留系统有时也要重启让某些修改生效,尽管在用户保存数据的时候正好重启的几率很小,尽管。。。)

 

方案2让我们需要额外支付一笔钱购买JRebel的License,同时JRebel比较适合开发环境,生产环境下应该没人用吧?JRebel开发团队说,目前还没有开发适于生产环境的JRebel。就算JRebel在生产环境不影响应用的性能,我仍然需要执行编译+上传的额外操作,不能做到“分分钟搞掂”,心有不甘。当然可以在应用中植入自动编译引擎,省却手工编译的工作,但前提必须是“JRebel在生产环境下性能表现很好”。

 

作为完美主义者的我毫不犹豫抛弃了这两条方案。

......

 

Groovy的meta programming特性给了我一些启发。我设想:因为db4o是利用反射从我的domain classes中读取它的field信息的 - 相当于传统关系型数据库的“表结构”, 我能不能hack一把,写个客制化的反射器,让它从其它地方(比如配置文件或db4o数据库本身)读取field信息呢?我纵身投入Db4o的代码之海,在其中七十进七十出,寻得财宝若干。

 

基于设想+对db4o源代码的探索,我引入了dynabean( 不同于struts中的dynabean),以下就是一个dynabean:

 

package com.grs.sctms.dynabeans

import com.grs.ast.GrsDomainClass

@GrsDomainClass
class MonitorVisitTrack {}

它里面没有任何field的信息,实际上系统中所有的dynabean都长得像这个样子,只是类名各异而已。

在这里还涉及到CoC原则:我并没有使用@dynabean标注,而是所有在com.grs.sctms.dynabeans命名空间中的domain被认为是dynabean.

 

那么它的fields信息在哪里呢?在这儿:

package com.grs.sctms.metadata

import com.grs.ast.GrsDomainClass

@GrsDomainClass
class ClassMetadata {
    String cls
    String[] aspects // like ['String name [nullable:false, unique:true, validator:{v,o->v=~/grs\-cro\.com$/}]', 'Integer age']
    static constraints = {
      cls nullable:false, unique:true, validator: {cls,meta->cls.startsWith('com.grs.sctms.dynabeans.')}  
    }
}

这样我如果要新建或修改domain class MonitorVisitTrack的fields信息,就不需要修改 MonitorVisitTrack本身了。在web-based console中执行如下代码,将会保存MonitorVisitTrack类的fields信息(四个字段):

new ClassMetadata(
 cls: 'com.grs.sctms.dynabeans.MonitorVisitTrack'
,aspects: [
    'List<Personnel> monitors [nullable:false,validator:{v,o->v.size()>0}]'
    ,'Date dateVisitStarted [nullable:false]'
    ,'Date dateVisitEnded [nullable:false]'
    ,'MonitorVisitType visitType [nullable:false]']).save()

无需重启,即刻生效。

String类型的定义,如

'Date dateVisitStarted [nullable:false]'

将会在运行期被解析成db4o的FieldMetadata对象...

 

我试着保存了几条MonitorVisitTrack数据,用UltraEdit打开db4o的数据文件,有如下发现:

1. 好消息:那四个字段的值(我赋予的是NULL)真的被持久化到数据库了(四个NULL在UltraEdit十六进制模式下依稀可辨)!

2. 坏消息:没有成功读取(OM中根本没有显示那四个字段)!

 

未完待续...