Database Migration Plugin のチェックサム

データベースへの反映履歴を管理するdatabasechangelogテーブルにはmd5sumというchangesetチェックサムを格納するカラムがあります。

liquibaseのリファレンスには以下のように記述されています。

LiquiBase が変更セットに到達すると、MD5Sum を計算して、”databasechangelog” に MD5Sum を保存します。MD5Sum を保存する意味は、LiquiBase が、ほかの誰かが実行されて以来変更セットを変更していないかどうかを知ることができるためです。変更セットが実行されたときから変更されていた場合、LiquiBase はエラーとともに移行を終了します。というのも、何が変更されたか知ることができず、データベースが変更ログが期待しているのと異なった状態にあるかもしれないからです。もし、適切な理由によって変更セットが変更されていた場合やこのエラーを無視したい場合は、databasechangelog テーブルを更新して、その行の id/author/ファイルのパス名に対応する MD5Sum を null に更新します。次回 LiquiBase が実行されると、MD5Sum の値を適切な値に更新してくれます。

MD5Sum は、”runOnChange” 変更セット属性と一緒に使用されます。普通はただ現在のバージョンが知りたいだけで新しい変更セットを追加したくないのに、更新されたときはいつでも適用したいときがなんどもあるでしょう。このよい例はストアドプロシージャに関するものです。ストアドプロシージャの全体をコピーして新しい変更セットを作るたび、とても長い変更ログが無駄に終わるだけでなく、ソースコード管理システムのマージや差分の機能を失うことになるのです。代わりに、runOnChange = “true” 属性を変更セットにあるストアドプロシージャのテキストにつけましょう。そのストアドプロシージャは、内容が変更されたときだけ再作成されるようになります。

簡単に言うと、changesetを変更しチェックサムが変わった場合に、前者のデフォルトの場合(runOnChange=false)はエラー、runOnChange=trueの場合は再度実行という動作の違いがあるようです。

書いてある通りなのですが実際に動作させながら検証してみます。

デフォルトの場合

以下のPersonドメインがあるとします。

class Person {
    String name
}

このドメインに対するchangelogを以下のように作成します。

databaseChangeLog = {

    changeSet(author: "yamkazu (generated)", id: "create-person") {
        createTable(tableName: "person") {
            column(name: "id", type: "int8") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "personPK")
            }

            column(name: "version", type: "int8") {
                constraints(nullable: "false")
            }

            column(name: "name", type: "varchar(255)") {
                constraints(nullable: "false")
            }
        }
    }
}

この状態で一度dbm-updateコマンドを使用してデータベースと同期しておきます。同期が成功すると上のchangesetを反映した履歴がdatabasechangelogが書き込まれます。

devDb=# select id, md5sum from databasechangelog;
      id       |               md5sum
---------------+------------------------------------
 create-person | 3:c2a46c4edd51cd911c9dced0a8fcabe3

changeSetの内容を元にmd5sumが書き込まれています。この状態でほど作成したchangeSetのnameカラムの型をvarchar(255)からtextに変更します。

databaseChangeLog = {

    changeSet(author: "yamkazu (generated)", id: "create-person") {
        createTable(tableName: "person") {
            ...
            column(name: "name", type: "text") {
                constraints(nullable: "false")
            }
        }
    }
}

同じIDにもかかわらずchangeSetの中身が書き換えられた状態です。この状態でdbm-validateコマンドを実行します。

grails> dbm-validate
| Starting dbm-validate for database test @ jdbc:postgresql://localhost:5432/devDb
Validation Error:
     1 change sets have changed since they were ran against the database
          changelog-0.1.groovy::create-person::yamkazu (generated)

このようにchangeSetの中身が変わりチェックサムがことなるためエラーとなります。dbm-updateなどの同期処理においても必ず最初にこのvalidateが行われるため、同様にエラーになります(1.3.2までこのエラーがdbm-validateコマンド以外では握りつぶされていましたが1.3.3で修正される予定です)。

textへの変更をvarchar(255)に戻して、次はドメインに変更を加えてみます。

class Person {
    String name
    Integer age
}

ageのプロパティを追加しました。このカラムを追加するchangesetを以下のように作成します。

databaseChangeLog = {
    ...
    changeSet(author: "yamkazu (generated)", id: "add-age") {
        addColumn(tableName: "person") {
            column(name: "age", type: "int4") {
                constraints(nullable: "false")
            }
        }
    }
}

再度dbm-updateコマンドを使用してデータベースと同期してします。databasechangelogは以下のようになります。

devDb=# select id, md5sum from databasechangelog;
      id       |               md5sum
---------------+------------------------------------
 create-person | 3:c2a46c4edd51cd911c9dced0a8fcabe3
 add-age       | 3:2e304646a134a175a22769177941c06a

すでにデータベースに反映が完了して、changesetのcreate-personadd-ageを正規化して一つにまとめたいと考えたとします。

databaseChangeLog = {

    changeSet(author: "yamkazu (generated)", id: "create-person") {
        createTable(tableName: "person") {
            column(name: "id", type: "int8") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "personPK")
            }

            column(name: "version", type: "int8") {
                constraints(nullable: "false")
            }

            column(name: "name", type: "varchar(255)") {
                constraints(nullable: "false")
            }

            column(name: "age", type: "int4") {
                constraints(nullable: "false")
            }
        }
    }
}

create-personのchangesetにageを追加し、add-ageは削除しました。この状態でdbm-updateを実施すると当然エラーになります。チャックサムが異なるためです。

Validation Error:
     1 change sets have changed since they were ran against the database
          changelog-0.1.groovy::create-person::yamkazu (generated)

特にデータベースへ反映を行うわけではないがchangesetだけ正規化するとこのような状態になります。この状態を解消するには一度dbm-clear-checksumを実行します。

grails> dbm-clear-checksums

databasechangelogは以下のようになります。

devDb=# select id, md5sum from databasechangelog;
      id       | md5sum
---------------+--------
 create-person |
 add-age       |

このようにmd5sumの値がクリアされます。この状態で再度dbm-updateを実施するとdatabasechangelogは以下のようになります。

