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.

Data Service Basics

Writing a Simple Data Service

In a Grails application you can create a Data Service in either src/main/groovy or grails-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

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.

Query Joins

You can specify query joins using the @Join annotation:

import static jakarta.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.

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)

Validating Data Services

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

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

import jakarta.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.

Data Services and Multiple Datasources

When using Data Services with multiple datasources, the service must declare which connection to use via the connection parameter of @Transactional.

Routing to a Secondary Datasource

Given a domain class mapped to a secondary datasource:

class Book {

    String title
    String author

    static mapping = {
        datasource 'books'
    }
}

Define an interface for your data access methods and an abstract class that declares the connection:

import grails.gorm.services.Service

interface BookDataService {

    Book get(Serializable id)

    Book save(Book book)

    void delete(Serializable id)

    List<Book> findAllByAuthor(String author)

    Long count()
}
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Service(Book)
@Transactional(connection = 'books')
abstract class BookService implements BookDataService {
    // All interface methods are auto-implemented by GORM
    // and route to the 'books' datasource automatically.
}

The @Transactional(connection = 'books') annotation on the abstract class ensures that all auto-implemented methods (get, save, delete, findBy*, countBy*, etc.) route to the books datasource. Without this annotation, queries silently route to the default datasource.

The @Service(Book) annotation identifies the domain class but does not determine which datasource to use. Even if Book declares datasource 'books' in its mapping block, you must specify @Transactional(connection = 'books') on the abstract class to route operations to the correct datasource.

How Connection Routing Works

When GORM compiles a @Service abstract class, the ServiceTransformation AST transform:

  1. Copies the @Transactional(connection = 'books') annotation from the abstract class to the generated implementation class

  2. For each auto-implemented method, resolves the connection identifier via findConnectionId()

  3. Generates method bodies that use the appropriate connection, routing CRUD operations and DetachedCriteria-based finder queries to the specified datasource

This means auto-implemented methods like get(), save(), delete(), findBy*(), countBy*(), @Where-annotated methods, @Query-annotated methods, and DetachedCriteria-based queries all respect the connection parameter without requiring manual implementations.

Complex Queries Using Domain Static Methods

Auto-implemented Data Service methods cover most query patterns, including dynamic finders with comparators, pagination, and property projections. For queries that require HQL, criteria builders, or aggregate functions, call domain static methods directly. When a domain class declares a non-default datasource in its mapping block, GORM registers its static API under that datasource. Methods like Book.executeQuery(), Book.createCriteria(), and Book.withCriteria() automatically route to the correct datasource:

import groovy.transform.CompileStatic
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional

@CompileStatic
@Service(Book)
@Transactional(connection = 'books')
abstract class BookService implements BookDataService {

    // Auto-implemented methods from interface are inherited
    // and route to 'books' datasource automatically.

    List getTopAuthors(int limit) {
        Book.executeQuery('''
            SELECT b.author, COUNT(b) as bookCount
            FROM Book b
            GROUP BY b.author
            ORDER BY bookCount DESC
        ''', Collections.emptyMap(), [max: limit])
    }

    List<Book> searchWithCriteria(String titlePattern, String author) {
        Book.createCriteria().list {
            like('title', "%${titlePattern}%")
            eq('author', author)
            order('title', 'asc')
        } as List<Book>
    }
}

These domain static methods all route to the datasource declared in the domain’s mapping block:

Method Description

Book.executeQuery(String hql, Map params)

HQL/JPQL queries

Book.executeUpdate(String hql, Map params)

Bulk UPDATE/DELETE statements

Book.withCriteria(Closure criteria)

Criteria query

Book.createCriteria()

Criteria builder for complex queries

Book.where(Closure query)

Where query

Book.withTransaction(Closure action)

Manual transaction management

Book.withNewSession(Closure action)

Obtain a new session for the domain’s datasource

Book.count()

Total record count

Book.get(Serializable id)

Find by primary key

Book.list(Map params)

Paginated list

Domain static methods work under @CompileStatic and route to the correct datasource automatically. The namespace syntax (e.g., Book.books.get(42)) is only needed when a domain is mapped to multiple datasources and you need to target a specific one.

Consuming Multi-Datasource Data Services

Other services inject the Data Service interface type. Spring resolves the abstract class bean automatically:

import groovy.transform.CompileStatic

@CompileStatic
class LibraryService {

    BookDataService bookDataService  // injected automatically

    Map getLibraryStats() {
        Long totalBooks = bookDataService.count()
        List<Book> recentBooks = bookDataService.findAllByAuthor('Tolkien')
        [total: totalBooks, tolkienBooks: recentBooks.size()]
    }
}

The consuming service does not need @Transactional(connection = 'books'). The Data Service handles datasource routing internally.

Data Services can also inject other Data Services. @CompileStatic works on @Service abstract classes that declare @Service-typed properties:

import grails.gorm.services.Service

interface AuthorDataService {

    Author get(Serializable id)

    Author save(Author author)
}
import groovy.transform.CompileStatic
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional

@CompileStatic
@Service(Author)
@Transactional(connection = 'books')
abstract class AuthorService implements AuthorDataService {

    BookDataService bookDataService  // injected @Service property

    Map getAuthorWithBooks(Serializable authorId) {
        Author author = get(authorId)
        List<Book> books = bookDataService.findAllByAuthor(author.name)
        [author: author, books: books]
    }
}

When the Spring context initializes the generated implementation class, it autowires all @Service-typed properties by type. By the time any user code runs, injected Data Services are fully available.

Multi-Tenancy with Multiple Datasources

Domain classes that use the MultiTenant trait and declare an explicit non-default datasource (e.g., datasource 'analytics') route correctly through Data Services. GORM preserves explicit datasource qualifiers for multi-tenant entities, so Data Services using @Transactional(connection = 'analytics') work the same way as for non-tenant domain classes:

import grails.gorm.MultiTenant

class Metric implements MultiTenant<Metric> {

    String name
    BigDecimal value

    static mapping = {
        datasource 'analytics'
    }
}
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Service(Metric)
@Transactional(connection = 'analytics')
abstract class MetricService {

    abstract Metric get(Serializable id)

    abstract Metric save(Metric metric)

    abstract List<Metric> list()
}

The connection parameter on the abstract class routes all auto-implemented operations - including save(), get(), and delete() - to the analytics datasource, regardless of multi-tenancy mode (DATABASE or DISCRIMINATOR).

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:7.0.10"

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)
}