9 The Service Layer - Reference Documentation
Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith
Version: 2.1.0
Table of Contents
9 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:grails create-service helloworld.simple
If no package is specified with the create-service script, Grails automatically uses the application name 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 helloworldclass SimpleService {
}
9.1 Declarative Transactions
Default 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. All services are transactional by default. To disable this set thetransactional
property to false
:class CountryService { static transactional = false }
true
to make it clear that the service is intentionally transactional.Warning: dependency injection is the only way that declarative transactions work. You will not get a transactional service if you use theThe result is that all methods are wrapped in a transaction and automatic rollback occurs if a method throws a runtime exception (i.e. one that extendsnew
operator such asnew BookService()
RuntimeException
) or an Error
. The propagation level of the transaction is by default set to PROPAGATION_REQUIRED.Checked exceptions do not roll back transactions. Even though Groovy blurs the distinction between checked and unchecked exceptions, Spring isn't aware of this and its default behaviour is used, so it's important to understand the distinction between checked and unchecked exceptions.
Custom Transaction Configuration
Grails also fully supports Spring'sTransactional
annotation for cases where you need more fine-grained control over transactions at a per-method level or need specify an alternative propagation level.Annotating a service method withIn this exampleTransactional
disables the default Grails transactional behavior for that service (in the same way that addingtransactional=false
does) so if you use any annotations you must annotate all methods that require transactions.
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 org.springframework.transaction.annotation.Transactionalclass BookService { @Transactional(readOnly = true) def listBooks() { Book.list() } @Transactional def updateBook() { // … } def deleteBook() { // … } }
transactional=true
):import org.springframework.transaction.annotation.Transactional@Transactional
class BookService { def listBooks() {
Book.list()
} def updateBook() {
// …
} def deleteBook() {
// …
}
}
listBooks
method overrides this to use a read-only transaction:import org.springframework.transaction.annotation.Transactional@Transactional class BookService { @Transactional(readOnly = true) def listBooks() { Book.list() } def updateBook() { // … } def deleteBook() { // … } }
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.
9.1.1 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 toLazyInitializationException
s.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] }
Author.withTransaction { status -> new Author(name: "Stephen King", age: 40).save() status.setRollbackOnly() }Author.withTransaction { status -> new Author(name: "Stephen King", age: 40).save() }
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 LazyInitializationException
s 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}" } } }
Author
's 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"]]) ...
books
association will be queried when retrieving the Author
.This is the optimal solution as it requires fewer queries then 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 } } }
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.ValidationExceptionclass 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) } } }
import grails.validation.ValidationExceptionclass 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] } } }
9.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 classrequest
- A new service will be created per requestflash
- A new service will be created for the current and next request onlyflow
- In web flows the service will exist for the scope of the flowconversation
- In web flows the service will exist for the scope of the conversation. ie a root flow and its sub flowssession
- A service is created for the scope of a user sessionsingleton
(default) - Only one instance of the service ever exists
If your service isTo enable one of the scopes, add a static scope property to your class whose value is one of the above, for exampleflash
,flow
orconversation
scoped it must implementjava.io.Serializable
and can only be used in the context of a Web Flow
static scope = "flow"
9.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 calledBookService
, if you define a property called bookService
in a controller as follows:class BookController { def bookService … }
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 theBookService
class would map to a property namedbookService
.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 theJDBCHelperService
class would beJDBCHelperService
, notjDBCHelperService
orjdbcHelperService
.See section 8.8 of the JavaBean specification for more information on de-capitalization rules.
Dependency Injection and Services
You can inject services in other services with the same technique. If you had anAuthorService
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)
}
}
9.4 Using Services from Java
One of the powerful things about services is that since they encapsulate re-usable logic, you can use them from other classes, including Java classes. There are a couple of ways you can reuse a service from Java. The simplest way is to move your service into a package within thegrails-app/services
directory. The reason this is important is that it is not possible to import classes into Java from the default package (the package used when no package declaration is present). So for example the BookService
below cannot be used from Java as it stands:class BookService { void buyBook(Book book) { // logic } }
grails-app/services/bookstore
and then modifying the package declaration:package bookstoreclass BookService {
void buyBook(Book book) {
// logic
}
}
package bookstoreinterface BookStore { void buyBook(Book book) }
class BookService implements bookstore.BookStore {
void buyBook(Book b) {
// logic
}
}
src/java
directory and add a setter that uses the type and the name of the bean in Spring:// src/java/bookstore/BookConsumer.java package bookstore;public class BookConsumer { private BookStore store; public void setBookStore(BookStore storeInstance) { this.store = storeInstance; } … }
grails-app/conf/spring/resources.xml
(for more information see the section on Grails and Spring):<bean id="bookConsumer" class="bookstore.BookConsumer"> <property name="bookStore" ref="bookService" /> </bean>
grails-app/conf/spring/resources.groovy
:import bookstore.BookConsumerbeans = { bookConsumer(BookConsumer) { bookStore = ref("bookService") } }