(クイックリファレンス)

8 サービスレイヤー

Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith ,
Japanese Translation: T.Yamamoto, Japanese Grails Doc Translating Team
このドキュメントの内容はスナップショットバージョンを元に意訳されているため、一部現行バージョンでは未対応の機能もあります。

Version: 2.1.0.BUILD-SNAPSHOT

8 サービスレイヤー

Grailsではサービスレイヤの概念を持っています。再利用や関心事の明確な分離を妨げるといった理由から、Grailsチームは、コントローラ内にアプリケーションのコアロジックを持ち込まないことを推奨します。

Grailsのサービスは、アプリケーション・ロジックの大部分を置く場所と考えられます。コントローラには、リダイレクトなどを介してリクエストの流れを処理する、といった責務が残ります。

サービスを作成する

ターミナルウィンドウで、プロジェクトのルートからcreate-serviceコマンドを実行すると、Grailsのサービスを作成することができます:

grails create-service helloworld.simple

create-serviceスクリプト実行した際にパッケージ名を指定しなかった場合は、アプリケーション名をパッケージ名として使用します。

上記の例では、grails-app/services/helloworld/SimpleService.groovyというサービスが作成されます。サービスの名前は、規約でServiceで終わることになっており、作成されるサービスは、普通のGroovyクラスです

package helloworld

class SimpleService { }

8.1 断定的なトランザクション

デフォルトの宣言的トランザクション

サービスは、典型的にはドメインクラス間の調整的なロジックに関わりますが、それゆえしばしば大規模な操作に渡る永続性に関わります。頻繁にトランザクションの振る舞いを必要とするサービスの性質を考えてください。もちろんwithTransactionメソッドを使ってプログラム的なトランザクションを行うことができますが、これは同じことを何度も繰り返すことになり、Springが持つ基本的なトランザクション抽象化の恩恵を十分活用することができません。

サービスは、トランザクション境界を使用することができ、サービス内のすべてのメソッドをトランザクションにすると宣言する宣言方法です。すべてのサービスは、デフォルトでトランザクション境界が可能です、不可にするにはtransactionalプロパティにfalseを設定します:

class CountryService {
    static transactional = false
}

サービスが意図的にトランザクションであるということを明確にするために、このプロパティにtrueを設定することもできます。

警告:依存性の注入は、宣言的トランザクションが機能する唯一の方法です。new BookService()のようにnew演算子を使用した場合、トランザクションなサービスを取得するできません。

その結果、すべてのメソッドがトランザクションでラップされ、例外がメソッドからスローされると、トランザクションは自動的にロールバックされます。トランザクションの伝播レベルは、デフォルトで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.

カスタムトランザクションの設定

メソッド単位のレベルでトランザクションのよりきめの細かいコントロールを必要とする場合や、別のトランザクション伝播レベルを指定する必要がある場合、GrailsはSpringのTransactionalアノテーションもフルサポートしています:

Transactionalアノテーションをメソッドに指定すると、デフォルトの書式(transactional=falseでの定義)でのトランザクションは無効になります。アノテーションを使用する場合は、トランザクションが必要な全てのメソッドに定義する必要があります。

この例ではlistBooksがread-onlyトランザクションを使用、updateBookがデフォルトのread-writeトランザクションを使用、deleteBookにはトランザクションは指定されていません(このメソッドの名前的に良くはありませんが)。
import org.springframework.transaction.annotation.Transactional

class 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() { // … } }

オーバーライドした、read-onlyトランザクションのlistBooksメソッド以外の、全メソッドがread-writeトランザクションになります(クラスレベルのアノテーションの定義によって)。

import org.springframework.transaction.annotation.Transactional

@Transactional class BookService {

@Transactional(readOnly = true) def listBooks() { Book.list() }

def updateBook() { // … }

def deleteBook() { // … } }

updateBookdeleteBookはアノテーション指定されていませんが、クラスレベルアノテーションの定義が受け継がれます。

さらなる情報はSpringユーザガイドのトランザクションセクションを参照してください。

Springと違いTransactionalを使用するための特別な定義は必要有りません。必要なアノテーションを記述すればGrailsが自動的に認識します。

8.1.1 トランザクションロールバックとセッション

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 LazyInitializationExceptions.

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 LazyInitializationExceptions 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 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"]])
        ...

In this example the 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 } } }

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

8.2 サービスのスコープ

デフォルトでは、サービスが持つメソッドへのアクセスは同期されないので、サービスが持つメソッドの同時実行を防ぐことはできません。実際には、サービスはシングルトンで同時に使用することが出来るため、サービスに状態を格納する場合は注意が必要です。簡単な(そしてより良い)方法は、サービスに状態を決して格納しないことです。

特定のスコープにサービスを置くことで、この振る舞いを変えることができます。サポートされているスコープは次のとおりです:

  • prototype - クラスへの注入時にサービスが生成される
  • request - リクエスト毎にサービスが生成される
  • flash - 現在とその次のリクエスト用にサービスが生成される
  • flow - フローの開始から終了まてのサービス(サブフロー含まず)
  • conversation - フローの開始から終了まてのサービス(サブフロー含む)
  • session - ユーザセッション毎にサービスが生成される
  • singleton (デフォルト) - サービスが1つのみ生成される

サービスのスコープがflashflowあるいはconversationの場合、java.io.Serializableを実装する必要があり、Webフローのコンテキストの中でだけ使うことができます。

スコープのいずれかを有効にするには、クラスにstaticのscopeプロパティを追加し、プロパティの値を上記のいずれかにします:
static scope = "flow"

8.3 依存注入とサービス

依存性の注入の基本

Grailsのサービスの重要な側面は、Springフレームワークが持つ依存性注入機能を活用するための能力です。Grailsは、「規約による依存性の注入」をサポートしています。言い換えれば、サービスのクラス名のプロパティ名表現を使用することで、自動的にコントローラ、タグライブラリなどにサービスが注入されます。

例として、BookServiceというサービスが与えられた場合、次のようにコントローラの中にbookServiceという名前のプロパティを配置します:

class BookController {
    def bookService
    …
}

この場合、Springコンテナが設定されたスコープに基づいてサービスのインスタンスを自動的に注入します。すべての依存性の注入は、名前によって行われます。次のように型を指定することもできます:

class AuthorService {
    BookService bookService
}

注:通常、プロパティ名は、型の最初の文字を小文字のにして生成します。たとえば、BookServiceクラスのインスタンスは、bookServiceという名前のプロパティにマップされます。

標準のJavaBean規約と整合をとるため、クラス名の最初の2文字が大文字の場合、プロパティ名はクラス名と同じになります。たとえばJDBCHelperServiceクラスは、jDBCHelperServiceまたjdbcHelperServiceでは無く、JDBCHelperServiceという名前のプロパティにマップされます。

小文字への変換規則の詳細については、JavaBeanの仕様のセクション8.8を参照してください。

依存性注入とサービス

同じ方法で他のサービスにサービスを注入することができます。たとえば、AuthorServiceBookService使用する場合、 次のようにAuthorServiceを宣言することで使用することができます:

class AuthorService {
    def bookService
}

依存性注入とドメインクラス/タグライブラリ

ドメインクラスとタグライブラリにもサービスを注入することでき、豊富なドメインモデルやビューの開発を支援することができます:

class Book {
    …
    def bookService

def buyBook() { bookService.buyBook(this) } }

8.4 Javaからサービスを使う

サービスについての強力な事の一つは、サービスは再利用可能なロジックをカプセル化するため、Javaのクラスを含む他のクラスからサービスを使用することができるということです。Javaからサービスを再利用する方法はいくつかあります。最も簡単な方法は、grails-app/servicesディレクトリ下で、パッケージの中にサービスを移動することです。これが重要な手順である理由は、デフォルトパッケージ(パッケージの宣言がない場合に使用されるパッケージ)からJavaにクラスをインポートできないからです。したがって、たとえば以下のBookServiceは、そのままではJavaから使用できません:

class BookService {
    void buyBook(Book book) {
        // logic
    }
}

しかし、このクラスをパッケージの中に置き、grails-app/services/bookstoreといったサブディレクトリの中にクラスを移動し、パッケージ宣言を修正することで、是正できます:

package bookstore

class BookService { void buyBook(Book book) { // logic } }

パッケージに代わる別の方法として、パッケージ内のインターフェイスをサービスが実装することです:

package bookstore

interface BookStore { void buyBook(Book book) }

そしてサービスは:

class BookService implements bookstore.BookStore {
    void buyBook(Book b) {
        // logic
    }
}

この後者の方法の方が、間違いなくクリーンです。Java側は、実装クラスではなくインタフェースへの参照を持てばよいからです。いずれにせよ、この実行の目的は、Javaがコンパイル時に使用するクラス(またはインターフェイス)を静的に解決できるようにすることです。

これで、src/javaディレクトリのパッケージにJavaクラスを作ることができ、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の中にSpringビーンとしてJavaクラスを設定することができます(詳細は、GrailsとSpringセクションを参照してください)

<bean id="bookConsumer" class="bookstore.BookConsumer">
    <property name="bookStore" ref="bookService" />
</bean>

または、 grails-app/conf/spring/resources.groovyに:

import bookstore.BookConsumer

beans = { bookConsumer(BookConsumer) { bookStore = ref("bookService") } }