Статьи

Grails: автоматическое обнаружение JPA-аннотированных классов доменов

Есть некоторые проблемы, которые нужно исправить с помощью поддержки добавления аннотаций JPA (например, @Entity ) к классам Groovy в grails-app/domain в 2.0. Это связано с изменениями, внесенными в добавление большинства методов GORM в байт-код класса домена с помощью преобразований AST вместо добавления их в метакласс во время выполнения с метапрограммированием. Существует обходной путь — поместите классы в src/groovy (или напишите их на Java и поместите в src/java ).

Это добавляет головную боль при обслуживании, потому что, находясь в grails-app/domain классы обнаруживаются автоматически, но нет сканирования src/groovy или src/java для аннотированных классов, поэтому они должны быть явно перечислены в grails-app/conf/hibernate/hibernate.cfg.xml . Мы поддерживаем нечто похожее с возможностью аннотировать классы Groovy и Java с помощью аннотаций bean-компонентов Spring, таких как @Component и в @Component есть необязательное свойство Config.groovy которое может содержать одно или несколько имен пакетов для поиска. Мы настраиваем сканер Spring, который ищет аннотированные классы и автоматически регистрирует их как bean-компоненты. Вот что нам нужно для аннотированных JPA классов src/groovy и src/java .

Оказывается, есть класс Spring, который делает это, org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean . Он расширяет стандартный класс фабричного компонента SessionFactory org.springframework.orm.hibernate3.LocalSessionFactoryBean и добавляет поддержку явного списка имен используемых классов, а также списка пакетов для сканирования. К сожалению, класс bean-компонента Grails org.codehaus.groovy.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean также расширяет LocalSessionFactoryBean так что если вы сконфигурируете свое приложение для использования AnnotationSessionFactoryBean вы потеряете много важных функций из ConfigurableLocalSessionFactoryBean . Итак, вот подкласс ConfigurableLocalSessionFactoryBean который заимствует полезную поддержку аннотаций из AnnotationSessionFactoryBean и может использоваться в приложении Grails:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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;
   }
}

Вы можете заменить bean-компонент Grails SessionFactory в приложении grails-app/conf/spring/resources.groovy вашего приложения, используя то же имя, что и регистры Grails:

1
2
3
4
5
6
7
8
import com.burtbeckwith.grails.jpa.AnnotationConfigurableLocalSessionFactoryBean
 
beans = {
   sessionFactory(AnnotationConfigurableLocalSessionFactoryBean) { bean ->
      bean.parent = 'abstractSessionFactoryBeanConfig'
      packagesToScan = ['com.mycompany.myapp.entity']
   }
}

Здесь я перечислил одно имя пакета в свойстве packagesToScan но вы можете указать сколько угодно. Вы также можете явно перечислить классы с помощью свойства annotatedClasses . Обратите внимание, что это для источника данных «по умолчанию»; если вы используете несколько источников данных, вам нужно будет сделать это для каждого из них.

Таким образом, это означает, что мы можем определить этот класс в src/groovy/com/mycompany/myapp/entity/Person.groovy :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
}

Он будет определен как класс домена, и если вы запустите сценарий экспорта схемы, таблица DDL будет там в target/ddl.sql .

Однако следует учитывать несколько проблем, в основном связанных с ограничениями. Вы не можете определить constraints или блок mapping в классе — они будут игнорироваться. Отображения, которые вы бы добавили, нужно просто добавить в аннотации. Например, я переопределил имена по умолчанию для свойств firstName и lastName в примере выше. Но nullable=true является значением по умолчанию для JPA, а в Grails — наоборот — свойства требуются по умолчанию. Таким образом, хотя аннотации будут влиять на схему базы данных, Grails не использует ограничения из аннотаций, и вы получите ошибку проверки для этого класса, если вы не укажете значение для initial свойства.

Вы можете решить эту проблему, создав файл ограничений в src/java ; см. документы для более подробной информации. Так что в этом случае я бы создал src/java/com/mycompany/myapp/entity/PersonConstraints.groovy со свойством нестатических constraints , например

1
2
3
4
constraints = {
   initial(nullable: true)
   email unique: true, length: 200)
}

Таким образом, ограничения Grails и базы данных синхронизируются; без этого я смог бы создать экземпляр класса домена, в котором есть электронное письмо с более чем 200 символами, и оно могло бы быть проверено, но при вставке строки вызывало исключение из базы данных.

Это также имеет то преимущество, что вы можете использовать ограничения Grails, которые не соответствуют ограничениям JPA, таким как email и blank .

Ссылка: автообнаружение аннотированных классов JPA в Grails от нашего партнера JCG Берта Беквита в блоге « Армия солипсистов» .