Reference

The Repository Artefact

This plugin adds a new artefact type Repository. Each domain will have a spring bean setup for it if one doesn't exists already.

A repository bean is configured for each domain with a DefaultGormRepo unless explicit repository class.
The trait GormRepo implements the RepositoryApi interface and is what backs the DefaultGormRepo. You'll mostly use the GormRepo trait when creating a custom concrete implementation of a Repository.

Reference to a Repository for given domain class can be easily obtained by calling MyDomainClass.repo static method.

Implementing A Repository

If you need to override the DefaultGormRepo that is attached to each domain then you can create your own service inside grails-app/repository and name it YourDomainNameRepo (eg OrgRepo). Plugin will automatically lookup all Repository classes and configure them as spring service beans to be used for your domain.

A Repository must either implement GormRepo Trait or if you wish extend DefaultGormRepo

Example:

class OrgRepo implements GormRepo<Org> {

   @RepoListener
   void beforeBind(Org org, Map params, BeforeBindEvent be) {
       //do some thing before bind

   }

   @Transactional
   boolean makeInactive(Long id){
       def org = Org.get(id)
       ... do transactional stuff
       org.persist()
   }

}

📝 @Transactional For performance reasons, @Transactional is not needed on the class

GormRepoEntity Trait

See Groovydocs api for the GormRepoEntity that is injected onto all domains.

Instance methods added to the domains

Every domain gets a repository which is either setup for you or setup by implementing GormRepo Each method is transactional and will prevent incomplete cascading saves.

  • persist(): calls the GormRepo's persist which in turn calls domain.save(failOnError:true) Throws a EntityValidationException

Statics added to the domain

  • create(params): calls the repo.create which does the bolier plate code you might find in a scaffolded controller. creates a new instance, sets the params and calls the repository.save (essentially the persist()). ex: Book.insertAndSave([name:'xyz',isbn:'123']) Throws a EntityValidationException if anything goes wrong
  • update(params): calls the repo.update which does the boiler plate code you might find in a scaffolded controller. gets the instance base in the params.id, sets the params and calls the repository.save for it. ex: Book.update([id:11,name:'aaa']) Throws a EntityValidationException if anything goes wrong
  • remove(id): calls the repository.removeById gets the instance base in the params.id, calls the delete for it. ex: Book.remove([id:11]) Throws a EntityNotFoundException if anything goes wrong
  • repo: a quick way to get to the repository for the Domain. It will return the DefaultGormRepo that was auto created or one you defined for the domain under grails-app/repository.

Repository Events

Methods for @RepoListener

Each Repository can implement any of the methods listed below and they will get called during persistence operation if they have the @RepoListener annotation.

  • beforeBind(T instance, Map data, BeforeBindEvent be) - Called before a new instance is saved, can be used to do custom data binding or initialize the state of domain etc.
  • afterBind(T instance, Map data, AfterBindEvent be) - Called after databinding is performed.
  • beforePersist(T instance, BeforePersistEvent e) - Called every time before an instance is saved.
  • afterPersist(T instance, AfterPersistEvent e) - Called every time after an instance is saved.
  • beforeRemove(T instance, BeforeRemoveEvent e) - Called before an instance is deleted. Can be utilized to cleanup related records etc.
  • afterRemove(T instance, AfterRemoveEvent e) - Called After an instance is deleted.

Grails Events

The Repository also provides a possibility to handle events using Grails annotations. Please see docs for Grails Events.

Publishing events

Grails provides two ways for creating events - using @Publisher annotation on a method and using EventBus directly, please see docs for Event Publishing. In case of using publisher annotation Grails takes event id from the method name (method with @Publisher annotation). If using EventBus we should specify event id manually.

By default Repository uses EventBus to create events (see RepoEventPublisher). It publishes a number of Repository Events and provides it's own way to build event ids. All ids of repository events correspond to the format <domainName>.<eventTypeName>. As we can see there are two values separated with a dot. The first comes a name of a domain class, for which an event is created and the second - a type of a specific repository event. For example, in case we call persist() method on a domain entity called Org, the repository invokes several events, one of them is BeforePersist event with id Org.beforePersist.

Subscribing to events

