Sunday, August 9, 2015

Using a basic groovy library packaged as a JAR, for sharing domain class definitions across many projects

Let's say I have multiple web applications all developed in Grails that needed access to a shared database as shown in the diagram below.



What would be the best way to define domain classes without duplicating code?

Prior to Grails 3.x, I typically created Grails Plugins to define domain classes shared across multiple web applications. All you had to do was define a plugin dependency in your BuildConfig.groovy configuration file.


 plugins {  
           runtime "my-plugin:my-plugin:1.0-SNAPSHOT"  
 }  

This worked reasonably well, that is, until we decided to add a new web application using Grails 3.x to the mix of existing web applications as shown in the diagram below:





 Grails 3.x was a massive change, now using Gradle as opposed to Maven to construct the builds. This meant that the file structure and layout changed dramatically due to differences in convention. What this also meant, was that the previously defined plugins in Grails 2.x no longer worked in Grails 3.x projects.
We were not in a position to upgrade all of our applications to 3.x because the example I've provided is simplified. We actually have many more web applications accessing the same shared database and would require extensive regression testing.

The other alternative, would be to maintain 2 versions of the plugin, one in grails 2.x and the other in grails 3.x. For obvious reasons, this was not ideal.

What I wanted was a single library (JAR file) with my domain class definitions that could be used by any web application regardless of the underlying grails version.

I've manged to find a way and present my findings here with you.

No more plugins, just a groovy library

Rather than using a plugin, I've created a library containing all  my domain class definitions using a combination of

  • Gradle - as the build tool
  • Groovy - to define my domain classes
  • JPA - to define domain class relational mappings - Java Persistence API

 Gradle installation

  • Download Gradle from the following webiste: Download Gradle
  • Set the bin folder to be in your environment variable PATH. This will allow you to run the gradle from the command-line
  • To test run the following from the command-line, 'gradle -v'

Define build.gradle in your library

 // Apply plugin must come first, the order is very important!  
 apply plugin: 'groovy'  
 apply plugin: 'java'  
 apply plugin: 'eclipse'  
 apply plugin: 'maven'  
 group = 'my-plugin'  
 version = '1.0-SNAPSHOT'  
 repositories {  
      mavenCentral()  
      mavenLocal()  
 }  
 dependencies {  
      compile 'org.codehaus.groovy:groovy-all:2.3.10'  
      compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'  
 }  

