Show Navigation

Grails GORM Data Services

In this guide, we will learn how to create GORM Data Services in a Grails Application.

Authors: Nirav Assar, Sergio del Amo

Grails Version: 3.3.8

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide, we are going to do a deep dive into GORM Data Services. Introduced in GORM 6.1, GORM Data Services take the work out of implemented service layer logic by adding the ability to automatically implement abstract classes or interfaces using GORM logic. It results in less code to write, compile time optimization, and ease of testing.

This guide will go into detail on how to create and use GORM Data Services with a sample Grails application. The guide will stay focused on the service layer of the app consistent with persistence.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.7 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-gorm-data-services/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-gorm-data-services/complete

3 Writing the Application

We are going to write a simple application with the domains Person and Address. These domains have a one-to-many relationship. We will develop GORM Data Services which perform query and write operations.

3.1 Domain Objects

Create the Person and Address domain classes. They have a one-to-many relationship.

grails-app/domain/example/grails/Person.groovy
package example.grails

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Person {

    String name
    Integer age
    Set<Address> addresses (1)

    static hasMany = [addresses: Address]
}
1 You normally don’t need to specify default type java.util.Set in a hasMany association. However, we query the association in a JPA-QL query later in this tutorial and thus we need to be explicit here.
grails-app/domain/example/grails/Address.groovy
package example.grails

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Address {
    String streetName
    String city
    String state
    String country

    static belongsTo = [person: Person]
}

4 Why GORM Data Services

Automatic implementation of an interface or an abstract class reduces the amount of code written. In addition, GORM Data Services define transaction boundary semantics automatically. For example, all public methods are marked with @Transactional (and read-only for query methods).

Advantages of Data Services

To summarize the advantages:

  • Type Safety - Data service method signatures are compile time checked and compilation will fail if the types of any parameters don’t match up with properties in your domain class.

  • Testing - Since Data Services are interfaces this makes them easy to mock.

  • Performance - The generated services are statically compiled and unlike competing technologies in the Java space no proxies are created so runtime performance doesn’t suffer. Moreover, update operations are performed with efficiency.

  • Correctly define transaction semantics. Users often don’t correctly define transactions semantics. Each method in a Data Service is wrapped in an appropriate transaction (a read-only transaction in the case of read operations).

  • Support Multi-tenancy. In combination with @CurrentTenant annotation, GORM data services ease multi-tenancy development.

5 Using GORM Data Services

To write a Data Service, create an interface and annotate it with grails.gorm.services.Service with the applied domain class. The @Service transformation looks at the method signatures of the interface to surmise what implementation should be generated. It looks at the return type along with the method stem to implement functionality. Check Data Service Conventions for details.

5.1 Dynamic Finders Like

If you have worked with GORM in the past, it is probable that you have you used Dynamic Finders to query.

A dynamic finder looks like a static method invocation, but the methods themselves don’t actually exist in any form at the code level. Instead, a method is auto-magically generated using code synthesis at runtime, based on the properties of a given class

With GORM Data Services, you can create an interface with methods with the same expressiveness as Dynamic Finders but without the disadvantages. E.g. you get type safety since GORM Data services are statically compiled.

To find a Person by name with a Dynamic finder you will use Person.findByName. Let’s implement a Data Service to achieve the same query.

Create an interface annotated with import grails.gorm.services.Service and declare a method with the same signature.

grails-app/services/example/grails/PersonDataService.groovy
package example.grails

import grails.gorm.services.Query


import grails.gorm.services.Service

@Service(Person) (1)
interface PersonDataService {

    Person findByName(String name) (1)


    void delete(Serializable id) (3)

}
1 Qualify the @Service with the domain class you want to use.
2 In the method findByName, the stem is find which tells GORM it’s a query (thus a read-only transaction will be used), and Name matches the domain property.
3 The method delete takes the id of the person which should be removed. It will automatically be wrapped in a writable transaction.

5.2 Unit Test

You could write a unit test for the previous Data Service as follows:

src/test/groovy/example/grails/PersonDataServiceSpec.groovy
package example.grails