devDb=# select id, md5sum from databasechangelog order by orderexecuted ;
      id       |               md5sum
---------------+------------------------------------
 create-person | 3:393ba7bc4288b805a68f49b8c3f3f3ee
 add-age       |

今度はエラーとならずにcreate-personに新たなハッシュ値が書き込まれました。 ちなみにこの時にdbm-rollback-count-sql 1を実行すると以下になります。

grails> dbm-rollback-count-sql 1
DROP TABLE person;

DELETE FROM databasechangelog  WHERE ID='create-person' AND AUTHOR='yamkazu (generated)' AND FILENAME='changelog-0.1.groovy';

どうも実行のsqlを見ているとmd5sumがnullのレコードを対象として動作するようです。少し注意が必要です。 このsqlロールバックするとadd-ageが残ってしまいますが、特に残っていても害はなさそうですが気になります。dbm-updateのオプションなどで再度チェックサム書いたあとに浮いたレコード削除してくれてもいいようなきもしますが、そんな機能は今のところないようです。

runOnChange=true の場合

runOnChange=trueはchangeSetの属性に設定します。注意点としては当たり前ですがrunOnChange=trueで再実行された際に問題なく動作するクエリを書くことです。

changeSet(author: "yamkazu (generated)", id: "create-one-function", runOnChange: true) {
    sql("""
        | CREATE OR REPLACE FUNCTION one() RETURNS integer
        |    AS 'SELECT 1 AS RESULT;'
        |    LANGUAGE SQL;
        """.stripMargin())
}

CREATE OR REPLACEというふうに宣言することで何度実行しても問題ありません。これでfunctionの中の実装が変わると再度実行されます。

SpockでビルトインされているExtensionsとかそのへん

G* Advent Calendar 2012 12日目担当のyamkazuです。こんにちは。

今日はみんな大好きSpockでビルトインされている機能拡張について、いくつかピックアップして紹介します。機能拡張にカテゴライズされないものもあるかもしれませんが、その辺はゆるやかに。

また、この記事はSpock0.7を元に記述していますが、バージョンが変わるとアノテーションが存在しないとかありますので、新しいバージョンが出た場合はそのへんを注意してお読みください。

それではさっそく。

@Ignore

これは説明不要だと思いますが、Ignoreを付与すると指定したフィーチャの実行がスキップされます。アノテーションに理由を書くような使い方もできます。

@Ignore
def "xxx"() { expect: true }

@Ignore('hogehogeのため')
def "yyy"() { expect: true }

スペックに指定すると全体がスキップされます。

@Ignore
class IgnoreSpec extends Specification { ... }

@IgnoreRest

IgnoreRestはIgnoreとは対照的に、IgnoreRestが付与されたフィーチャのみを実行します。IDEを使っている場合は対象のフィーチャを決め打ちで実行するのは比較的容易なのですが、コンソールからtestを実行する場合する場合などはそうではありません。このアノテーションを使用すると実行したいフィーチャメソッドを簡単に指定することができます。

class IgnoreRestSpec extends Specification {
    def "xxx"() { ... }

    @IgnoreRest
    def "yyy"() { ... }

    @IgnoreRest
    def "zzz"() { ... }
}

上記のように複数指定することもでき、この例ではyyy、zzzのみが実行されます。

@IgnoreIf

IgnoreIfは指定されたクロージャの実行結果がtrueの場合にフィーチャの実行がスキップされます。クロージャの中では暗黙的に以下の変数が使用可能です。

  • env - System.getenv()のショートカット
  • properties - System.getProperties()のショートカット
  • javaVersion - Javaのバージョン

以下のように使用します。

@IgnoreIf({ true })
def "trueなので実行されない"() { expect: false }

@IgnoreIf({ false })
def "falseなので実行される"() { expect: true }

@IgnoreIf({ 1 < 2 })
def "1 < 2 はtrueなので実行されない"() { expect: false }

@IgnoreIf({ 1 > 2 })
def "1 > 2 はfalseなので実行される"() { expect: true }

@IgnoreIf({
    def a = 1
    def b = 1
    a + b == 2
})
def "closureをcallしているだけなので複数行書いても良い"() { expect: false }

@IgnoreIf({ javaVersion > 1.6 })
def "javaVersionでJVMのバージョンが参照できる"() { expect: false }

@IgnoreIf({ env["LANG"] != 'C' })
def "envがSystem.getenv()のショートカットになっている"() { expect: false }

@IgnoreIf({ properties["os.name"] == 'Mac OS X' })
def "propertiesがSystem.getProperties()のショートカットになっている"() { expect: false }

@FailsWith

Spockでは例外のテストを行う際は、以下のように

then:
MyException e = thrown()

thrown()を使用することができますが、FailsWithはいわゆるJUnit4の@Test(expected = MyException.class)のような記述の仕方を可能にするアノテーションで、指定した例外でフィーチャが失敗することを宣言できます。

@FailsWith(MyException)
def "xxx"() { expect: throw new MyException() }

@FailsWith(value = MyException, reason = "hogehogeのため")
def "yyy"() { expect: throw new MyException() }

スペックに付与することも可能です。

@FailsWith(MyException)
class FailWithSpec extends Specification { ... }

この場合は、スペック上のすべてのフィーチャが指定した例外で失敗することを宣言しています。

@Timeout

Timeoutはフィーチャの実行時間のタイムアウト値を指定することができます。このタイムアウト値を超過した場合はorg.spockframework.runtime.SpockTimeoutErrorがスローされます。

@Timeout(1)
def "1秒以内に終わる"() {
    expect: Thread.sleep 500
}

@FailsWith(SpockTimeoutError)
@Timeout(1)
def "1秒以内に終わらない"() {
    expect: Thread.sleep 1100
}

デフォルトでは単位は秒に設定されています。単位を変更したい場合はunit属性を指定します。

@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
def "500ミリ秒以内に終わる"() {
    expect: Thread.sleep 250
}

@FailsWith(SpockTimeoutError)
@Timeout(value = 250, unit = TimeUnit.MILLISECONDS)
def "500ミリ秒以内に終わらない"() {
    expect: Thread.sleep 300
}

@Unroll