Define your domain classes using JPA

 package anu.individual  
 import java.util.Collection;  
 import java.util.List;  
 import java.util.Set;  
 import javax.persistence.ElementCollection  
 import javax.persistence.Entity  
 import javax.persistence.EnumType  
 import javax.persistence.Enumerated  
 import javax.persistence.GeneratedValue  
 import javax.persistence.Id  
 import javax.persistence.JoinColumn  
 import javax.persistence.JoinTable  
 import javax.persistence.ManyToOne  
 import javax.persistence.OneToMany  
 /**  
  * Individual domain class, defined using JPA   
  *   
  * @author Philip  
  *  
  */  
 // Using JPA annotations  
 @Entity  
 class Individual {  
      @Id  
      @GeneratedValue  
      Long id  
      String externalId  
      String externalIdUpperCase // variable not used  
      Boolean deleted= false  
      @ManyToOne  
      @JoinColumn(name="mother_id")  
      Individual mother  
      @ManyToOne  
      @JoinColumn(name="father_id")  
      Individual father  
      Long mustererBarcode  
      @ElementCollection  
      Set<String> phenotypes // snowmed CT IDs  
      @Enumerated(EnumType.STRING)  
      Species species  
      Boolean affected  
      Boolean proband  
      @Enumerated(EnumType.STRING)  
      Gender gender  
      @OneToMany       
      @JoinTable(  
           name="individual_team",  
           joinColumns= @JoinColumn(name="individual_id", referencedColumnName="id"),  
           inverseJoinColumns= @JoinColumn(name="team_id", referencedColumnName="id")   
      )  
      Set<Team> teams  

Package the library

To package the library using gradle, you can use the 'gradle install' from the command-line. This will automatically install the library in your local maven repository (.m2 folder) which can be used by your Grails application as a depedency.

Add the dependency to your Grails 3 application

If you're using Grails 3, then the way to configure your dependencies is in the build.gradle file. An example is shown below with the library highlighted in red:


 buildscript {  
   ext {  
     grailsVersion = project.grailsVersion  
   }  
   repositories {  
     mavenLocal()  
     maven { url "https://repo.grails.org/grails/core" }  
   }  
   dependencies {  
     classpath "org.grails:grails-gradle-plugin:$grailsVersion"  
     classpath 'com.bertramlabs.plugins:asset-pipeline-gradle:2.1.1'  
   }  
 }  
 plugins {  
   id "io.spring.dependency-management" version "0.5.2.RELEASE"  
 }  
 version "0.1"  
 group "individual"  
 apply plugin: "spring-boot"  
 apply plugin: "war"  
 apply plugin: "asset-pipeline"  
 apply plugin: 'eclipse'  
 apply plugin: 'idea'  
 apply plugin: "org.grails.grails-web"  
 apply plugin: "org.grails.grails-gsp"  
 ext {  
   grailsVersion = project.grailsVersion  
   gradleWrapperVersion = project.gradleWrapperVersion  
 }  
 assets {  
   minifyJs = true  
   minifyCss = true  
 }  
 repositories {  
   mavenLocal()  
   maven { url "https://repo.grails.org/grails/core" }  
 }  
 dependencyManagement {  
   imports {  
     mavenBom "org.grails:grails-bom:$grailsVersion"  
   }  
   applyMavenExclusions false  
 }  
 dependencies {  
   compile "org.springframework.boot:spring-boot-starter-logging"  
   compile "org.springframework.boot:spring-boot-starter-actuator"  
   compile "org.springframework.boot:spring-boot-autoconfigure"  
   compile "org.springframework.boot:spring-boot-starter-tomcat"  
   compile "org.grails:grails-dependencies"  
   compile "org.grails:grails-web-boot"  
   compile "org.grails.plugins:hibernate"  
   compile "org.grails.plugins:cache"  
   compile "org.hibernate:hibernate-ehcache"  
   compile "org.grails.plugins:scaffolding"  
   runtime "org.grails.plugins:asset-pipeline"  
   testCompile "org.grails:grails-plugin-testing"  
   testCompile "org.grails.plugins:geb"  
   // Note: It is recommended to update to a more robust driver (Chrome, Firefox etc.)  
   testRuntime 'org.seleniumhq.selenium:selenium-htmlunit-driver:2.44.0'  
   console "org.grails:grails-console"  
      // custom dependencies  
      compile 'anu.individual:individual-lib:1.0-SNAPSHOT'  
      runtime 'postgresql:postgresql:9.1-901.jdbc4'  
 }  
 task wrapper(type: Wrapper) {  
   gradleVersion = gradleWrapperVersion  
 }  

Tell Grails which domain classes should apply GORM

In the Grails app, create a file called hibernate.cfg.xml and place it in the /conf folder
 <!DOCTYPE hibernate-configuration SYSTEM  
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">  
 <hibernate-configuration>  
   <session-factory>  
     <mapping package="anu.individual" />  
     <mapping class="anu.individual.Individual" />  
     <mapping class="anu.individual.Team" />  
   </session-factory>  
 </hibernate-configuration>  

The file should list all the domain classes defined in the library

If you want to provide a different datasource for the domain classes defined in your external library, you can prefix the datasource name to the hibernate.cfg.xml file. Quote from the grails website says the following:

"To specify that an annotated class uses a non-default datasource, create a hibernate.cfg.xml file for that datasource with the file name prefixed with the datasource name."
For example, if I have a datasource named ds1, then i would change the name of the hibernate config file to ds1_hibernate.cfg.xml

Run the grails application

To test if your configuration is working correctly, run the application using 'grails run-app' command and your tables should automatically be generated.

Good luck!


References:
http://grails.github.io/grails-doc/latest/guide/plugins.html#creatingAndInstallingPlugins
http://stackoverflow.com/questions/23654116/importing-external-domain-classes-into-grails
https://grails.github.io/grails-doc/latest/guide/hibernate.html
https://spring.io/blog/2010/08/26/reuse-your-hibernate-jpa-domain-model-with-grails
https://jira.grails.org/browse/GRAILS-9389
https://jira.grails.org/browse/GRAILS-9410
http://blog.bripkens.de/2012/03/Install-artefacts-local-repository-gradle/
https://spring.io/guides/gs/accessing-data-jpa/
https://docs.gradle.org/current/userguide/tutorial_groovy_projects.html
https://grails.github.io/grails-doc/latest/guide/single.html#dependencyResolution

No comments:

Post a Comment