Grails provides several options for handling events, please see Grails docs for Event Handling.

In case of adding @Subscriber annotation to a method, Grails determines event id from the method name by default. For example, methods like someEvent() or onSomeEvent() listen to the event with id someEvent.

Due to the fact that ids of repository events contain . symbol, we should pass event id to the Subscriber annotation like so:

    @Subscriber("SomeDomain.someEvent")
    void someMethod() {}

According to Grails docs, a class which contains a listener method (with @Subscriber annotation) should be a spring bean.

Please see the example with OrgSubscriber below:

Example

import grails.events.annotation.Subscriber
import gorm.tools.repository.events.BeforePersistEvent
import gorm.tools.repository.events.AfterPersistEvent

class OrgSubscriber {

    @Subscriber("Org.beforePersist")
    void beforePersist(BeforePersistEvent event) {
       // ...
    }

    @Subscriber("Org.afterPersist")
    void afterPersist(AfterPersistEvent event) {
       // ...
    }
}
In this example we can see two listeners which handle events that occur before and after persisting an entity of the Org domain class.

Spring Events

Spring events are recomended over grails.events as they are much more performant if processing large numbers of domains The Repository also publishes a number of events as listed in the Groovydoc API

Example

import org.springframework.context.event.EventListener
import gorm.tools.repository.events.BeforeBindEvent

class OrgListener {

    @EventListener
    void beforeBind(BeforeBindEvent<Org> event) {
       Org org = event.entity
       //Do some thing here.
    }
}

📝 Calling methods which trigger events inside an event listener causes an infinite loop

External refreshable beans for Events

Since 2.0 Spring added support for defining beans using supported dynamic languages. Eg. groovy. This makes it possible to create groovy script files outside of application which contains class definition, and use it as spring bean. This feature can be used to create refreshable beans, spring will watch the external script for changes and automatically reload the bean if it has changed. The interval can be configured using refresh-check-delay. This feature makes it possible to externalize the event listeners outside of application.

Here is an example of how to use an external refreshable bean as event listener.

Create a groovy script containing bean definition for event listener some where on file system out side of grails application directory.

File OrgEventListener.groovy

import grails.events.annotation.Subscriber
import gorm.tools.repository.events.BeforePersistEvent
import gorm.tools.repository.events.AfterPersistEvent

public class OrgEventListener {

      @Subscriber("Org.beforePersist")
      void beforePersist(BeforePersistEvent e){
          Org org = (Org) e.entity
          //do some thing with org
      }

      @Subscriber("Org.afterPersist")
      void afterPersist(AfterPersistEvent e){
          Org org = (Org) e.entity
          //do some thing with org
      }
}

The above example uses Subscriber annotation from Grails async project. The event handler methods will get called asynchronously and does not take part in transaction. Repository also publishes events using spring event mechanism which can be used to define event listeners which gets called synchronously and takes part in current transaction.

Following example shows how to define synchronous event listener.

import org.springframework.context.ApplicationListener 
import gorm.tools.repository.events.BeforePersistEvent

class OrgEventListener implements ApplicationListener<BeforePersistEvent<Org>>  {

    void onApplicationEvent(BeforePersistEvent<Org> event) {
        Org org = event.entity
        //dome some thing with org.
    }
} 

Define OrgEventListener as spring bean in grails-app/conf/spring/resources.groovy

 xmlns lang: "http://www.springframework.org/schema/lang"
 lang.groovy(id: "orgEventListener", 'script-source': "file:<path to OrgEventListener.groovy>", 'refresh-check-delay': 1000)

Now the refreshableBean can be injected into any other bean. Spring will reload it automatically if the RefreshableBean.groovy changes.

See Spring dynamic languages support for more details on dynamic language support.

RepoUtil, RepoMessage Helpers

See RepoUtil

RepoUtil:

checkFound(entity, Map params,String domainClassName) checks does the entity exists, if not throws EntityNotFoundException with human readable error text

checkVersion(entity,ver) checks the passed in version with the version on the entity (entity.version) make sure entity.version is not greater, throws OptimisticLockingFailureException

flush() flushes the session

clear() clears session cache

flushAndClear() flushes the session and clears the session cache