通常Spockではwhereのパラメタライズテストを実行すると、そのフィーチャに対し1つの実行結果が出力されます。

def "x + y の合計を計算する"() {
    expect:
    x + y == sum

    where:
    x | y || sum
    1 | 2 || 3
    3 | 4 || 7
    5 | 6 || 11
}

実行結果

Test                    Duration    Result
x + y の合計を計算する  0.001s      passed

Unrollはこのパラメタライズテストをそれぞれの独立したフィーチャとして実行してくれます。また以下のように#でパラメータをフィーチャ名に埋め込むことができます。

@Unroll
def "#x + #y の合計は #sum になる"() { ... }

実行結果

Test                       Duration    Result
1 + 2 の合計は 3 になる    0s          passed
3 + 4 の合計は 7 になる    0s          passed
5 + 6 の合計は 11 になる   0s          passed

#形式での参照は引数なしのメソッドあれば、メソッドをチェインして参照することも可能です。詳細はリファレンスを参照してください。

@Shared

通常フィールドで宣言したフィクスチャはフィーチャの実行毎に初期化されます。

def counter = 0

def "counterをインクリメントする"() {
    expect:
    counter++ == expectedCounter

    where:
    expectedCounter << [0, 0, 0]
}

Sharedはフィーチャ間でフィクスチャを共有するSharedFixtureを実現してくれます。生成コストが高いオブジェクトをフィーチャ間で共有したい場合に便利です。

@Shared
def counter = 0

def "counterをインクリメントする"() {
    expect:
    counter++ == expectedCounter

    where:
    expectedCounter << [0, 1, 2]
}

@AutoCleanup

AutoCleanupはフィールドに設定することで自動で後処理をしてくれます。デフォルトではcloseメソッドが自動で呼びされます。

@AutoCleanup
def closeable = new Closeable()

明示的に呼び出す後処理のメソッドを指定することもできます。

@AutoCleanup("shutdown")
def shutdownable = new Shutdownable()

後処理の中に例外が発生した場合に発生した例外を握りつぶしたい場合はquiet属性にtrueを指定します。デフォルトはfalseです。

@AutoCleanup(value = 'shutdown', quiet = true)
def shutdownable = new Shutdownable()

これらの後処理はフィーチャ実行毎に実行されますが、@Sharedが付与されたフィールドでは全フィーチャの実行後に1度だけ実行されます。

@Stepwise

Stepwiseを使用すると一連のフィーチャをそれぞれ定義した順に実行してくれます。フィーチャが一連のシナリオとして実行されるイメージで、途中のテストが失敗すると以降のテストが実行されません。

@Stepwise
class StepwiseSpec extends Specification {
    def "first"() { expect: true }
    def "second"() { expect: false }
    def "third"() { expect: true }
}

上記の例ではfirst、second、thirdの順に実行されるはずですが、secondで失敗するため、thirdは実行されません。

おわりに

今回は紹介できませんが独自のアノテーションを定義して拡張するといったことも容易に出来るようになっています。きっと誰か書いてくれるはず。誰も書かなかったらそのうち書きます。

それでは明日は @irof さんです。

Grailsのcriteriaでlikeにescapeを指定する

どうも見つからない。

いろいろ探してたらorg.hibernate.criterion.LikeExpressionなるものを発見したがorg.hibernate.criterion.Exampleで使われているくらいで他に使われていない。なんぞこれ。

とりあえず、これを使おう。LikeExpressionはコンストラクタがprotectedだがgroovyの前では、そんなものは飾りにすぎない。

Domain1.withCriteria {
    add new LikeExpression("value", "x@_x", MatchMode.ANYWHERE, '@' as char, false)
    addToCriteria new LikeExpression("value", "x@_x", MatchMode.ANYWHERE, '@' as char, false)
}

withCriteria内で動いているbuilderはCriteriaBuilderのようだけど、invokeMethodとかで適当にcriteria側にディスパッチして動いてくれる模様。addToCriteriaでも同じようにいけた。addToCriteriaもprotectedで非常にあれではあります。

動作させたときのSQLログ

2012-11-13 22:44:13,322 [main] DEBUG hibernate.SQL  - 
    /* criteria query */ select
        this_.id as id4_0_,
        this_.version as version4_0_,
        this_.value as value4_0_ 
    from
        domain1 this_ 
    where
        this_.value like ? escape '@' 
        and this_.value like ? escape '@'
2012-11-13 22:44:13,327 [main] TRACE sql.BasicBinder  - binding parameter [1] as [VARCHAR] - %x@_x%
2012-11-13 22:44:13,341 [main] TRACE sql.BasicBinder  - binding parameter [2] as [VARCHAR] - %x@_x%

本当にこれでよいかはわからない。だれかいい方法があったら教えてください><

Database Migration Pluginがすごい ~ロールバック編~

前回の続き

Database Migration Pluginのすごいところはロールバックが出来る事。ロールバックを行うコマンドは主に3つ種類がある。

  • dbm-rollback-count
  • dbm-rollback-to-date
  • dbm-rollback

それぞれ個別に見ていく。

dbm-rollback-count

dbm-rollback-countは指定した数分の変更履歴をロールバックするコマンド。例えばdatabasechangelogの状態が以下のようになっていたとする。

devDb=> select * from databasechangelog;
       id        |       author        |     filename      |         dateexecuted          | orderexecuted | exectype |               md5sum               |        description         | comments | tag | liquibase 
-----------------+---------------------+-------------------+-------------------------------+---------------+----------+------------------------------------+----------------------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table               |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence            |          |     | 2.0.5
 1351333217527-1 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.174495+09 |             3 | EXECUTED | 3:500d2782c8ddcc7d7b89b0b29b2d7342 | Create Table               |          |     | 2.0.5
 1351333217527-2 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.190441+09 |             4 | EXECUTED | 3:d4dcaaa9285f777d2a05f33e275488b7 | Add Column                 |          |     | 2.0.5
 1351333217527-3 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.200821+09 |             5 | EXECUTED | 3:77e6c768bd70db135e30edfb2ef6788e | Add Foreign Key Constraint |          |     | 2.0.5