import org.grails.orm.hibernate.HibernateDatastore
import org.springframework.test.annotation.Rollback
import org.springframework.transaction.PlatformTransactionManager
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class PersonDataServiceSpec extends Specification { (1)

    @Shared (2)
    PersonDataService personDataService

    @Shared (2)
    @AutoCleanup (3)
    HibernateDatastore hibernateDatastore

    @Shared (2)
    PlatformTransactionManager transactionManager

    void setupSpec() {
        hibernateDatastore = new HibernateDatastore(Person) (4)
        transactionManager = hibernateDatastore.getTransactionManager() (5)
        personDataService = this.hibernateDatastore.getService(PersonDataService)
    }

    @Rollback (5)
    void "test find person by name"() {
        given:
        Person p = personDataService.save("Nirav", 39)

        when:
        Person person = personDataService.findByName("Nirav")

        then:
        person.name == "Nirav"
        person.age == 39

        cleanup:
        personDataService.delete(p.id) (6)
    }
}
1 The test should extend spock.lang.Specification
2 The Shared annotation is used to indicate to Spock that the HibernateDatastore, personDataService and transactionManager properties are shared across all tests.
3 The AutoCleanup annotation makes sure that HibernateDatastore is shutdown when all tests finish executing.
4 Within the setupSpec method a new HibernateDatastore is constructed with the domain classes to use as the argument to the constructor.
5 In general you have to wrap your test execution logic in a session or transaction. You can obtain the PlatformTransactionManager from the HibernateDatastore.
6 Typically you want to annotate your feature methods with the Rollback annotation which is used to rollback any changes made within each test.
7 Cleanup the person created during the test.

Luckily, Grails Hibernate Plugin includes a utility class grails.test.hibernate.HibernateSpec which you could extend from and simplify the previous unit test:

src/test/groovy/example/grails/PersonDataServiceSpec.groovy
package example.grails

import grails.test.hibernate.HibernateSpec
import spock.lang.Shared

class PersonDataServiceSpec extends HibernateSpec {

    @Shared
    PersonDataService personDataService

    def setup() {
        personDataService = hibernateDatastore.getService(PersonDataService)
    }

    void "test find person by name"() {
        given:
        personDataService.save("Nirav", 39)

        when:
        Person person = personDataService.findByName("Nirav")

        then:
        person.name == "Nirav"
        person.age == 39

        cleanup:
        personDataService.delete(person.id)
    }
}

5.3 Property Projection

Add a method to the GORM Data Service which returns a projection. This has the advantage of bringing back only one column instead of the whole object. There are a few ways to implement projections. One way is to is to use the convention T find[Domain Class][Property].

For example, for the property age of domain class Person the method will be:

grails-app/services/example/grails/PersonDataService.groovy
Integer findPersonAge(String name)

Add to the unit test to verify correctness.

src/test/groovy/example/grails/PersonDataServiceSpec.groovy
void "test find persons age projection"() {
    given:
    Person person = personDataService.save("Nirav", 39)

    when:
    Integer age = personDataService.findPersonAge("Nirav")

    then:
    age == 39

    cleanup:
    personDataService.delete(person.id)
}

5.4 Save Operation

Data Services can perform write operations as well.

grails-app/services/example/grails/PersonDataService.groovy
Person save(Person person)

Add to the unit test to verify correctness.

src/test/groovy/example/grails/PersonDataServiceSpec.groovy
void "test save person"() {
    when:
    Person person = personDataService.save("Bob", 22)

    then:
    person.name == "Bob"
    person.age == 22
    personDataService.count() == old(personDataService.count()) + 1 (1)

    cleanup:
    personDataService.delete(person.id)
}
1 With Spock’s old method we get the value a statement had before the when: block is executed.
Run all the tests to make sure they pass → gradlew check.

5.5 Join Query

Data Services can also implement a join query using the grails.gorm.services.Join annotation. Grails associations are lazily loaded by default. For a one to many relationship, loading of the many side can cause n + 1 problems resulting in many select statements. This can severely hinder performance.

Essentially we can perform an eager load with @Join. Apply this to concept to a query on Person and Address.

grails-app/services/example/grails/PersonDataService.groovy
@Join('addresses') (1)
Person findEagerly(String name)

Until this point we have defined the Person GORM Data Service with an interface. You can use an abstract class instead. A combination of interface and an abstract class implementing the interface with some custom methods is a common pattern. Rewrite PersonDataService as shown below:

grails-app/services/example/grails/PersonDataService.groovy
package example.grails

import grails.gorm.services.Query


import grails.gorm.services.Service
import grails.gorm.services.Join
import grails.gorm.transactions.Transactional
import groovy.util.logging.Slf4j

interface IPersonDataService {

    Person findByName(String name)

    Integer findPersonAge(String name)

    Number count()

    @Join('addresses') (1)
    Person findEagerly(String name)

    void delete(Serializable id)

    Person save(Person person)

    Person save(String name, Integer age)

}

@Service(Person)
abstract class PersonDataService implements IPersonDataService {

    @Transactional
    Person saveWithListOfAddressesMap(String name, Integer age, List<Map<String, Object>> addresses) {
        saveWithAddresses(name, age, addresses.collect { Map m ->
            new Address(streetName:  m.streetName as String,
                city:  m.city as String,
                state: m.state as String,
                country: m.country as String)
        } as List<Address>)
    }

