grails create-service helloworld.simple
12 The Service Layer
Version: 6.2.1
Table of Contents
12 The Service Layer
Grails defines the notion of a service layer. The Grails team discourages the embedding of core application logic inside controllers, as it does not promote reuse and a clean separation of concerns.
Services in Grails are the place to put the majority of the logic in your application, leaving controllers responsible for handling request flow with redirects and so on.
Creating a Service
You can create a Grails service by running the create-service command from the root of your project in a terminal window:
If no package is specified with the create-service script, Grails automatically uses the grails.defaultPackage defined in grails-app/conf/application.yml as the package name.
|
The above example will create a service at the location grails-app/services/helloworld/SimpleService.groovy
. A service’s name ends with the convention Service
, other than that a service is a plain Groovy class:
package helloworld
class SimpleService {
}
12.1 Declarative Transactions
Declarative Transactions
Services are typically involved with coordinating logic between domain classes, and hence often involved with persistence that spans large operations. Given the nature of services, they frequently require transactional behaviour. You can use programmatic transactions with the withTransaction method, however this is repetitive and doesn’t fully leverage the power of Spring’s underlying transaction abstraction.
Services enable transaction demarcation, which is a declarative way of defining which methods are to be made transactional. To enable transactions on a service use the Transactional
transform:
import grails.gorm.transactions.*
@Transactional
class CountryService {
}
The result is that all methods are wrapped in a transaction and automatic rollback occurs if a method throws an exception (both Checked or Runtime exceptions) or an Error. The propagation level of the transaction is by default set to PROPAGATION_REQUIRED.
Version Grails 3.2.0 was the first version to use GORM 6 by default. Checked exceptions did not roll back transactions before GORM 6. Only a method which threw a runtime exception (i.e. one that extends RuntimeException) rollbacked a transaction. |
Warning: dependency injection is the only way that declarative transactions work. You will not get a transactional service if you use the new operator such as new BookService()
|
The Transactional annotation vs the transactional property
In versions of Grails prior to Grails 3.1, Grails created Spring proxies and used the transactional
property to enable and disable proxy creation. These proxies are disabled by default in applications created with Grails 3.1 and above in favor of the @Transactional
transformation.
For versions of Grails 3.1.x and 3.2.x, if you wish to renable this feature (not recommended) then you must set grails.spring.transactionManagement
to true or remove the configuration in grails-app/conf/application.yml
or grails-app/conf/application.groovy
.
In Grails 3.3.x Spring proxies for transaction management has been dropped completely, and you must use Grails' AST transforms. In Grails 3.3.x, if you wish to continue to use Spring proxies for transaction management you will have to configure them manually, using the appropriate Spring configuration.
In addition, prior to Grails 3.1 services were transactional by default, as of Grails 3.1 they are only transactional if the @Transactional transformation is applied.
|
Custom Transaction Configuration
Grails also provides @Transactional
and @NotTransactional
annotations for cases where you need more fine-grained control over transactions at a per-method level or need to specify an alternative propagation level. For example, the @NotTransactional
annotation can be used to mark a particular method to be skipped when a class is annotated with @Transactional
.
Annotating a service method with Transactional disables the default Grails transactional behavior for that service (in the same way that adding transactional=false does) so if you use any annotations you must annotate all methods that require transactions.
|
In this example listBooks
uses a read-only transaction, updateBook
uses a default read-write transaction, and deleteBook
is not transactional (probably not a good idea given its name).
import grails.gorm.transactions.Transactional
class BookService {
@Transactional(readOnly = true)
def listBooks() {
Book.list()
}
@Transactional
def updateBook() {
// ...
}
def deleteBook() {
// ...
}
}
You can also annotate the class to define the default transaction behavior for the whole service, and then override that default per-method:
import grails.gorm.transactions.Transactional
@Transactional
class BookService {
def listBooks() {
Book.list()
}
def updateBook() {
// ...
}
def deleteBook() {
// ...
}
}
This version defaults to all methods being read-write transactional (due to the class-level annotation), but the listBooks
method overrides this to use a read-only transaction:
import grails.gorm.transactions.Transactional
@Transactional
class BookService {
@Transactional(readOnly = true)
def listBooks() {
Book.list()
}
def updateBook() {
// ...
}
def deleteBook() {
// ...
}
}
Although updateBook
and deleteBook
aren’t annotated in this example, they inherit the configuration from the class-level annotation.
For more information refer to the section of the Spring user guide on Using @Transactional.
Unlike Spring you do not need any prior configuration to use Transactional
; just specify the annotation as needed and Grails will detect them up automatically.
Transaction status
An instance of TransactionStatus is available by default in Grails transactional service methods.
Example:
import grails.gorm.transactions.Transactional
@Transactional
class BookService {
def deleteBook() {
transactionStatus.setRollbackOnly()
}
}
12.1.1 Transactions and Multi-DataSources
Given two domain classes such as:
class Movie {
String title
}
class Book {
String title
static mapping = {
datasource 'books'
}
}
You can supply the desired data source to @Transactional
or @ReadOnly
annotations.
import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic
@CompileStatic
class BookService {
@ReadOnly('books')
List<Book> findAll() {
Book.where {}.findAll()
}
@Transactional('books')
Book save(String title) {
Book book = new Book(title: title)
book.save()
book
}
}
@CompileStatic
class MovieService {
@ReadOnly
List<Movie> findAll() {
Movie.where {}.findAll()
}
}
12.1.2 Transactions Rollback and the Session
Understanding Transactions and the Hibernate Session
When using transactions there are important considerations you must take into account with regards to how the underlying persistence session is handled by Hibernate. When a transaction is rolled back the Hibernate session used by GORM is cleared. This means any objects within the session become detached and accessing uninitialized lazy-loaded collections will lead to a LazyInitializationException
.
To understand why it is important that the Hibernate session is cleared. Consider the following example:
class Author {
String name
Integer age
static hasMany = [books: Book]
}
If you were to save two authors using consecutive transactions as follows:
Author.withTransaction { status ->
new Author(name: "Stephen King", age: 40).save()
status.setRollbackOnly()
}
Author.withTransaction { status ->
new Author(name: "Stephen King", age: 40).save()
}
Only the second author would be saved since the first transaction rolls back the author save()
by clearing the Hibernate session. If the Hibernate session were not cleared then both author instances would be persisted and it would lead to very unexpected results.
It can, however, be frustrating to get a LazyInitializationException
due to the session being cleared.
For example, consider the following example:
class AuthorService {
void updateAge(id, int age) {
def author = Author.get(id)
author.age = age
if (author.isTooOld()) {
throw new AuthorException("too old", author)
}
}
}
class AuthorController {
def authorService
def updateAge() {
try {
authorService.updateAge(params.id, params.int("age"))
}
catch(e) {
render "Author books ${e.author.books}"
}
}
}
In the above example the transaction will be rolled back if the age of the Author
age exceeds the maximum value defined in the isTooOld()
method by throwing an AuthorException
. The AuthorException
references the author but when the books
association is accessed a LazyInitializationException
will be thrown because the underlying Hibernate session has been cleared.
To solve this problem you have a number of options. One is to ensure you query eagerly to get the data you will need:
class AuthorService {
...
void updateAge(id, int age) {
def author = Author.findById(id, [fetch:[books:"eager"]])
...
In this example the books
association will be queried when retrieving the Author
.
This is the optimal solution as it requires fewer queries than the following suggested solutions. |
Another solution is to redirect the request after a transaction rollback:
class AuthorController {
AuthorService authorService
def updateAge() {
try {
authorService.updateAge(params.id, params.int("age"))
}
catch(e) {
flash.message = "Can't update age"
redirect action:"show", id:params.id
}
}
}
In this case a new request will deal with retrieving the Author
again. And, finally a third solution is to retrieve the data for the Author
again to make sure the session remains in the correct state:
class AuthorController {
def authorService
def updateAge() {
try {
authorService.updateAge(params.id, params.int("age"))
}
catch(e) {
def author = Author.read(params.id)
render "Author books ${author.books}"
}
}
}
Validation Errors and Rollback
A common use case is to rollback a transaction if there are validation errors. For example consider this service:
import grails.validation.ValidationException
class AuthorService {
void updateAge(id, int age) {
def author = Author.get(id)
author.age = age
if (!author.validate()) {
throw new ValidationException("Author is not valid", author.errors)
}
}
}
To re-render the same view that a transaction was rolled back in you can re-associate the errors with a refreshed instance before rendering:
import grails.validation.ValidationException
class AuthorController {
def authorService
def updateAge() {
try {
authorService.updateAge(params.id, params.int("age"))
}
catch (ValidationException e) {
def author = Author.read(params.id)
author.errors = e.errors
render view: "edit", model: [author:author]
}
}
}
12.2 Scoped Services
By default, access to service methods is not synchronised, so nothing prevents concurrent execution of those methods. In fact, because the service is a singleton and may be used concurrently, you should be very careful about storing state in a service. Or take the easy (and better) road and never store state in a service.
You can change this behaviour by placing a service in a particular scope. The supported scopes are:
-
prototype
- A new service is created every time it is injected into another class -
request
- A new service will be created per request -
flash
- A new service will be created for the current and next request only -
flow
- In web flows the service will exist for the scope of the flow -
conversation
- In web flows the service will exist for the scope of the conversation. ie a root flow and its sub flows -
session
- A service is created for the scope of a user session -
singleton
(default) - Only one instance of the service ever exists
If your service is flash , flow or conversation scoped it must implement java.io.Serializable and can only be used in the context of a Web Flow.
|
To enable one of the scopes, add a static scope property to your class whose value is one of the above, for example
static scope = "flow"
Upgrading
Starting with Grails 2.3, new applications are generated with configuration that defaults the scope of controllers to See Controllers and Scopes in the user guide for more information. |
Lazy initialization
You can also configure whether the service is lazily initialized. By default, this is set to true
, but you can disable this and make initialization eager with the lazyInit
property:
static lazyInit = false
12.3 Dependency Injection and Services
Dependency Injection Basics
A key aspect of Grails services is the ability to use Spring Framework's dependency injection features. Grails supports "dependency injection by convention". In other words, you can use the property name representation of the class name of a service to automatically inject them into controllers, tag libraries, and so on.
As an example, given a service called BookService
, if you define a property called bookService
in a controller as follows:
class BookController {
def bookService
...
}
In this case, the Spring container will automatically inject an instance of that service based on its configured scope. All dependency injection is done by name. You can also specify the type as follows:
class AuthorService {
BookService bookService
}
NOTE: Normally the property name is generated by lower casing the first letter of the type. For example, an instance of the BookService class would map to a property named bookService .
|
To be consistent with standard JavaBean conventions, if the first 2 letters of the class name are upper case, the property name is the same as the class name. For example, the property name of the JDBCHelperService
class would be JDBCHelperService
, not jDBCHelperService
or jdbcHelperService
.
See section 8.8 of the JavaBean specification for more information on de-capitalization rules.
Only the top level object is subjected to injection as traversing all nested objects to perform injection would be a performance issue. |
Be careful when injecting the non-default datasources. For example, using this config:
dataSources:
dataSource:
pooled: true
jmxExport: true
.....
secondary:
pooled: true
jmxExport: true
.....
You can inject the primary dataSource
like you would expect:
class BookSqlService {
def dataSource
}
But to inject the secondary
datasource, you have to use Spring’s Autowired
injection or resources.groovy
.
class BookSqlSecondaryService {
@Autowired
@Qualifier('dataSource_secondary')
def dataSource2
}
Dependency Injection and Services
You can inject services in other services with the same technique. If you had an AuthorService
that needed to use the BookService
, declaring the AuthorService
as follows would allow that:
class AuthorService {
def bookService
}
Dependency Injection and Domain Classes / Tag Libraries
You can even inject services into domain classes and tag libraries, which can aid in the development of rich domain models and views:
class Book {
...
def bookService
def buyBook() {
bookService.buyBook(this)
}
}
Since Grails 3.2.8 this is not enabled by default. If you want to enable it again, take a look at Spring Autowiring of Domain Instance |
Service Bean Names
The default bean name which is associated with a service can be problematic if there are multiple services with the same name defined in different packages. For example consider the situation where an application defines a service class named com.demo.ReportingService
and the application uses a plugin named ReportingUtilities
and that plugin provides a service class named com.reporting.util.ReportingService
.
The default bean name for each of those would be reportingService
so they would conflict with each other. Grails manages this by changing the default bean name for services provided by plugins by prefixing the bean name with the plugin name.
In the scenario described above the reportingService
bean would be an instance of the com.demo.ReportingService
class defined in the application and the reportingUtilitiesReportingService
bean would be an instance of the com.reporting.util.ReportingService
class provided by the ReportingUtilities
plugin.
For all service beans provided by plugins, if there are no other services with the same name within the application or other plugins in the application then a bean alias will be created which does not include the plugin name and that alias points to the bean referred to by the name that does include the plugin name prefix.
For example, if the ReportingUtilities
plugin provides a service named com.reporting.util.AuthorService
and there is no other AuthorService
in the application or in any of the plugins that the application is using then there will be a bean named reportingUtilitiesAuthorService
which is an instance of this com.reporting.util.AuthorService
class and there will be a bean alias defined in the context named authorService
which points to that same bean.