ここでbookだけがあった状態に戻したいといった場合、最新の3つのchangesetを打ち消す必要がある。ここでdbm-rollback-count-sqlというコマンドを実行してみる。

これはロールバック系のコマンドだけでなく他のコマンドにも用意されて、xxx-sqlの形になっている。これを使用すると、xxxのコマンドを実行した時に実際にどんなSQLが実行されるのか確認出来る。また、grailsコマンドが使用できないような環境にシステムがあるばあはこのSQLを持って行く事もできる。

grails> dbm-rollback-count-sql 3
| Starting dbm-rollback-count-sql for database test @ jdbc:postgresql://localhost:5432/devDb
-- *********************************************************************
-- Rollback 3 Change(s) Script
-- *********************************************************************
-- Change Log: changelog.groovy
-- Ran at: 12/10/28 17:09
-- Against: test@jdbc:postgresql://localhost:5432/devDb
-- Liquibase version: 2.0.5
-- *********************************************************************

-- Lock Database
-- Rolling Back ChangeSet: add-author.groovy::1351333217527-3::yamkazu (generated)::(Checksum: 3:77e6c768bd70db135e30edfb2ef6788e)
ALTER TABLE book DROP CONSTRAINT FK2E3AE9B2F6003C;

DELETE FROM databasechangelog  WHERE ID='1351333217527-3' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

-- Rolling Back ChangeSet: add-author.groovy::1351333217527-2::yamkazu (generated)::(Checksum: 3:d4dcaaa9285f777d2a05f33e275488b7)
ALTER TABLE book DROP COLUMN author_id;

DELETE FROM databasechangelog  WHERE ID='1351333217527-2' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

-- Rolling Back ChangeSet: add-author.groovy::1351333217527-1::yamkazu (generated)::(Checksum: 3:500d2782c8ddcc7d7b89b0b29b2d7342)
DROP TABLE author;

DELETE FROM databasechangelog  WHERE ID='1351333217527-1' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

| Finished dbm-rollback-count-sql

changesetをぞれぞれ評価していって、そのchangesetの変更を取り返すSQLが出力さているのがわかる。問題無さそうなので実際に実行。

grails> dbm-rollback-count 3
| Finished dbm-rollback-count

DBを確認してみる。

devDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(4 rows)

devDb=> select * from databasechangelog;
       id        |       author        |    filename     |         dateexecuted          | orderexecuted | exectype |               md5sum               |   description   | comments | tag | liquibase 
-----------------+---------------------+-----------------+-------------------------------+---------------+----------+------------------------------------+-----------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table    |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence |          |     | 2.0.5
(2 rows)

ちゃんとロールバックされている。

dbm-rollback-to-date

次はdbm-rollback-to-dateで指定した時間までロールバックするというもの。DBの状態が以下のようになっていたとする。dateexecutedの日時に対して判断される。

devDb=> select * from databasechangelog;
       id        |       author        |     filename      |         dateexecuted          | orderexecuted | exectype |               md5sum               |        description         | comments | tag | liquibase 
-----------------+---------------------+-------------------+-------------------------------+---------------+----------+------------------------------------+----------------------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table               |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence            |          |     | 2.0.5
 1351333217527-1 | yamkazu (generated) | add-author.groovy | 2012-10-28 17:17:07.623519+09 |             3 | EXECUTED | 3:500d2782c8ddcc7d7b89b0b29b2d7342 | Create Table               |          |     | 2.0.5
 1351333217527-2 | yamkazu (generated) | add-author.groovy | 2012-10-28 17:17:07.639352+09 |             4 | EXECUTED | 3:d4dcaaa9285f777d2a05f33e275488b7 | Add Column                 |          |     | 2.0.5
 1351333217527-3 | yamkazu (generated) | add-author.groovy | 2012-10-28 17:17:07.648655+09 |             5 | EXECUTED | 3:77e6c768bd70db135e30edfb2ef6788e | Add Foreign Key Constraint |          |     | 2.0.5

ここでauthorが追加される前の状態まで戻りたいとする。日付と時刻はそれぞれ、yyyy-MM-dd、 HH:mm:ssで指定する。時刻はオプションで指定しなかった場合は00:00:00が指定されたものと同じとなる。

先ほどと同じようにdbm-rollback-to-date-sqlで事前確認

grails> dbm-rollback-to-date-sql 2012-10-28
Starting dbm-rollback-to-date-sql for database test @ jdbc:postgresql://localhost:5432/devDb-- *********************************************************************-- Rollback to Sun Oct 28 00:00:00 JST 2012 Script
-- *********************************************************************
-- Change Log: changelog.groovy
-- Ran at: 12/10/28 17:24-- Against: test@jdbc:postgresql://localhost:5432/devDb-- Liquibase version: 2.0.5-- *********************************************************************-- Lock Database-- Rolling Back ChangeSet: add-author.groovy::1351333217527-3::yamkazu (generated)::(Checksum: 3:77e6c768bd70db135e30edfb2ef6788e)
ALTER TABLE book DROP CONSTRAINT FK2E3AE9B2F6003C;

DELETE FROM databasechangelog  WHERE ID='1351333217527-3' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

-- Rolling Back ChangeSet: add-author.groovy::1351333217527-2::yamkazu (generated)::(Checksum: 3:d4dcaaa9285f777d2a05f33e275488b7)
ALTER TABLE book DROP COLUMN author_id;

DELETE FROM databasechangelog  WHERE ID='1351333217527-2' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

-- Rolling Back ChangeSet: add-author.groovy::1351333217527-1::yamkazu (generated)::(Checksum: 3:500d2782c8ddcc7d7b89b0b29b2d7342)
DROP TABLE author;

DELETE FROM databasechangelog  WHERE ID='1351333217527-1' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

| Finished dbm-rollback-to-date-sql

問題無さそう。では実行。

grails> dbm-rollback-to-date 2012-10-28
| Finished dbm-rollback-to-date

DBを確認。

devDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(4 rows)

devDb=> select * from databasechangelog;
       id        |       author        |    filename     |         dateexecuted          | orderexecuted | exectype |               md5sum               |   description   | comments | tag | liquibase 