    @Transactional
    Person saveWithAddresses(String name, Integer age, List<Address> addresses) {
        Person person = new Person(name: name, age: age)
        addresses.each { Address address ->
            person.addToAddresses(address)
        }
        save(person)
    }
}

Create an integration test:

src/integration-test/groovy/example/grails/PersonDataServiceIntSpec.groovy
package example.grails

import grails.testing.mixin.integration.Integration
import spock.lang.Specification

@Integration (1)
class PersonDataServiceIntSpec extends Specification {

    PersonDataService personDataService (2)

    void "test join eager load"() {
        given:
        Person p = personDataService.saveWithListOfAddressesMap('Nirav',39, [
                [streetName: "101 Main St", city: "Grapevine", state: "TX", country: "USA"],
                [streetName: "2929 Pearl St", city: "Austin", state: "TX", country: "USA"],
                [streetName: "21 Sewickly Hills Dr", city: "Sewickley", state: "PA", country: "USA"]
        ])

        when:
        Person person = personDataService.findEagerly("Nirav")

        then:
        person.name == "Nirav"
        person.age == 39

        when:
        List<String> cities = person.addresses*.city

        then:
        cities.contains("Grapevine")
        cities.contains("Austin")
        cities.contains("Sewickley")

        cleanup:
        personDataService.delete(p.id)

    }

}
1 Place your integration tests in src/integration-test and annotate them with grails.testing.mixin.integration.Integration.
2 Inject the Data Service.

In order to observe how Data Service operations translate into SQL statements, we will adjust the logger configuration.

In the logback.groovy, append the next line line to get a more verbose output of the SQL queries being executed:

grails-app/conf/logback.groovy
logger 'org.hibernate.SQL', TRACE, ['STDOUT'], false

Execute the integration test ./gradlew integrationTest --tests example.grails.PersonDataServiceIntSpec

The previous tests outputted SQL log statement which demonstrates a join has been issued. Below is a sample.

log output
2018-08-20 16:28:40.460 DEBUG --- [    Test worker] org.hibernate.SQL                        : insert into person (id, version, age, name) values (null, ?, ?, ?)
2018-08-20 16:28:40.472 DEBUG --- [    Test worker] org.hibernate.SQL                        : insert into address (id, version, person_id, street_name, city, country, state) values (null, ?, ?, ?, ?, ?, ?)
2018-08-20 16:28:40.474 DEBUG --- [    Test worker] org.hibernate.SQL                        : insert into address (id, version, person_id, street_name, city, country, state) values (null, ?, ?, ?, ?, ?, ?)
2018-08-20 16:28:40.474 DEBUG --- [    Test worker] org.hibernate.SQL                        : insert into address (id, version, person_id, street_name, city, country, state) values (null, ?, ?, ?, ?, ?, ?)
2018-08-20 16:28:40.533 DEBUG --- [    Test worker] org.hibernate.SQL                        : select this_.id as id1_1_1_, this_.version as version2_1_1_, this_.age as age3_1_1_, this_.name as name4_1_1_, addresses2_.person_id as person_i3_0_3_, addresses2_.id as id1_0_3_, addresses2_.id as id1_0_0_, addresses2_.version as version2_0_0_, addresses2_.person_id as person_i3_0_0_, addresses2_.street_name as street_n4_0_0_, addresses2_.city as city5_0_0_, addresses2_.country as country6_0_0_, addresses2_.state as state7_0_0_ from person this_ left outer join address addresses2_ on this_.id=addresses2_.person_id where this_.name=?

As you can see only one select query is performed. A query which joins the addresses.

You can see those statements in the tests reports build/reports/tests/classes/example.grails.PersonDataServiceIntSpec.html standard output.

6 JPA-QL Query

JPA-QL (JPA Query Language) queries query the entity model using a SQL-like text query language. Queries are expressed using entities, properties, and relationship.

GORM Data Service support JPA-QL Queries via the grails.gorm.services.Query annotation.

JPA-QL queries allows for more complicated queries. Nonetheless, they are still statically compiled and safeguard against SQL injection attacks.

Create a query that searches people by country.

grails-app/services/example/grails/PersonDataService.groovy
    (1)
    @Query("""\
select distinct ${p}
from ${Person p} join ${p.addresses} a
where a.country = $country
""") (2)
    List<Person> findAllByCountry(String country) (3)
1 @Query used to define a JPA-QL query.
2 You can use multiline strings to define your JPA-QL queries which adds readability.
3 country param can be used within the HQL query.

Create an integration test:

src/integration-test/groovy/example/grails/PersonDataServiceIntSpec.groovy
package example.grails

