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 helloworldclass SimpleService {
}
8.1 断定的なトランザクション
デフォルトの宣言的トランザクション
サービスは、典型的にはドメインクラス間の調整的なロジックに関わりますが、それゆえしばしば大規模な操作に渡る永続性に関わります。頻繁にトランザクションの振る舞いを必要とするサービスの性質を考えてください。もちろんwithTransactionメソッドを使ってプログラム的なトランザクションを行うことができますが、これは同じことを何度も繰り返すことになり、Springが持つ基本的なトランザクション抽象化の恩恵を十分活用することができません。 サービスは、トランザクション境界を使用することができ、サービス内のすべてのメソッドをトランザクションにすると宣言する宣言方法です。すべてのサービスは、デフォルトでトランザクション境界が可能です、不可にするにはtransactional
プロパティにfalse
を設定します:class CountryService { static transactional = false }
依存性の注入は、宣言的トランザクションが機能する唯一の方法です。警告: その結果、すべてのメソッドがトランザクションでラップされ、例外がメソッドからスローされると、トランザクションは自動的にロールバックされます。トランザクションの伝播レベルは、デフォルトでPROPAGATION_REQUIREDが設定されます。new BookService()
のようにnew
演算子を使用した場合、トランザクションなサービスを取得するできません。
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.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
メソッド以外の、全メソッドがread-writeトランザクションになります(クラスレベルのアノテーションの定義によって)。import org.springframework.transaction.annotation.Transactional@Transactional class BookService { @Transactional(readOnly = true) def listBooks() { Book.list() } def updateBook() { // … } def deleteBook() { // … } }
updateBook
とdeleteBook
はアノテーション指定されていませんが、クラスレベルアノテーションの定義が受け継がれます。
さらなる情報は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 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] } } }
8.2 サービスのスコープ
デフォルトでは、サービスが持つメソッドへのアクセスは同期されないので、サービスが持つメソッドの同時実行を防ぐことはできません。実際には、サービスはシングルトンで同時に使用することが出来るため、サービスに状態を格納する場合は注意が必要です。簡単な(そしてより良い)方法は、サービスに状態を決して格納しないことです。 特定のスコープにサービスを置くことで、この振る舞いを変えることができます。サポートされているスコープは次のとおりです:prototype
- クラスへの注入時にサービスが生成されるrequest
- リクエスト毎にサービスが生成されるflash
- 現在とその次のリクエスト用にサービスが生成されるflow
- フローの開始から終了まてのサービス(サブフロー含まず)conversation
- フローの開始から終了まてのサービス(サブフロー含む)session
- ユーザセッション毎にサービスが生成されるsingleton
(デフォルト) - サービスが1つのみ生成される
サービスのスコープが スコープのいずれかを有効にするには、クラスにstaticのscopeプロパティを追加し、プロパティの値を上記のいずれかにします:flash
、flow
あるいはconversation
の場合、java.io.Serializableを実装する必要があり、Webフローのコンテキストの中でだけ使うことができます。
static scope = "flow"
8.3 依存注入とサービス
依存性の注入の基本
Grailsのサービスの重要な側面は、Springフレームワークが持つ依存性注入機能を活用するための能力です。Grailsは、「規約による依存性の注入」をサポートしています。言い換えれば、サービスのクラス名のプロパティ名表現を使用することで、自動的にコントローラ、タグライブラリなどにサービスが注入されます。 例として、BookService
というサービスが与えられた場合、次のようにコントローラの中にbookService
という名前のプロパティを配置します:class BookController { def bookService … }
class AuthorService { BookService bookService }
注:通常、プロパティ名は、型の最初の文字を小文字のにして生成します。たとえば、BookService
クラスのインスタンスは、bookService
という名前のプロパティにマップされます。 標準のJavaBean規約と整合をとるため、クラス名の最初の2文字が大文字の場合、プロパティ名はクラス名と同じになります。たとえばJDBCHelperService
クラスは、jDBCHelperService
またjdbcHelperService
では無く、JDBCHelperService
という名前のプロパティにマップされます。 小文字への変換規則の詳細については、JavaBeanの仕様のセクション8.8を参照してください。
依存性注入とサービス
同じ方法で他のサービスにサービスを注入することができます。たとえば、AuthorService
がBookService
使用する場合、 次のように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 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
ディレクトリのパッケージに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.BookConsumerbeans = { bookConsumer(BookConsumer) { bookStore = ref("bookService") } }