SpringOne 2GX 2011

Chicago, October 25-28, 2011

Dynamic GORM Domain Classes

Posted by: Burt Beckwith on 2010-10-13 03:10:00.0

A recent discussion on the Grails Dev mailing list about creating a dynamic form builder involved needing to compile new domain classes at runtime. The consensus seemed to be that it's not possible and/or not advisable, but I've thought a lot about this topic and had done similar work when creating the Dynamic Controller plugin, so I started playing with it.

The solution I came up with isn't pretty but seems to work. There were a few issues to tackle. One is that Grails does quite a bit of work to convert your relatively simple domain classes into full GORM classes, registered with Hibernate and wired up with validation, convenience MetaClass methods, etc. There's also the issue of automatically compiling in an id and version field, a default toString() method, and collections corresponding to hasMany declarations. In addition there are four Spring beans created for each domain class. There's a lot being done under the hood that we tend to take for granted.

But the big hurdle is registering the new entity with Hibernate. It's expected that this is done at startup and never changed, so the data fields in SessionFactoryImpl are mostly private and in two cases final. So the solution is rather hackish and involves brute force reflection. It just so happens that when using reflection, final fields are only mostly final. So I create a whole new SessionFactoryImpl (it'd be convenient to create a small one with just the new domain class, but then you couldn't reference other domain classes) and replace the real SessionFactoryImpl's data with the data from the new one. I can't replace the SessionFactoryImpl since other classes will have a reference to the previous one.

The primary class is DynamicDomainService, although this could certainly be in a helper class in src/groovy:

package com.burtbeckwith.dynamicdomain

import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.grails.commons.ApplicationHolder as AH
import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler
import org.codehaus.groovy.grails.commons.GrailsDomainClass
import org.codehaus.groovy.grails.compiler.injection.ClassInjector
import org.codehaus.groovy.grails.compiler.injection.DefaultGrailsDomainClassInjector
import org.codehaus.groovy.grails.compiler.injection.GrailsAwareClassLoader
import org.codehaus.groovy.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean
import org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin
import org.codehaus.groovy.grails.plugins.orm.hibernate.HibernatePluginSupport
import org.codehaus.groovy.grails.validation.GrailsDomainClassValidator

import org.springframework.beans.factory.config.MethodInvokingFactoryBean
import org.springframework.beans.factory.config.RuntimeBeanReference
import org.springframework.beans.factory.support.AbstractBeanDefinition
import org.springframework.beans.factory.support.GenericBeanDefinition
import org.springframework.util.ReflectionUtils

/**
 * Compiles and registers domain classes at runtime.
 *
 * @author <a href='mailto:[email protected]'>Burt Beckwith</a>
 */

class DynamicDomainService {

   static transactional = false

   void registerNewDomainClass(String code) {
      def application = AH.application
   
      def clazz = compile(code, application)

      // register it as if it was a class under grails-app/domain
      GrailsDomainClass dc = application.addArtefact(
            DomainClassArtefactHandler.TYPE, clazz)

      def ctx = application.mainContext

      registerBeans ctx, dc

      wireMetaclass ctx, dc

      updateSessionFactory ctx
   }

   private Class compile(String code, application) {
      Class clazz = new DynamicClassLoader().parseClass(code)
      application.classLoader.setClassCacheEntry clazz // TODO hack
      clazz
   }