-----------------+---------------------+-----------------+-------------------------------+---------------+----------+------------------------------------+-----------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table    |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence |          |     | 2.0.5
(2 rows)

うまくいっている。

dbm-rollback

最後にdbm-rollback。これは指定したタグまでロールバックするというもの。そもそもタグとは何か。

タグはdbm-tagコマンドで設定できる。現在のDB状態に名前が付けられる。DB状態が以下のようになっていたとする。

devDb=> select * from databasechangelog;
       id        |       author        |    filename     |         dateexecuted          | orderexecuted | exectype |               md5sum               |   description   | comments | tag | liquibase 
-----------------+---------------------+-----------------+-------------------------------+---------------+----------+------------------------------------+-----------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table    |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence |          |     | 2.0.5

この状態でdbm-tagを実行する。現状のDBの状態にv1.0という名前のタグを付けてみる。

grails> dbm-tag v1.0
| Finished dbm-tag

DBを再確認。

devDb=> select * from databasechangelog;
       id        |       author        |    filename     |         dateexecuted          | orderexecuted | exectype |               md5sum               |   description   | comments | tag  | liquibase 
-----------------+---------------------+-----------------+-------------------------------+---------------+----------+------------------------------------+-----------------+----------+------+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table    |          |      | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence |          | v1.0 | 2.0.5
(2 rows)

tagのところにv1.0が入っている。この後DBの変更があって以下になったとする。

devDb=> select * from databasechangelog;
       id        |       author        |     filename      |         dateexecuted          | orderexecuted | exectype |               md5sum               |        description         | comments | tag  | liquibase 
-----------------+---------------------+-------------------+-------------------------------+---------------+----------+------------------------------------+----------------------------+----------+------+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table               |          |      | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence            |          | v1.0 | 2.0.5
 1351333217527-1 | yamkazu (generated) | add-author.groovy | 2012-10-28 17:42:39.667878+09 |             3 | EXECUTED | 3:500d2782c8ddcc7d7b89b0b29b2d7342 | Create Table               |          |      | 2.0.5
 1351333217527-2 | yamkazu (generated) | add-author.groovy | 2012-10-28 17:42:39.686335+09 |             4 | EXECUTED | 3:d4dcaaa9285f777d2a05f33e275488b7 | Add Column                 |          |      | 2.0.5
 1351333217527-3 | yamkazu (generated) | add-author.groovy | 2012-10-28 17:42:39.699466+09 |             5 | EXECUTED | 3:77e6c768bd70db135e30edfb2ef6788e | Add Foreign Key Constraint |          |      | 2.0.5

ここでv1.0の状態までロールバックしたいとする。まずはsqlから確認。

grails> dbm-rollback-sql v1.0
| Starting dbm-rollback-sql for database test @ jdbc:postgresql://localhost:5432/devDb
-- *********************************************************************
-- Rollback to 'v1.0' Script
-- *********************************************************************
-- Change Log: changelog.groovy
-- Ran at: 12/10/28 17:43
-- Against: test@jdbc:postgresql://localhost:5432/devDb
-- Liquibase version: 2.0.5
-- *********************************************************************

-- Lock Database
-- Rolling Back ChangeSet: add-author.groovy::1351333217527-3::yamkazu (generated)::(Checksum: 3:77e6c768bd70db135e30edfb2ef6788e)
ALTER TABLE book DROP CONSTRAINT FK2E3AE9B2F6003C;

DELETE FROM databasechangelog  WHERE ID='1351333217527-3' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

-- Rolling Back ChangeSet: add-author.groovy::1351333217527-2::yamkazu (generated)::(Checksum: 3:d4dcaaa9285f777d2a05f33e275488b7)
ALTER TABLE book DROP COLUMN author_id;

DELETE FROM databasechangelog  WHERE ID='1351333217527-2' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

-- Rolling Back ChangeSet: add-author.groovy::1351333217527-1::yamkazu (generated)::(Checksum: 3:500d2782c8ddcc7d7b89b0b29b2d7342)
DROP TABLE author;

DELETE FROM databasechangelog  WHERE ID='1351333217527-1' AND AUTHOR='yamkazu (generated)' AND FILENAME='add-author.groovy';

| Finished dbm-rollback-sql

問題無さそうなので実行。

grails> dbm-rollback v1.0
| Finished dbm-rollback

DBの確認。

devDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(4 rows)

devDb=> select * from databasechangelog;
       id        |       author        |    filename     |         dateexecuted          | orderexecuted | exectype |               md5sum               |   description   | comments | tag  | liquibase 
-----------------+---------------------+-----------------+-------------------------------+---------------+----------+------------------------------------+-----------------+----------+------+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table    |          |      | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence |          | v1.0 | 2.0.5
(2 rows)

v1.0のタグまでロールバックしていることがわかる。

ロールバックはこんなもんで。

Database Migration Pluginがすごい ~入門編~

ずっと放置していたけどDatabase Migration Pluginを触ってみた。

やばい。これは使わないと。

実際にアプリケーションを作りながら説明してく。とりあえずプロジェクトを作成。

$ grails create-app database-migration-test

作成したらBuildConfig.groovyを覗いてみる。

    plugins {
        ...
        runtime ":database-migration:1.1"
        ...
    }

database-migrationがデフォルトで入っているのがわかる。今回はDatabaseにPostgreSQLを使用するのでdependencyに依存関係を追加。

    dependencies {
        ....
        runtime 'postgresql:postgresql:9.1-901.jdbc4'
    }

次にDataSource.groovyを編集する。dbCreateはHibernateの自動DDL生成機能だがdatabase-migrationと競合する機能なため動かないようにする。

