9ci Confidential

Quick Start

Quick Start Example

To show what Repository data services are all about let’s walk through an example.

Domain setup

Lets assume we are setting up a way to track details for this Project. We might setup a couple of domains like this.

//change the default contraints to be nullable:true
gorm.default.constraints = {
    '*'(nullable:true)
}

@GrailsCompileStatic
class Project {
    String name
    String description

    GitHubInfo gitHubInfo 


}

@GrailsCompileStatic
class GitHubInfo {
    Long repId         //1829344
    String slug         //yakworks/gorm-tools
    String description  //gorm tools for a clean shaved yak
}

and lets say we have a map, perhaps that came from a restful json request or some other service, test data etc...

params = [
    name: 'gorm-tools',
    gitHubInfo: [
        repId: 1829344,
        slug: 'yakworks/gorm-tools',
    ]
]

Stock Grails Gorm

Using stock Grails Gorm we would probably implement something like the following simplified boiler plate design pattern for the C in CRUD

@GrailsCompileStatic @Transactional
class ProjectService {

    Project createNew(Map data){
        def project = new Project()
        project.properties = data
        project.save(failOnError:true) //throw runtime to roll back if error
    }

    //.... other imps
}

// elsewhwere, probably in a controller action, we would inject the service and use it to save
@Autowired ProjectService projectService
...
projectService.createNew(params)
or perhaps we would do it with the new Data Services

Using the Repository

With this Gorm repository plugin, we have shaved the yak for you and each domain has a Repository automatically setup for this pattern. So with this plugin all the boiler plate from above can be replaced with 1 line!

// elsewhere, probably in a controller action. 
Project.create(params)

Thats it. The Project.create() actually delegates to the DefaultGormRepo.create(). The create is wrapped in a transaction, creates the intance, binds the data and defaults to saving with failOnError:true. Like all transactional methods if the method is called from inside another transaction it will use it otherwise it creates a new one.

💡 Other Repository Domain Methods
You can do the same thing as above for an update or delete. The details of whats available can be seen in the GormRepoEntity trait or in the GormRepoEntity source and are outlined below

Testing the Domain

If you used the script to create the domains then the tests will already be in place for us or you can add one manually like so.

package testing

import gorm.tools.testing.hibernate.AutoHibernateSpec
import spock.lang.Specification

class ProjectSpec extends AutoHibernateSpec<Project> {
    /** automatically runs tests on persist(), create(), update(), delete().*/
}

Notice the absence of test methods? Running with the the mantra of "convetion over configuration" and "intelligent defaults" DomainAutoTest will mock the domain, setup and create the data for you then exercise the domain and the default repository service for you. We'll see in the next section how to override the automated tests in the DomainAutoTest.

Implementing ProjectRepo Service

The DefaultGormRepo that is setup for the Project domain will of course not always be adequate for the business logic. Again running with the "intelligent defaults but easy to override" mantra we can easily and selectively override the defaults in the repository. Lets say we want to do something more advanced during the create such as validate and retrieve info from GitHub. Its not recomended to autowire beans into the domains for performance reasons It can also be tricky and at times fairly messy trying to modify or create domains using gorm's hibernate inspired event methods. Such as beforeCreate inside the Project domain and deal with flushing.

We can abstract out the logic into a ProjectRepo.

Lets say we wanted to use a service to validate Github repo and retrieve the description on create. We can add a class to the grails-app/repository directory as in the following example.

package tracker

@CompileStatic
class ProjectRepo implements GormRepo<Project>{

    @Autowired GitHubApi gitHubApi

    //event method used to update the descriptions with the ones in github
    @RepoListener
    void afterBind(Project project, Map params, AfterBindEvent be){
        Map repoInfo = gitHubApi.getGitRepo(params.gitHubRepo)
        //check that it was found using the slug or repoId
        if (!repoInfo) 
            throw new DataRetrievalFailureException("Github Repo is invalid for ${params.gitHubInfo}")

        if(repoInfo.description){
            //force the gitHubRepo.description to be whats in github
            project.gitHubInfo.description = repoInfo.description
            //update project.description to be the same if its null
            project.description = project.description ?: repoInfo.description
            project.persist() //optional
        }
    }
}

// elsewhere, you can call and it will be automatically taken care of
Project.create(params)

Events methods and fired spring events are run inside the inherited transaction and as usual an uncaught runtime exception will rollback. The persist methods is another addition to the domains added by the GormRepoEntity trait. In the example above its not really needed as the changes will be flushed during the transaction commit. They are here to doc whats happening and so that validation failures can be easily seen.

Testing the ProjectRepo Changes

DomainAutoTest already contains default tests for create, update, persist, delete methods, that are called from repo. So if, for example, any changes were made for create method for ProjectRepo:

@Artefact("Repository")
@Transactional
class ProjectRepo extends DefaultGormRepo<Project> {

    @Override
    @CompileDynamic
    Org create(Map params) {
        params.name = "#" + params.name 
        super.create(params)
     }
}
then you can override test case to check specific value

class ProjectSpec extends DomainAutoTest<Project> {

    void test_create() {
            when:
            D entity = getDomainClass().create(values)
            then:
            entity.id != null
            entity.name[0] == "#"
        }

}