   // this is typically done in DomainClassGrailsPlugin.doWithSpring
   private void registerBeans(ctx, GrailsDomainClass dc) {

      ctx.registerBeanDefinition dc.fullName,
         new GenericBeanDefinition(
            beanClass: dc.clazz,
            scope: AbstractBeanDefinition.SCOPE_PROTOTYPE,
            autowireMode:AbstractBeanDefinition.AUTOWIRE_BY_NAME)

      GenericBeanDefinition beanDef = new GenericBeanDefinition(
         beanClass: MethodInvokingFactoryBean,
         lazyInit: true)
      setBeanProperty beanDef, 'targetObject',
            new RuntimeBeanReference('grailsApplication', true)
      setBeanProperty beanDef, 'targetMethod', 'getArtefact'
      setBeanProperty beanDef, 'arguments',
            [DomainClassArtefactHandler.TYPE, dc.fullName]
      ctx.registerBeanDefinition "${dc.fullName}DomainClass", beanDef

      beanDef = new GenericBeanDefinition(
         beanClass: MethodInvokingFactoryBean,
         lazyInit: true)
      setBeanProperty beanDef, 'targetObject',
            new RuntimeBeanReference("${dc.fullName}DomainClass")
      setBeanProperty beanDef, 'targetMethod', 'getClazz'
      ctx.registerBeanDefinition "${dc.fullName}PersistentClass", beanDef

      beanDef = new GenericBeanDefinition(
         beanClass: GrailsDomainClassValidator,
         lazyInit: true)
      setBeanProperty beanDef, 'messageSource',
            new RuntimeBeanReference('messageSource')
      setBeanProperty beanDef, 'domainClass',
            new RuntimeBeanReference("${dc.fullName}DomainClass")
      setBeanProperty beanDef, 'grailsApplication',
            new RuntimeBeanReference('grailsApplication', true)
      ctx.registerBeanDefinition "${dc.fullName}Validator", beanDef
   }

   private void setBeanProperty(GenericBeanDefinition bean,
            String name, value) {
      bean.propertyValues.addPropertyValue name, value
   }

   private void wireMetaclass(ctx, GrailsDomainClass dc) {
      def fakeApplication = new FakeApplication(dc)
      DomainClassGrailsPlugin.enhanceDomainClasses(
            fakeApplication, ctx)
      HibernatePluginSupport.enhanceSessionFactory(
            ctx.sessionFactory, fakeApplication, ctx)
   }

   // creates a new session factory so new classes can
   // reference existing, and then replaces the data in
   // the original session factory with the new combined data
   private void updateSessionFactory(ctx) {
      def sessionFactoryBean = ctx.getBean('&sessionFactory')
      def newSessionFactoryFactory = new ConfigurableLocalSessionFactoryBean(
         dataSource: ctx.dataSource,
         configLocations: getFieldValue(
               sessionFactoryBean, 'configLocations'),
         configClass: getFieldValue(sessionFactoryBean, 'configClass'),
         hibernateProperties: getFieldValue(
               sessionFactoryBean, 'hibernateProperties'),
         grailsApplication: ctx.grailsApplication,
         lobHandler: getFieldValue(sessionFactoryBean, 'lobHandler'),
         entityInterceptor: getFieldValue(
               sessionFactoryBean, 'entityInterceptor'))

      newSessionFactoryFactory.afterPropertiesSet()

      def newSessionFactory = newSessionFactoryFactory.object
     
      ['entityPersisters', 'collectionPersisters', 'identifierGenerators',
       'namedQueries', 'namedSqlQueries', 'sqlResultSetMappings',
       'imports', 'collectionRolesByEntityParticipant',
       'classMetadata', 'collectionMetadata'].each { fieldName ->
         def field = ReflectionUtils.findField(
               ctx.sessionFactory.getClass(), fieldName)
         field.accessible = true
         field.set ctx.sessionFactory, new HashMap(
               field.get(newSessionFactory))
      }
   }

   private getFieldValue(sessionFactoryBean, String fieldName) {
      def field = ReflectionUtils.findField(
               sessionFactoryBean.getClass(), fieldName)
      field.accessible = true
      field.get sessionFactoryBean
   }
}

This depends on a helper class that extends DefaultGrailsApplication and is used to ensure that only the new class gets MetaClass methods wired up:

package com.burtbeckwith.dynamicdomain

import org.codehaus.groovy.grails.commons.ApplicationHolder as AH
import org.codehaus.groovy.grails.commons.DefaultGrailsApplication
import org.codehaus.groovy.grails.commons.GrailsDomainClass

/**
 * This is needed when calling DomainClassGrailsPlugin.enhanceDomainClasses()
 * and HibernatePluginSupport.enhanceSessionFactory() so they only act
 * on the new class.
 *
 * @author Burt
 */

class FakeApplication extends DefaultGrailsApplication {

   final domainClasses

   FakeApplication(GrailsDomainClass dc) {
      super([dc.clazz] as Class[], AH.application.classLoader)
      domainClasses = [dc]
   }
}