RepoMessage contains bunch of help methods for creating text messages

See RepoMessage

The example below shows how to build saved message for a domain:

    User user = new User(id:100,version:1)

    Map msg = RepoMessage.saved(user)
    assert 'default.saved.message' == msg.code //i18 code
    assert 100 == msg.args[1]

List of available messages

  • saved
  • not saved
  • updated
  • not updated
  • deleted
  • not deleted
  • notFound
  • optimisticLockingFailure - Another user has updated the resource while you were editing

Gorm-tools provides its own types of exceptions to handle errors which relate to domains.

EntityValidationException

See EntityValidationException

An extension of the default ValidationException. It is possible to pass the entity and the message map.

EntityNotFoundException

See EntityNotFoundException

An extension of the EntityValidationException to be able to handle rest request which should respond with 404 error.

Async batch processing support

Plugin makes it easy to process list of batches asynchronously with transaction using ParallelTools. GparsParallelTools is default implementation provided by the plugin.

batchSize - Is the batchsize used for slicing the list. The default value is obtained from hibernate.jdbc.batch_size configuration setting. However it can be explicitely passed in args as shown in below example.
poolSize - Is the size of Gpars thread pool used by GparsParallelTools. The default value can configured using gorm.tools.async.poolSize. If not configured, it will use the default poolsize used by Gpars. which is available processors + 1

Example:

class TestService {
    ParallelTools parallelTools

    void insertBatches(List<Map> list) {
        parallelTools.parallelCollate([batchSize:100], list) { Map record, Map args ->
            Org.create(record)
        }
    }

}

The above code snippet will slice the list into batches of 100 and run each batch in parallel and wrap it in transaction.

The list can be processed in parallel without it being wrapped in transaction using parallelTools.parallel method.

parallelTools.parallel(parallelTools.collate(list)) { List batch, Map args ->
    //do some thing with the batch.
}

Testing support

Plugin provides GormToolsTest and GormToolsHibernateSpec To make it easy to write tests which utilizes repository.

Writing unit tests using GormToolsTest
GormToolsTest extends grails DataTest and configures a repository bean for every mock domain.
The repository class must exist in same package as the domain class, or else, it will configure DefaultGormRepo as the repository for the given domain.

class CitySpec extends Specification implements GormToolsTest {

   void setup() {
     mockDomain(City)
   }

   void "test create"() {
     given:
     Map params = [name:"Chicago"]

     when:
     City city = City.create(params)

     then:
     city.name == "Chicago"
   }
}

GormToolsHibernateSpec
GormToolsHibernateSpec extends HibernateSpec and setups repository beans for domains. Can be used to unit test with full hibernate support with inmemory database.

class CitySpec extends GormToolsHibernateSpec {

  static List entityClasses = [City]

   void "test create"() {
     given:
     Map params = [name:"Chicago"]

     when:
     City city = City.create(params)
     //or City.repo.create(params)

     then:
     city.name == "Chicago"
   }
}

When getDomainClasses() is overridden GormToolsHibernateSpec will try to find the repository in the same package as domain class. Alternatively if getPackageToScan() is provided, it will find all the repository from the given package and below it.

DomainAutoTest Also plugin provides DomainAutoTest abstract class that contains default tests cases for CRUD operations. DomainAutoTest will mock the domain, setup and create the data for you then exercise the domain and the default repository service for you. See example bellow:

import yakworks.testing.gorm.hibernate.AutoHibernateSpec
import testing.Project

class ProjectSpec extends AutoHibernateSpec<Project> {

  /** automatically runs tests on persist(), create(), update(), delete().*/

}

The next methods will be added and executed for Project class:

  • test_create
  • test_update
  • test_persist
  • test_delete

Each of them can be overridden by the method with the same name if needed.

Test data is build with help of BuildExampleData and [BuildExampleHolder].

BuildExampleHolder - is a holder that stores BuildExampleData instances for domains, to avoid creating of the same values several times

BuildExampleData is class that builds test data based on example property from constraints section. If domain class has association that shouldn't be null(has constrain nullable: false), creates test data for it to, left null otherwise. Dates in example should be string format, they will be parsed to dates.