(Quick Reference)

10 GORM Data Services

Version: 2023.3.0-SNAPSHOT

10 GORM Data Services

Introduced in GORM 6.1, 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.

To illustrate what GORM Data Services are about let’s walk through an example.

10.1 Data Service Basics

Writing a Simple Data Service

In a Grails application you can create a Data Service in either src/main/groovy or app/services. To write a Data Service you should create either an interface (although abstract classes can also be used, more about that later) and annotate it with the grails.gorm.services.Service annotation with the domain class the service applies to:

@Service(Book)
interface BookService {
    Book getBook(Serializable id)
}

The @Service annotation is an AST transformation that will automatically implement the service for you. You can then obtain the service via Spring autowiring:

@Autowired BookService bookService

Or if you are using GORM standalone by looking it up from the HibernateDatastore instance:

BookService bookService = hibernateDatastore.getService(BookService)
The above example also works in Spock unit tests that extend HibernateSpec

How Does it Work?

The @Service transformation will look at the the method signatures of the interface and make a best effort to find a way to implement each method.

If a method cannot be implemented then a compilation error will occur. At this point you have the option to use an abstract class instead and provide an implementation yourself.

The @Service transformation will also generate a META-INF/services file for the service so it can be discovered via the standard Java service loader. So no additional configuration is necessary.

Advantages of Data Services

There are several advantages to Data Services that make them worth considering to abstract your persistence logic.

  • 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 test via Spock Mocks

  • 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

  • Transaction Management - Each method in a Data Service is wrapped in an appropriate transaction (a read-only transaction in the case of read operations) that can be easily overridden.

Abstract Class Support

If you come across a method that GORM doesn’t know how to implement, then you can provide an implementation by using an abstract class.

For example:

interface IBookService {
    Book getBook(Serializable id)
    Date someOtherMethod()
}
@Service(Book)
abstract class BookService implements IBookService {

   @Override
   Date someOtherMethod() {
      // impl
   }
}

In this case GORM will implement the interface methods that have not been defined by the abstract class.

In addition, all public methods of the domain class will be automatically wrapped in the appropriate transaction handling.

What this means is that you can define protected abstract methods that are non-transactional in order to compose logic. For example:

@Service(Book)
abstract class BookService  {

   protected abstract Book getBook(Serializable id) (1)

   protected abstract Author getAuthor(Serializable id) (1)

   Book updateBook(Serializable id, Serializable authorId) { (2)
      Book book = getBook(id)
      if(book != null) {
          Author author = getAuthor(authorId)
          if(author == null) {
              throw new IllegalArgumentException("Author does not exist")
          }
          book.author = author
          book.save()
      }
      return book
   }
}
1 Two protected abstract methods are defined that are not wrapped in transaction handling
2 The updateBook method uses the two methods that are implemented automatically by GORM and being public is automatically made transactional.
If you have public methods that you do not wish to be transactional, then you can annotate them with @NotTransactional

10.2 Data Service Queries

GORM Data Services will implement queries for you using a number of different strategies and conventions.

It does this by looking at the return type of a method and the method stem and picking the most appropriate implementation.

The following table summarizes the conventions:

Table 1. Data Service Conventions
Method Stem Description Possible Return Types

count*

Count the number of results

Subclass of Number or Observable<Number>

countBy*

Dynamic Finder Count the number of results

Subclass of Number or Observable<Number>

delete*

Delete an instance for the given arguments

T, void, subclass of Number or Observable<Number>

find*, get*, list* or retrieve*

Query for the given parameters

T, Iterable<T>, T[], List<T>, Observable<T>

findBy*, listBy*, findAllBy* or getBy*

Dynamic finder query for given parameters

T, Iterable<T>, T[], List<T>, Observable<T>

save*, store*, or persist*

Save a new instance

T or Observable<T>

update*

Updates an existing instance. First parameter should be id

T or Observable<T>

The conventions are extensible (more on that later), in terms of queries there are two distinct types.

Simple Queries

Simple queries are queries that use the arguments of the method. For example:

@Service(Book)
interface BookService {
    Book findBook(String title)
}

In the example above the Data Service will generate the implementation based on the fact that the title parameter matches the title property of the Book class both in terms of name and type.

If you were to misspell the title parameter or use an incorrect type then a compilation error will occur.

You can alter the return type to return more results:

@Service(Book)
interface BookService {
    List<Book> findBooks(String title)
}

And if you wish to control pagination and query arguments you can add an args parameter that should be a Map:

@Service(Book)
interface BookService {
    List<Book> findBooks(String title, Map args)
}

In this case the following query will control pagination and ordering:

List<Book> books = bookService.findBooks(
    "The Stand",
    [offset:10, max:10, sort:'title', order:'desc']
)

You can include multiple parameters in the query:

@Service(Book)
interface BookService {
    List<Book> findBooks(String title, Date publishDate)
}

In this case a conjunction (AND) query will be executed. If you need to do a disjunction (OR) then it is time you learn about Dynamic Finder-style queries.

Dynamic Finder Queries

Dynamic finder styles queries use the stem plus the word By and then a Dynamic Finder expression.

For example:

@Service(Book)
interface BookService {
    List<Book> findByTitleAndPublishDateGreaterThan(String title, Date publishDate)
}

The signature above will produce a dynamic finder query using the method signature expression.

The possible method expressions are the same as those possible with GORM’s static Dynamic Finders

In this case the names of the properties to query are inferred from the method signature and the parameter names are not critical. If you misspell the method signature a compilation error will occur.

Where Queries

If you have a more complex query then you may want to consider using the @Where annotation:

    @Where({ title ==~ pattern && releaseDate > fromDate })
    Book searchBooks(String pattern, Date fromDate)

With the @Where annotation the method name can be anything you want and query is expressed within a closure passed with the @Where annotation.

The query will be type checked against the parameters and compilation will fail if you misspell a parameter or property name.

The syntax is the same as what is passed to GORM’s static where method, see the section on Where Queries for more information.

10.3 Query Joins

You can specify query joins using the @Join annotation:

import static javax.persistence.criteria.JoinType.*

@Service(Book)
interface BookService {
    @Join('author')
    Book find(String title) (1)

    @Join(value='author', type=LEFT) (2)
    Book findAnother(String title)
}
1 Join on the author property
2 Join on the author property using a LEFT OUTER join

JPA-QL Queries

If you need even more flexibility, then HQL queries can be used via the @Query annotation:

@Query("from $Book as book where book.title like $pattern")
Book searchByTitle(String pattern)

Note that in the example above, if you misspell the pattern parameter passed to the query a compilation error will occur.

However, if you were to incorrectly input the title property no error would occur since it is part of the String and not a variable.

You can resolve this by declaring the value of book within the passed GString:

@Query("from ${Book book} where ${book.title} like $pattern")
Book searchByTitle(String pattern)

In the above example if you misspell the title property then a compilation error will occur. This is extremely powerful as it gives you the ability to type check HQL queries, which has always been one of the disadvantages of using them in comparison to criteria.

This support for type checked queries extends to joins. For example consider this query:

@Query("""
 from ${Book book} (1)
 inner join ${Author author = book.author} (2)
 where $book.title = $title and $author.name = $author""") (3)
Book find(String title, String author)
1 Using from to define the root query
2 Use inner join and a declaration to define the association to join on
3 Apply any conditions in the where clause

Query Projections

There are a few ways to implement projections. One way is to is to use the convention T find[Domain Class][Property]. For example say the Book class has a releaseDate property of type Date:

@Service(Book)
interface BookService {
   Date findBookReleaseDate(String title)
}

This also works for multiple results:

@Service(Book)
interface BookService {
   List<Date> findBookReleaseDate(String publisher)
}

And you can use the Map argument to provide ordering and pagination if necessary:

@Service(Book)
interface BookService {
   List<Date> findBookReleaseDate(String publisher, Map args)
}

JPA-QL Projections

You can also use a JPA-QL query to perform a projection:

@Service(Book)
interface BookService {
   @Query("select $b.releaseDate from ${Book b} where $b.publisher = $publisher order by $b.releaseDate")
   List<Date> findBookReleaseDates(String publisher)
}

Interface Projections

Sometimes you want to expose a more limited set of a data to the calling class. In this case it is possible to use interface projections.

For example:

class Author {
    String name
    Date dateOfBirth (1)
}
interface AuthorInfo {
    String getName() (2)
}
@Service(Author)
interface AuthorService {
   AuthorInfo find(String name) (3)
}
1 The domain class Author has a property called dateOfBirth that we do not want to make available to the client
2 You can define an interface that only exposes the properties you want to expose
3 Return the interface from the service.
If a property exists on the interface but not on the domain class you will receive a compilation error.

10.4 Data Service Write Operations

Write operations in Data Services are automatically wrapped in a transaction. You can modify the transactional attributes by simply adding the @Transactional transformation to any method.

The following sections discuss the details of the different write operations.

Create

To create a new entity the method should return the new entity and feature either the parameters to be used to create the entity or the entity itself.

For example:

@Service(Book)
interface BookService {
    Book saveBook(String title)

    Book saveBook(Book newBook)
}

If any of the parameters don’t match up to a property on the domain class then a compilation error will occur.

If a validation error occurs then a ValidationException will be thrown from the service.

Update

Update operations are similar to Create operations, the main difference being that the first argument should be the id of the object to update.

For example:

@Service(Book)
interface BookService {
    Book updateBook(Serializable id, String title)
}

If any of the parameters don’t match up to a property on the domain class then a compilation error will occur.

If a validation error occurs then a ValidationException will be thrown from the service.

You can also implement update operations using JPA-QL:

@Query("update ${Book book} set ${book.title} = $newTitle where $book.title = $oldTitle")
Number updateTitle(String newTitle, String oldTitle)

Delete

Delete operations can either return void or return the instance that was deleted. In the latter case an extra query is required to fetch the entity prior to issue a delete.

@Service(Book)
interface BookService {
    Number deleteAll(String title)

    void delete(Serializable id)
}

You can also implement delete operations using JPA-QL:

@Query("delete ${Book book} where $book.title = $title")
void delete(String title)

Or via where queries:

@Where({ title == title && releaseDate > date })
void delete(String title, Date date)

10.5 Validating Data Services

GORM Data Services have built in support for javax.validation annotations for method parameters.

You will need to have a javax.validation implementation on your classpath (such as hibernate-validator and then simply annotate your method parameters using the appropriate annotation. For example:

import javax.validation.constraints.*

@Service(Book)
interface BookService {

    Book find(@NotNull String title)
}

In the above example the NotNull constraint is applied to the title property. If null is passed to the method a ConstraintViolationException exception will be thrown.

10.6 RxJava Support

GORM Data Services also support returning RxJava 1.x rx.Observable or rx.Single types.

RxJava 2.x support is planned for a future release

To use the RxJava support you need to ensure that the grails-datastore-gorm-rx dependencies is on the classpath by adding the following to build.gradle:

build.gradle
compile "org.grails:grails-datastore-gorm-rx:2023.3.0-SNAPSHOT"

For example:

import rx.*

@Service(Book)
interface BookService {
   Single<Book> findOne(String title)
}

When a rx.Single is used then a single result is returned. To query multiple results use an rx.Observable instead:

import rx.*

@Service(Book)
interface BookService {
   Observable<Book> findBooks(String title)
}

For regular GORM entities, GORM will by default execute the persistence operation using RxJava’s IO Scheduler.

For RxGORM entities where the underlying database supports non-blocking access the database driver will schedule the operation accordingly.

You can run the operation on a different scheduler using the RxSchedule annotation:

import rx.*
import grails.gorm.rx.services.RxSchedule
import grails.gorm.services.Service
import rx.schedulers.Schedulers

@Service(Book)
interface BookService {

   @RxSchedule(scheduler = { Schedulers.newThread() })
   Observable<Book> findBooks(String title)
}