import grails.testing.mixin.integration.Integration
import spock.lang.Specification

@Integration (1)
class PersonDataServiceIntSpec extends Specification {

    PersonDataService personDataService (2)

    void "test search persons by country"() {
        given:
        [
                [name: 'Nirav', age: 39, addresses: [
                        [streetName: "101 Main St", city: "Grapevine", state: "TX", country: "USA"],
                        [streetName: "2929 Pearl St", city: "Austin", state: "TX", country: "USA"],
                        [streetName: "21 Sewickly Hills Dr", city: "Sewickley", state: "PA", country: "USA"]]],
                [name: 'Jeff', age: 50, addresses: [
                        [streetName: "888 Olive St", city: "St Louis", state: "MO", country: "USA"],
                        [streetName: "1515 MLK Blvd", city: "Austin", state: "TX", country: "USA"]]],
                [name: 'Sergio', age: 35, addresses: [
                        [streetName: "19001 Calle Mayor", city: "Guadalajara", state: 'Castilla-La Mancha', country: "Spain"]]]
        ].each { Map<String, Object> m ->
            personDataService.saveWithListOfAddressesMap(m.name as String,
                    m.age as Integer,
                    m.addresses as List<Map<String, Object>>)
        }

        when:
        List<Person> usaPersons = personDataService.findAllByCountry("USA")

        then:
        usaPersons
        usaPersons.size() == 2
        usaPersons.find { it.name == "Nirav"}
        usaPersons.find { it.name == "Jeff"}

        when:
        List<Person> spainPersons = personDataService.findAllByCountry("Spain")

        then:
        spainPersons.size() == 1
        spainPersons.find { it.name == "Sergio"}
    }

7 JPA-QL Projection

You can also create a projection to a POGO (Plan Groovy Object) with JPA-QL.

If you have a POGO such as:

src/main/groovy/example/grails/Country.groovy
package example.grails

import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor

@TupleConstructor (1)
@CompileStatic
class Country {
    String name
}
1 Use Groovy’s TupleConstructor AST Transform to generate a constructor with one parameter.

Create a GORM Data Service for Address domain class:

grails-app/services/example/grails/AddressDataService.groovy
package example.grails

import grails.gorm.services.Query
import grails.gorm.services.Service

@Service(Address)
interface AddressDataService {

    @Query("select new example.grails.Country(${a.country}) from ${Address a} group by ${a.country}") (1)
    List<Country> findCountries()

}
1 The constructor generated by TupleConstructor is used in the JPA-QL query.

You can create a unit test to verify the behaviour:

src/test/groovy/example/grails/AddressDataServiceSpec.groovy
package example.grails

import grails.test.hibernate.HibernateSpec
import spock.lang.Shared

class AddressDataServiceSpec extends HibernateSpec {
    @Shared
    AddressDataService addressDataService

    @Shared
    PersonDataService personDataService

    def setup() {
        addressDataService = hibernateDatastore.getService(AddressDataService)
        personDataService = hibernateDatastore.getService(PersonDataService)
    }

    def "projections to POGO work"() {
        given:
        List<Long> personIds = []
        [
                [name: 'Nirav', age: 39, addresses: [
                        [streetName: "101 Main St", city: "Grapevine", state: "TX", country: "USA"],
                        [streetName: "2929 Pearl St", city: "Austin", state: "TX", country: "USA"],
                        [streetName: "21 Sewickly Hills Dr", city: "Sewickley", state: "PA", country: "USA"]]],
                [name: 'Jeff', age: 50, addresses: [
                        [streetName: "888 Olive St", city: "St Louis", state: "MO", country: "USA"],
                        [streetName: "1515 MLK Blvd", city: "Austin", state: "TX", country: "USA"]]],
                [name: 'Sergio', age: 35, addresses: [
                        [streetName: "19001 Calle Mayor", city: "Guadalajara", state: 'Castilla-La Mancha', country: "Spain"]]]
        ].each { Map<String, Object> m ->
            personIds << personDataService.saveWithListOfAddressesMap(m.name as String,
                    m.age as Integer,
                    m.addresses as List<Map<String, Object>>).id
        }

        when:
        List<Country> countries = addressDataService.findCountries()

        then:
        countries
        countries.size() == 2
        countries.any { it.name == 'USA' }
        countries.any { it.name == 'Spain' }

        cleanup:
        personIds.each {
            personDataService.delete(it)
        }
    }
}

8 Conclusion

Allow GORM to implement the methods for you and manage transactions, while you simply define the interface. Data Services can reduce errors, performance issues and are testable. Plus it has a high COOL factor!

Read GORM Data Services documentation to learn more.

Watch Graeme Rocher presentation at Greach Conf 2018:

9 Help with Grails

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team