environments {
dataSource {
    pooled = true
    driverClassName = "org.postgresql.Driver"
    username = "test"
    password = ""
}
hibernate {
    cache.use_second_level_cache = false
    cache.use_query_cache = false
    cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory'
}
// environment specific settings
environments {
    development {
        dataSource {
//            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
            url = "jdbc:postgresql://localhost:5432/devDb"
            pooled = true
        }
    }
    test {
        dataSource {
//            dbCreate = "update"
            url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
        }
    }
    production {
        dataSource {
//            dbCreate = "update"
            url = "jdbc:postgresql://localhost:5432/prodDb"
            pooled = true
            properties {
               maxActive = -1
               minEvictableIdleTimeMillis=1800000
               timeBetweenEvictionRunsMillis=1800000
               numTestsPerEvictionRun=3
               testOnBorrow=true
               testWhileIdle=true
               testOnReturn=true
               validationQuery="SELECT 1"
            }
        }
    }
}

実際にやるときはDevとTestの扱いをどうするか戦略の余地がありますがとりあえず今回はこれで。またPostgreSQLを使用するので必要な箇所を書き換えた。

これで準備完了。実際に使っていく。インタラクティブモードで操作していくほうが効率が良いため、まずはインタラクティブモードを起動。

$ grails

はじめにデータベースの変更履歴を管理するchangelogを作成する。xmlかgroovyのDSLが使える。grailsで使うならgroovyを選択したくなるのが自然。changelogを作成する方法2種類。データベースの情報をもとに生成するか、GORMのドメインクラスを元に生成するか。

前者のコマンドが dbm-generate-changelog を使用し、後者は dbm-generate-gorm-changelog を使用する。ここではデータベースは空だし、ドメインもまだ何も作成していないので、どちらで実行しても変わりはないが、データベースの接続だけでも確認するという意味で dbm-generate-changelog を実行する(単に空ファイル作るだけなのdbm-create-changelogで本来は良い)。

grails> dev dbm-generate-changelog changelog.groovy

environmentとファイル名を指定して実行する(ここではどのDBも空なので何していしても一緒)。environmentは省略するとdevが使用される。ファイル名は拡張子が重要で、.groovyとつけるとGroovy DSLの定義ファイルになる。

うまくいくとgrails-app/migrations/changelog.groovyが生成されているはず。

databaseChangeLog = {
}

このようなファイルができてればOK。

changelogにまだ変更がない、データベースもまだ空。ということで変更履歴の同期がとれている。この同期がとれているという状態を作るのが dbm-changelog-sync というコマンド。

grails> dbm-changelog-sync

これを実行すると初回実行の場合はデータベースにdatabasechangelogに作られる。

$ psql -U test devDb                                                                                                                                                                                                          18:43:09
psql (9.0.7)
Type "help" for help.

devDb=> \d
               List of relations
 Schema |         Name          | Type  | Owner 
--------+-----------------------+-------+-------
 public | databasechangelog     | table | test
 public | databasechangeloglock | table | test
(2 rows)

devDb=> select * from databasechangelog;
 id | author | filename | dateexecuted | orderexecuted | exectype | md5sum | description | comments | tag | liquibase 
----+--------+----------+--------------+---------------+----------+--------+-------------+----------+-----+-----------
(0 rows)

devDb=> 

まだ中身は空だが、この中で変更履歴の反映が管理されていく。

そろそろ開発を進めていく。ドメインクラスを作る。

package org.yamkazu

class Book {

    String title

}

ドメインの開発が完了!これをデータベースに反映するchangelogを作成する。changelogを作成する方法はdbm-generate-changelog、dbm-generate-gorm-changelogを使用する方法が上で出てきたが、これらは初回のchangelogを作る際に既存のデータベースが存在していたり、既存のドメインをもとに一括生成する用途で、一度開発が始まった後は基本的にはdiffを使って前回との差分のchangelogを生成していく。

diffのコマンドは、dbm-diff、dbm-gorm-diffの2つがある。前者データベースの指定されたenviromentのDB同士の比較、後者はドメインとDBの比較が行える。今回はdbm-gorm-diffを使う。

grails> dbm-gorm-diff --add add-book.groovy

これの意味するところenviromentがdev(省略したので)のDBと、現在のドメインとの差分、それをadd-book.groovyに出力する。--addというオプションは出力されたファイルへの参照を起点となるchangelogファイルに追加してくれるというもの。

changelog.groovyを見てみる。

databaseChangeLog = {
    include file: 'add-book.groovy'
}

add-book.groovyが追加されていることがわかる。続いてadd-book.groovyを見てみる。

databaseChangeLog = {

    changeSet(author: "yamkazu (generated)", id: "1351331869483-1") {
        createTable(tableName: "book") {
            column(name: "id", type: "int8") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "bookPK")
            }

            column(name: "version", type: "int8") {
                constraints(nullable: "false")
            }

            column(name: "title", type: "varchar(255)") {
                constraints(nullable: "false")
            }
        }
    }

    changeSet(author: "yamkazu (generated)", id: "1351331869483-2") {
        createSequence(sequenceName: "hibernate_sequence")
    }

}

Bookドメインを反映するchangesetが定義されており、1351331869483-1と1351331869483-2のchangesetがある。

changesetが出来たので、これをデータベースに反映する。changesetを反映するにはdbm-updateを使用する。

grails> dbm-update

反映が終わったらデータベースを確認してみる。

devDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(4 rows)

devDb=> select * from databasechangelog;
       id        |       author        |    filename     |         dateexecuted          | orderexecuted | exectype |               md5sum               |   description   | comments | tag | liquibase 
-----------------+---------------------+-----------------+-------------------------------+---------------+----------+------------------------------------+-----------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table    |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence |          |     | 2.0.5
(2 rows)

bookテーブルとシーケンスが生成されて、反映したchangesetがdatabasechangelogに記録されている。この様に適用したchangesetを記録するため、もう一度dbm-updateを実行しても、同じidのchangesetが実行されない。

開発を続ける。Authorドメインを作成する。

package org.yamkazu

class Author {

    String name

}

BookからもAuthorへの関連も追加。

class Book {

    Author author

    String title

}

先ほどと同じように dbm-gorm-diff を使う。

grails> dbm-gorm-diff --add add-author.groovy

add-author.groovyはこんなん。createTableやらbookへのaddColumnがあったりする。

