参考URL
http://desmontandojava.blogspot.de/2013/08/grails-pitfalls-dont-do-flushtrue-when.html
http://stackoverflow.com/questions/2979786/rolling-back-a-transaction-in-a-grails-service
トランザクションを用いたデータベースの更新がしたい
GrailsではServiceという機構を用いて簡単に実現できます。
どれだけ簡単化というと、単純にgrails-app/servicesディレクトリに 任意の名前Service.groovy というサービスを用意して、その中でメソッドを記述すれば、そのメソッドが自動的にトランザクショナルに実行されます。
(もしくはGrailsのcreate-serviceコマンドを利用して生成する)
コントローラからServiceを呼び出すには、そのServiceをDIします。
DIだなんていうと面倒くさそうに感じますが、単純にサービス名の先頭を小文字にして変数宣言するだけです。
ServiceをControllerのアクションメソッドから呼び出す
テスト用クラス
ドメイン
create-domain-class Book
を実行
class Book{
String title
static constraints = {
}
}
サービス
create-service Test
を実行
import grails.transaction.Transactional
@Transactional
class TestService {
def test() {
new Book(title: "test in first").save()
//throw new RuntimeException("omg")
}
}
コントローラ
TestController {
// 作成したサービス"TestService"をDI
def testService
def index(){
try{
// サービスのtestメソッドを実行
testSerivce.test()
render "OK"
} catch (Exception e){
render "NG"
}
}
}
コミット
コントローラのindex()にアクセスすると、TestService#test()が実行され、Bookインスタンスが保存される。
問題なくTestService#test()が終了すれば自動的にコミットされ、 OK と表示される。
ロールバック
サービスのメソッドの中で、 RuntimeExceptionが発生してメソッドが終了すれば 自動的にロールバックされ、画面に NG と表示される。
Grails、Groovyが自動的に発生させるRuntimeExceptionではなく、特定の条件を満たさない場合にロールバックしたいなどの場合は自分で throw new RuntimeException()
を記述する。
ちょっとメモ
コントローラのアクションメソッド内の、サービスのメソッドを呼び出したところでtry-catchしていても、ビューに行く前にorg.springframework.transaction.UnexpectedRollbackException
という例外が発生することがある。
これは、サービスを呼び出したコントローラのアクションメソッドが@Transactional(readOnly = true)
な状態のため。
まずはコントローラ自体か、サービスを実行したいコントローラのアクションメソッドに@Transactional
アノテーションを追加する。
Grailsの再起動(キャッシュもクリアした方が良い)を行う。
いろいろ試してみる
前提条件
以下、全てコントローラから TestService#first() メソッドが実行されるものとする。
1.サービス内のメソッドで、例外をキャッチするとどうなるか
保存されてしまった。
つまり、サービスのメソッドが例外が投げられて終われば、ロールバックされると言う事。
def first(){
try {
new Book(title: "test in first").save()
throw new RuntimeException("omg")
} catch(RuntimeException e) {
// メソッド内で例外をキャッチしてしまうと、ロールバックされない
println "oh"
}
}
なので、以下のようにすればロールバックされる。(意味の無いコードだけど動作確認として)
def first(){
try {
new Book(title: "test in first").save()
throw new RuntimeException("omg")
} catch(RuntimeException e) {
println "oh"
} finally {
// このメソッドが最終的に例外(RuntimeException)を投げればロールバックされる。
throw new RuntimeException("yeah! rollback baby!")
}
}
2.データベースに保存される順番
import grails.transaction.Transactional
@Transactional
class TestService {
def first(){
new Book(title:"first 1").save()
this.second()
new Book(title:"first 2").save()
}
def second(){
new Book(title:"second1").save()
}
}
この場合、データベースには最終的に実行され順番にレコードが保存される。
なので、bookテーブルに連番が振られる場合、
1: first 1
2: second 1
3: first 2
となる。
3.サービス内のメソッドが別のメソッドを呼び、そこが例外になった場合
import grails.transaction.Transactional
@Transactional
class ServiceTestService {
def first(){
new Book(title:"first 1").save()
this.second()
new Book(title:"first 2").save()
}
def second(){
new Book(title:"second1").save()
throw new RuntimeException("omg")
}
}
両方のメソッドで、保存されない。ロールバックされる。
そもそもGroovy(Java)的に呼び出したメソッドで例外がキャッチされずにスローされると、呼び出し元にそのままその例外が投げられるので、今回の場合firstもthis.second()を呼んだ結果RuntimeExceptionを受け取るので、その下のnew Book(title:"first 2").save()
はそもそも実行されない。
そして、その例外でfirstが終了するので、ロールバックされると言う流れ。
4.サービス内のメソッドが別のメソッドを呼び、最初に呼ばれた物が例外になった場合
import grails.transaction.Transactional
@Transactional
class TestService {
def first(){
new Book(title:"first 1").save()
this.second()
new Book(title:"first 2").save()
throw new RuntimeException("omg")
}
def second(){
new Book(title:"second1").save()
}
}
今度は、呼び出されたメソッド自体は正常終了した場合。
コレも両方ロールバックされる。
この事から、一番最初に呼び出されたサービス内のメソッドが、最終的にRuntimeExceptionを起こせば、そのメソッドから呼び出された別メソッドも含めて全てロールバックされると言える。
5.上記のパターンで、どこかに @Transactional(readOnly = true)
import grails.transaction.Transactional
@Transactional
class ServiceTestService {
def first(){
new Book(title:"first 1").save()
this.second()
new Book(title:"first 2").save()
//throw new RuntimeException("omg")
}
@Transactional(readOnly = true)
def second(){
new Book(title:"second1").save()
//throw new RuntimeException("====?")
}
}
呼び出すサービス内のメソッドにreadOnlyが付いていても、ちゃんとロールバックされる。
今回の場合で言えば、first()でRuntimeExceptionが発生しても、second()でRuntimeExceptionが発生しても、どちらの場合でもちゃんとロールバックされる。
メモ
PostgreSQLで試してないけど、少なくともPostgreSQLでは、トランザムクション内でINSERTされて、最終的にロールバックされたとしてもシリアルの場合ならそのシリアルが歯抜けになる。
注意!
ドメインの中で以下のように記述した場合、java.lang.NullPointerException
が発生して、DB上から全データが消えてしまった・・・
static Person addUrlaubTo(Person person, Integer year){
person.baseUrlaubDays.times{ person.addToUrlaube(new Urlaub(urlaubYear: year)) }
person.save()
new Person(username:"test1", password: "password").save()
//throw new RuntimeException("asdf")
person
}
開発環境だからなのか、何が起こっているのかは良く分からない。
とりあえず複数のドメインを更新するようなロジックの場合、おとなしくサービスを使ったほうが良さそう。
ちなみに上記コードのRuntimeExceptionをコメントしないと、データは消えずに単純にこの例外がthrowされて終了する。
なので、メソッドが終わった後で何かが起こっている模様。
この問題は放置するには怖すぎるので、時間のあるときに別途調査予定。
まとめ
1. ちょっと複雑な処理は全てServiceを使おう。
2. Serivce内では自分でRuntimeExceptionをキャッチしない方がいい。
3. Serivce内で意図的にロールバックしたい場合はRuntimeExceptionをスローする。
追記
[Grails]servicesで直接SQLを実行する方法でService内でSQLを直接実行する方法を書いた。
なお、SQLを直接Service内で実行しても、ちゃんとこの記事の通りにロールバックできる。