We also need a custom GrailsDomainClassInjector since the standard mechanism doesn't recognize the new classes as domain classes:

package com.burtbeckwith.dynamicdomain

import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.grails.compiler.injection.DefaultGrailsDomainClassInjector

/**
 * Works around the fact that the class is dynamically
 * compiled and not from a file.
 *
 * @author <a href='mailto:[email protected]'>Burt Beckwith</a>
 */

class DynamicDomainClassInjector extends DefaultGrailsDomainClassInjector {

   // always true since we're only compiling dynamic domain classes
   @Override
   boolean shouldInject(URL url) { true }

   // always true since we're only compiling dynamic domain classes
   @Override
   protected boolean isDomainClass(ClassNode cn, SourceUnit su) { true }
}

Finally we need a custom classloader that uses the custom injector:

package com.burtbeckwith.dynamicdomain

import java.security.CodeSource

import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.Phases
import org.codehaus.groovy.grails.commons.ApplicationHolder
import org.codehaus.groovy.grails.compiler.injection.ClassInjector
import org.codehaus.groovy.grails.compiler.injection.GrailsAwareClassLoader
import org.codehaus.groovy.grails.compiler.injection.GrailsAwareInjectionOperation

/**
 * Uses a custom injector.
 *
 * @author <a href='mailto:[email protected]'>Burt Beckwith</a>
 */

class DynamicClassLoader extends GrailsAwareClassLoader {

   private final ClassInjector[] _classInjectors = [
         new DynamicDomainClassInjector()]

   DynamicClassLoader() {
      super(ApplicationHolder.application.classLoader,
            CompilerConfiguration.DEFAULT)
      classInjectors = _classInjectors
   }

   @Override
   protected CompilationUnit createCompilationUnit(
            CompilerConfiguration config, CodeSource source) {
       CompilationUnit cu = super.createCompilationUnit(config, source)
       cu.addPhaseOperation(new GrailsAwareInjectionOperation(
          getResourceLoader(), _classInjectors), Phases.CANONICALIZATION)
       cu
   }
}

Here's an example demonstrating usage. First compile the Book class:

String bookCode = """
package com.foo.testapp.book

class Book {
   String title
}
"""

dynamicDomainService.registerNewDomainClass bookCode

and then the Author class:

String authorCode = """
package com.foo.testapp.author

import com.foo.testapp.book.Book

class Author {

   static hasMany = [books: Book]

   String name
}
"""

dynamicDomainService.registerNewDomainClass authorCode

Then you can load the classes dynamically and persist new instances:

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)

Note that you can't use new since the classes are compiled dynamically, so use newInstance instead.


The reason I've been interested in this idea is that I'd like to make domain class reloading during development more friendly. Currently if you edit a domain class while running in dev mode with run-app, the application restarts. This is frustrating if you only made a change in a helper method that has no impact on persistence though. So I'm hoping to be able to diff the previous persistence metadata with the data after recompiling, and if there's no difference, just reload the class like a service or controller.

On the other end of the spectrum, if you do make a change affecting persistence but you're running in create-drop mode, it should be straightforward to rebuild the session factory and reload the class, plus re-export the schema, all without restarting the application. I'm not sure yet what to do with the other cases, e.g. when running in update mode or with no dbCreate set at all. Hopefully this will make it into Grails 1.4 or 2.0.


You can download the referenced code here.


About Burt Beckwith

Burt Beckwith

Burt Beckwith is a Java and Groovy developer with over ten years of experience in a variety of industries including biotech, travel, e-learning, social networking, and financial services. For the past three years he's been working with Grails and Groovy full-time. Along the way he's created over fifteen Grails plugins and made significant contributions to several others. He was the technical editor for Grails in Action.

More About Burt »

NFJS, the Magazine

2011-08-01 00:00:00.0 Issue Now Available
  • Programming with Scala Traits – Part One

    by Venkat Subramaniam
  • On Eloquent Conversations – Part Two

    by Raju Gandhi
  • NoXML: Spring for XML Haters

    by Craig Walls
  • Handling Big Data with HBase

    by Scott Leberknight
Learn More »