databaseChangeLog = {

    changeSet(author: "yamkazu (generated)", id: "1351333217527-1") {
        createTable(tableName: "author") {
            column(name: "id", type: "int8") {
                constraints(nullable: "false", primaryKey: "true", primaryKeyName: "authorPK")
            }

            column(name: "version", type: "int8") {
                constraints(nullable: "false")
            }

            column(name: "name", type: "varchar(255)") {
                constraints(nullable: "false")
            }
        }
    }

    changeSet(author: "yamkazu (generated)", id: "1351333217527-2") {
        addColumn(tableName: "book") {
            column(name: "author_id", type: "int8") {
                constraints(nullable: "false")
            }
        }
    }

    changeSet(author: "yamkazu (generated)", id: "1351333217527-3") {
        addForeignKeyConstraint(baseColumnNames: "author_id", baseTableName: "book", constraintName: "FK2E3AE9B2F6003C", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "author", referencesUniqueColumn: "false")
    }

}

changesetを反映する。

grails> dbm-update

DBを確認

devDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | author                | table    | test
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(5 rows)

devDb=> select * from databasechangelog;
       id        |       author        |     filename      |         dateexecuted          | orderexecuted | exectype |               md5sum               |        description         | comments | tag | liquibase 
-----------------+---------------------+-------------------+-------------------------------+---------------+----------+------------------------------------+----------------------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.890237+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table               |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy   | 2012-10-27 19:07:44.907147+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence            |          |     | 2.0.5
 1351333217527-1 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.174495+09 |             3 | EXECUTED | 3:500d2782c8ddcc7d7b89b0b29b2d7342 | Create Table               |          |     | 2.0.5
 1351333217527-2 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.190441+09 |             4 | EXECUTED | 3:d4dcaaa9285f777d2a05f33e275488b7 | Add Column                 |          |     | 2.0.5
 1351333217527-3 | yamkazu (generated) | add-author.groovy | 2012-10-27 19:27:06.200821+09 |             5 | EXECUTED | 3:77e6c768bd70db135e30edfb2ef6788e | Add Foreign Key Constraint |          |     | 2.0.5
(5 rows)

dbm-updateを使うのがめんどくさかったり、自動でやりたい場合はConfig.groovyに以下の設定を追加することでアプリケーションの起動時に自動反映させることが出来る。

grails.plugin.databasemigration.updateOnStart = true
grails.plugin.databasemigration.updateOnStartFileNames = ["changelog.groovy"]

updateOnStartをtrueにすることでアプリケーションの自動的にupdateOnStartFileNamesに指定されたファイルを反映する。updateOnStartFileNamesはgrails-app/migrationsからの相対パスを指定する。warとして動作する場は空気読んで_Events.groovyでWEB-INF/classes/migrationに置換してくれているから動作環境を心配する必要はない。

そろそろ開発を終えてプロダクション環境にデプロイしてみる。せっかくなのでwarデプロイする。

grails> prod war

生成されたwarをtomcatにデプロイする。

Databaseを確認。さきほどのdevDbではなくprodDbのほう。

$ psql -U test prodDb                                                                                                                                                                                                         20:09:05
psql (9.0.7)
Type "help" for help.

prodDb=> \d
                 List of relations
 Schema |         Name          |   Type   | Owner 
--------+-----------------------+----------+-------
 public | author                | table    | test
 public | book                  | table    | test
 public | databasechangelog     | table    | test
 public | databasechangeloglock | table    | test
 public | hibernate_sequence    | sequence | test
(5 rows)

prodDb=> select * from databasechangelog;
       id        |       author        |     filename      |         dateexecuted          | orderexecuted | exectype |               md5sum               |        description         | comments | tag | liquibase 
-----------------+---------------------+-------------------+-------------------------------+---------------+----------+------------------------------------+----------------------------+----------+-----+-----------
 1351331869483-1 | yamkazu (generated) | add-book.groovy   | 2012-10-27 20:00:41.822337+09 |             1 | EXECUTED | 3:378572087c807b0eae512c8e1bac00b7 | Create Table               |          |     | 2.0.5
 1351331869483-2 | yamkazu (generated) | add-book.groovy   | 2012-10-27 20:00:41.854293+09 |             2 | EXECUTED | 3:1f8c97c0685b3c4f68b4ac2954ed9919 | Create Sequence            |          |     | 2.0.5
 1351333217527-1 | yamkazu (generated) | add-author.groovy | 2012-10-27 20:00:41.870867+09 |             3 | EXECUTED | 3:500d2782c8ddcc7d7b89b0b29b2d7342 | Create Table               |          |     | 2.0.5
 1351333217527-2 | yamkazu (generated) | add-author.groovy | 2012-10-27 20:00:41.884293+09 |             4 | EXECUTED | 3:d4dcaaa9285f777d2a05f33e275488b7 | Add Column                 |          |     | 2.0.5
 1351333217527-3 | yamkazu (generated) | add-author.groovy | 2012-10-27 20:00:41.900688+09 |             5 | EXECUTED | 3:77e6c768bd70db135e30edfb2ef6788e | Add Foreign Key Constraint |          |     | 2.0.5
(5 rows)

prodDb=> 

ちゃんと自動適用されている!
長くなってきたので今日はこのへんで!

GrailsをIntellijからリモートデバッグする

いまだにprintfデバッグを卒業出来ません。

そんなことはどうでもよくてリモートデバッグしてみる。山本さんの
http://d.hatena.ne.jp/mottsnite/20120705/1341495778
に書いてあるとおり2.1からデバッグする際は-debugオプションを使ったほうが良いとのこと。

そもそもIntellijからデバック起動すればいいじゃんって話はあるけど、IDE上からgrailsコマンド実行するとすごい動作が遅いし、操作性もあまり良くなく、結構つらいのでコマンドラインでgrailsコマンド使いつつIntellijでリモートデバッグするという方法を試してみる。

debugオプションの使い方は以下の様な感じ。

grails -debug
grails -debug run-app
grails -debug test-app

インタラクティブモードでも問題ありません。注意としては-debugはgrailsの次に書くこと。

grails run-app -debug

とかだとダメ(2.1.1現在)。で実行してみると以下のようになります。

$ grails -debug                                                                                                                                                                                                                     
Listening for transport dt_socket at address: 5005

