Groovy

Grails: Autodiscovery of JPA-annotated domain classes

There are some issues to be fixed with the support for adding JPA annotations (for example @Entity) to Groovy classes in grails-app/domain in 2.0. This is due to the changes made to adding most GORM methods to the domain class bytecode with AST transformations instead of adding them to the metaclass at runtime with metaprogramming. There is a workaround – put the classes in src/groovy (or write them in Java and put them in src/java).

This adds a maintenance headache though because by being in grails-app/domain the classes are automatically discovered, but there’s no scanning of src/groovy or src/java for annotated classes so they must be explicitly listed in grails-app/conf/hibernate/hibernate.cfg.xml. We do support something similar with the ability to annotate Groovy and Java classes with Spring bean annotations like @Component and there is an optional property grails.spring.bean.packages in Config.groovy that can contain one or more packages names to search. We configure a Spring scanner that looks for annotated classes and automatically registers them as beans. So that’s what we need for JPA-annotated src/groovy and src/java classes.

It turns out that there is a Spring class that does this, org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean. It extends the standard SessionFactory factory bean class org.springframework.orm.hibernate3.LocalSessionFactoryBean and adds support for an explicit list of class names to use and also a list of packages to scan. Unfortunately the Grails factory bean class org.codehaus.groovy.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean also extends LocalSessionFactoryBean so if you configure your application to use AnnotationSessionFactoryBean you’ll lose a lot of important functionality from ConfigurableLocalSessionFactoryBean. So here’s a subclass of ConfigurableLocalSessionFactoryBean that borrows the useful annotation support from AnnotationSessionFactoryBean and can be used in a Grails application:

package com.burtbeckwith.grails.jpa;

import java.io.IOException;

import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;

import org.codehaus.groovy.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean;
import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.cfg.Configuration;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;

/**
 * Based on org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean.
 * @author Burt Beckwith
 */
public class AnnotationConfigurableLocalSessionFactoryBean extends ConfigurableLocalSessionFactoryBean implements ResourceLoaderAware {

   private static final String RESOURCE_PATTERN = '/**/*.class';

   private Class<?>[] annotatedClasses;
   private String[] annotatedPackages;
   private String[] packagesToScan;

   private TypeFilter[] entityTypeFilters = new TypeFilter[] {
         new AnnotationTypeFilter(Entity.class, false),
         new AnnotationTypeFilter(Embeddable.class, false),
         new AnnotationTypeFilter(MappedSuperclass.class, false),
         new AnnotationTypeFilter(org.hibernate.annotations.Entity.class, false)};

   private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

   public AnnotationConfigurableLocalSessionFactoryBean() {
      setConfigurationClass(GrailsAnnotationConfiguration.class);
   }

   public void setAnnotatedClasses(Class<?>[] annotatedClasses) {
      this.annotatedClasses = annotatedClasses;
   }

   public void setAnnotatedPackages(String[] annotatedPackages) {
      this.annotatedPackages = annotatedPackages;
   }

   public void setPackagesToScan(String[] packagesToScan) {
      this.packagesToScan = packagesToScan;
   }

   public void setEntityTypeFilters(TypeFilter[] entityTypeFilters) {
      this.entityTypeFilters = entityTypeFilters;
   }

   public void setResourceLoader(ResourceLoader resourceLoader) {
      this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
   }

   @Override
   protected void postProcessMappings(Configuration config) throws HibernateException {
      GrailsAnnotationConfiguration annConfig = (GrailsAnnotationConfiguration)config;
      if (annotatedClasses != null) {
         for (Class<?> annotatedClass : annotatedClasses) {
            annConfig.addAnnotatedClass(annotatedClass);
         }
      }
      if (annotatedPackages != null) {
         for (String annotatedPackage : annotatedPackages) {
            annConfig.addPackage(annotatedPackage);
         }
      }
      scanPackages(annConfig);
   }

   protected void scanPackages(GrailsAnnotationConfiguration config) {
      if (packagesToScan == null) {
         return;
      }

      try {
         for (String pkg : packagesToScan) {
            logger.debug('Scanning package '' + pkg + ''');
            String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                  ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;
            Resource[] resources = resourcePatternResolver.getResources(pattern);
            MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
            for (Resource resource : resources) {
               if (resource.isReadable()) {
                  MetadataReader reader = readerFactory.getMetadataReader(resource);
                  String className = reader.getClassMetadata().getClassName();
                  if (matchesFilter(reader, readerFactory)) {
                     config.addAnnotatedClass(resourcePatternResolver.getClassLoader().loadClass(className));
                     logger.debug('Adding annotated class '' + className + ''');
                  }
               }
            }
         }
      }
      catch (IOException ex) {
         throw new MappingException('Failed to scan classpath for unlisted classes', ex);
      }
      catch (ClassNotFoundException ex) {
         throw new MappingException('Failed to load annotated classes from classpath', ex);
      }
   }

   private boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
      if (entityTypeFilters != null) {
         for (TypeFilter filter : entityTypeFilters) {
            if (filter.match(reader, readerFactory)) {
               return true;
            }
         }
      }
      return false;
   }
}

You can replace the Grails SessionFactory bean in your application’s grails-app/conf/spring/resources.groovy by using the same name as the one Grails registers:

import com.burtbeckwith.grails.jpa.AnnotationConfigurableLocalSessionFactoryBean

beans = {
   sessionFactory(AnnotationConfigurableLocalSessionFactoryBean) { bean ->
      bean.parent = 'abstractSessionFactoryBeanConfig'
      packagesToScan = ['com.mycompany.myapp.entity']
   }
}

Here I’ve listed one package name in the packagesToScan property but you can list as many as you want. You can also explicitly list classes with the annotatedClasses property. Note that this is for the “default” DataSource; if you’re using multiple datasources you will need to do this for each one.

So this means we can define this class in src/groovy/com/mycompany/myapp/entity/Person.groovy:

package com.mycompany.myapp.entity

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.Version

@Entity
class Person {

   @Id @GeneratedValue
   Long id

   @Version
   @Column(nullable=false)
   Long version

   @Column(name='first', nullable=false)
   String firstName

   @Column(name='last', nullable=false)
   String lastName

   @Column(nullable=true)
   String initial

   @Column(nullable=false, unique=true, length=200)
   String email
}

It will be detected as a domain class and if you run the schema-export script the table DDL will be there in target/ddl.sql.

There are a few issues to be aware of however, mostly around constraints. You can’t define a constraints or mapping block in the class – they will be ignored. The mappings that you would have added just need to go in the annotations. For example I have overridden the default names for the firstName and lastName properties in the example above. But nullable=true is the default for JPA and it’s the opposite in Grails – properties are required by default. So while the annotations will affect the database schema, Grails doesn’t use the constraints from the annotations and you will get a validation error for this class if you fail to provide a value for the initial property.

You can address this by creating a constraints file in src/java; see the docs for more details. So in this case I would create src/java/com/mycompany/myapp/entity/PersonConstraints.groovy with a non-static constraints property, e.g.

constraints = {
   initial(nullable: true)
   email unique: true, length: 200)
}

This way the Grails constraints and the database constraints are in sync; without this I would be able to create an instance of the domain class that has an email with more than 200 characters and it would validate, but cause a database constraint exception when inserting the row.

This also has the benefit of letting you use the Grails constraints that don’t correspond to a JPA constraint such as email and blank.
 

Reference: Autodiscovery of JPA-annotated domain classes in Grails from our JCG partner Burt Beckwith at the An Army of Solipsists blog.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button