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-person
とadd-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は実行されません。
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..." を選択。
"+"ボタンをクリックしてremoteを選択。
適当な名前つけて保存。
runメニューから今作ったリモートを選択してdebug実行。
そうするとコンソールの方で出力が進む。
$ grails -debug Listening for transport dt_socket at address: 5005 | Enter a script name to run. Use TAB for completion: grails>
あとはIntellij側でブレイクポイントを設定して
コンソール側で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 } } }
わかれば意外と素直な動作。