5005で待ち受け状態になるでこの状態でIntellijからつなぐ。runメニューの "Edit Configurations..." を選択。

f:id:yamkazu:20121020232752j:plain

"+"ボタンをクリックしてremoteを選択。

f:id:yamkazu:20121020232753j:plain

適当な名前つけて保存。

f:id:yamkazu:20121021010137j:plain

runメニューから今作ったリモートを選択してdebug実行。

f:id:yamkazu:20121021010148j:plain

そうするとコンソールの方で出力が進む。

$ grails -debug                                                                                                                                                                                                                     
Listening for transport dt_socket at address: 5005
| Enter a script name to run. Use TAB for completion: 
grails> 

あとはIntellij側でブレイクポイントを設定して

f:id:yamkazu:20121020232816j:plain

コンソール側でrun-appなりtest-appなりすればブレイクポイントで止まってくれる!
意外と簡単!

Spock0.7で追加されたStubについて

Spock0.7でStubを作る機能が追加されました。
http://docs.spockframework.org/en/latest/interaction_based_testing.html#stubs

Mockとの違いはデフォルトで返す値が違うとのこと。mockはnullを返しますがstubでは

といった感じ。どんな値を返すかはorg.spockframework.mock.EmptyOrDummyResponseで定義されている。すごく小さいクラスなので、ざっとみるだけでもどんな動作をするのかわかると思う。
あとorg.spockframework.smoke.mock.StubDefaultResponses.groovyというテストクラスがあるので、それをみるとわかりやすい。

このStubDefaultResponses.groovyをもとにmockとの動作の違いを確認してみた。

package org.yamkazu

import org.spockframework.mock.MockDetector
import spock.lang.Specification
import spock.lang.Unroll

class MockStubDiffSpec extends Specification {

    @Unroll
    def "#methodの値は、mockの場合は#mockExpected、stubの場合は#stubExpected"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect:
        mock."$method" == mockExpected
        stub."$method" == stubExpected

        where:
        method           | mockExpected | stubExpected
        'byte'           | 0            | 0
        'short'          | 0            | 0
        'int'            | 0            | 0
        'long'           | 0            | 0
        'float'          | 0            | 0
        'double'         | 0            | 0
        'boolean'        | false        | false
        'char'           | 0            | 0

        'byteWrapper'    | null         | 0
        'shortWrapper'   | null         | 0
        'intWrapper'     | null         | 0
        'longWrapper'    | null         | 0
        'floatWrapper'   | null         | 0
        'doubleWrapper'  | null         | 0
        'booleanWrapper' | null         | false
        'charWrapper'    | null         | 0

        'bigInteger'     | null         | BigInteger.ZERO
        'bigDecimal'     | null         | BigDecimal.ZERO

        'charSequence'   | null         | ""
        'string'         | null         | ""
        'GString'        | null         | ""

        'primitiveArray' | null         | [] as int[]
        'interfaceArray' | null         | [] as IPerson[]
        'classArray'     | null         | [] as Person[]

        'iterable'       | null         | []
        'collection'     | null         | []
        'queue'          | null         | []
        'list'           | null         | []
        'set'            | null         | [] as Set
        'map'            | null         | [:]
        'sortedSet'      | null         | [] as Set
        'sortedMap'      | null         | [:]
    }

    def "インタフェースが戻り値のメソッドの場合はそのインタフェースのstubを返す"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect: "mockの場合はnull"
        mock.unknownInterface == null

        and: "stubの場合はそのインタフェースのstubを返す"
        with(stub.unknownInterface) { // 0.7から出来るwithという書き方
            new MockDetector().isMock(it)
            name == ""
            age == 0
            children == []
        }
    }

    def "デフォルトコンストラクタがあるクラスが戻り値の場合はそのクラスのインスタンスを返す"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect: "mockの場合はnull"
        mock.unknownClassWithDefaultCtor == null

        and: "stubの場合はそのクラスの本物(モックではない)のインスタンスを返す"
        with(stub.unknownClassWithDefaultCtor) {
            !new MockDetector().isMock(it)
            name == "default"
            age == 0
            children == null
        }
    }

    // デフォルトコンストラクタがないクラスのstubを作るのに以下が必要となる。
    // "cglib:cglib-nodep:2.2.2"
    // "org.objenesis:objenesis:1.2"
    def "デフォルトコンストラクタがないクラスが戻り値の場合はそのクラスのstubを返す"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect: "mockの場合はnull"
        mock.unknownClassWithoutDefaultCtor == null

        and: "stubの場合はそのクラスのstubを返す"
        with(stub.unknownClassWithoutDefaultCtor) {
            new MockDetector().isMock(it)
            name == ""
            age == 0
            children == []
        }
    }

    static interface TestInterface {
        byte getByte()
        short getShort()
        int getInt()
        long getLong()
        float getFloat()
        double getDouble()
        boolean getBoolean()
        char getChar()

        Byte getByteWrapper()
        Short getShortWrapper()
        Integer getIntWrapper()
        Long getLongWrapper()
        Float getFloatWrapper()
        Double getDoubleWrapper()
        Boolean getBooleanWrapper()
        Character getCharWrapper()

        BigInteger getBigInteger()
        BigDecimal getBigDecimal()

        CharSequence getCharSequence()
        String getString()
        GString getGString()

        int[] getPrimitiveArray()
        IPerson[] getInterfaceArray()
        Person[] getClassArray()

        Iterable getIterable()
        Collection getCollection()
        Queue getQueue()
        List getList()
        Set getSet()
        Map getMap()
        SortedSet getSortedSet()
        SortedMap getSortedMap()

        IPerson getUnknownInterface()
        Person getUnknownClassWithDefaultCtor()
        ImmutablePerson getUnknownClassWithoutDefaultCtor()
    }

    static interface IPerson {
        String getName()
        int getAge()
        List<String> getChildren()
    }

    static class Person implements IPerson {
        String name = "default"
        int age
        List<String> children
    }

    static class ImmutablePerson extends Person {
        ImmutablePerson(String name, int age, List<String> children) {
            this.name = name
            this.age = age
            this.children = children
        }
    }
}

わかれば意外と素直な動作。