@Service(Book)
interface BookService {
Book getBook(Serializable id)
}
10 GORM Data Services
Version: 2023.3.0-SNAPSHOT
Table of Contents
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:
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:
Method Stem | Description | Possible Return Types |
---|---|---|
|
Count the number of results |
Subclass of |
|
Dynamic Finder Count the number of results |
Subclass of |
|
Delete an instance for the given arguments |
|
|
Query for the given parameters |
|
|
Dynamic finder query for given parameters |
|
|
Save a new instance |
|
|
Updates an existing instance. First parameter should be |
|
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
:
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)
}