动态创建Domain Class

最近,Grails用户组里正在热列地讨论着关于Form Builder插件的技术问题,该插件的目标是给Grails提供一种动态创建Form的解决方案。其中,不可避免地谈到了Form对应的Domain Class创建的问题。Burt Beckwith就这个难题进行一番技术探索之后,将他的结果张贴在了自己的Blog上

Burt承认自己的解决方案并不完美,因为Grails做了大量工作才将一个简单的Domain Class转换成一个完整的GORM类。其间涉及向Hibernate注册,装配动态方法,自动编译进去id、version、缺省的toString方法、以及对应hasMany声明的集合。此外,对于每个Domain Class,还要创建4个Spring Bean。总之是份苦差事!

在Burt看来,实现动态创建Domain Class的最大障碍在于向Hibernate注册新实体:

该过程[即注册新实体]被期望是在启动时完成,以后就永远不会改变。因此,SessionFactoryImpl里的数据字段绝大部分是private,以及2种final(注:普通的final和final transient)。故该解决方案相当具有黑客风格,涉及到暴力的反射。一旦使用反射,问题就迎刃而解了,final字段就只是个摆设。我创建了一个全新的SessionFactoryImpl(创建一个小的且只包含新的Domain Class的很方便,但这样就没法引用其他Domain Class了),用这个新类的数据替换实际的SessionFactoryImpl里的数据。我不能替换旧的SessionFactoryImpl,因为其他类会引用它。

整个解决方案包含4个类:

  • FakeApplication,它是DefaultGrailsApplication的子类,确保新类装配上MetaClass的方法。
  • GrailsDomainClassInjector,由于标准机制无法将新类识别成Domain Class,这个类提供了绕开这个限制的方法。
  • DynamicClassLoader,它将使用上面定义的Injector,然后编译Domain Class。
  • DynamicDomainService,它是解决方案的核心类,作用是动态编译和注册Domain Class。在这个类里,Burt先编译Groovy类,然后给它装配上所需的MetaClas方法,以及相关的Spring Bean。然后,就像他一开始说的,新建了一个SessionFactory,包含新创建的Domain Class。完成之后,利用ReflectionUtils把旧SessionFactory的数据用新的替换掉。

Burt给出了示例代码。首先是动态创建Domain Class:

    String bookCode = """
    package com.foo.testapp.book 
    class Book {
       String title
    }
    "
    "" 
    dynamicDomainService.registerNewDomainClass bookCode

    String authorCode = """
    package com.foo.testapp.author 
    import com.foo.testapp.book.Book
    class Author {
       static hasMany = [books: Book]
       String name
    }
    "
    "" 
    dynamicDomainService.registerNewDomainClass authorCode
    

然后使这些Domain Class的使用:

    def Book = grailsApplication.getClassForName('com.foo.testapp.book.Book')
    def Author = grailsApplication.getClassForName('com.foo.testapp.author.Author') 
    def author = Author.newInstance(name: 'Stephen King')
    author.addToBooks(Book.newInstance(title: 'The Shining'))
    author.addToBooks(Book.newInstance(title: 'Rose Madder'))
    author.save(failOnError: true)    
    

在文末,Burt道出了自己对于这个问题感兴趣的原委:

……我期望让Domain Class在开发过程中的重新加载更加友好些。当前情况下,如果你是在开发模式下运行的应用,只要你编辑了Domain Class,应用就会重启。这实在是让人烦心,想想你只是修改了一个对持久化根本没有影响的helper方法。因此,我期望能够比较编译前后的持久化元数据,如果没有变化,就只是象重新加载一个服务类或Controller那样去加载它。

另一方面,如果你的变化影响了持久化,而你是在create-drop模式下运行的,很明显就要重建Session Factory并且重新加载类,此外还要输出数据库模式,所有这些完全不用重新启动应用。对于其他情况我还不太确定如何处理,如运行在update模式或者根本没有设定dbCreate。希望Grails 1.4或2.0里能考虑进来。

本站先前曾经也有读者提过如何动态创建领域类的问题,Burt的解决办法对于该问题极具参考价值。但我依旧还是认为,仅仅只是动态创建领域类并非这个问题的全部内涵。从完整解决问题的角度来讲,它必然涉及到对于新创建的领域类如何消费的问题。典型的代表就是如何统计,如何查询。因此,当你做的项目,用户提出这种问题时,就需要小心了!

有兴趣的读者可以访问原文了解Burt的源代码,从中你可以知道很多关于Grails的秘密。

By foxgem - Posted on 18 十月 2010