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

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

grailsのHibernateでSQLのパラメータまで出力する

元ネタ http://burtbeckwith.com/blog/?p=1604

grailsSQLを出力する方法は2通りあってDataSource.groovyで

dataSource {
    ...
    logSql = true
}

とするか、Config.groovyで

log4j = {
    ...
    debug  'org.hibernate.SQL'
}

とするか。前者は標準出力で、後者はログとして出力される。これで出力されるSQLにパラメータは出力されない。

以下の様な感じ。

    insert 
    into
        book
        (id, version, title) 
    values
        (null, ?, ?)

今までパラメータまで出力する場合は、後者のログの出力レベルを変更してやる方法でorg.hibernate.typeパッケージをtraceとして方法でやっていた。

log4j = {
    ...
    trace  'org.hibernate.type'
    debug  'org.hibernate.SQL'
}

この設定でどんなログが出力されるのか以下のテストを実行。(見やすいようにDataSource.groovyにformatSql=trueを追加して実行)

    def "logの確認"() {
        given:
        new Book(title: 'a').save(flush: true)
        new Book(title: 'b').save(flush: true)
        new Book(title: 'c').save(flush: true)
        Book.withSession { it.clear() } // selectで1次キャッシュを使わないようにクリア

        when:
        def books = Book.list()

        then:
        books.size() == 3
    }

ログは以下のようなに出力される。

| Running 1 spock test... 1 of 1
--Output from logの確認--
2012-10-20 13:08:57,706 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, title) 
    values
        (null, ?, ?)
2012-10-20 13:08:57,707 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:08:57,707 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - a
2012-10-20 13:08:57,710 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, title) 
    values
        (null, ?, ?)
2012-10-20 13:08:57,710 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:08:57,710 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - b
2012-10-20 13:08:57,713 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, title) 
    values
        (null, ?, ?)
2012-10-20 13:08:57,713 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:08:57,713 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - c
2012-10-20 13:08:57,749 [main] DEBUG org.hibernate.SQL - 
    select
        this_.id as id24_0_,
        this_.version as version24_0_,
        this_.title as title24_0_ 
    from
        book this_
2012-10-20 13:08:57,750 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id24_0_]
2012-10-20 13:08:57,750 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version24_0_]
2012-10-20 13:08:57,750 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [a] as column [title24_0_]
2012-10-20 13:08:57,750 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id24_0_]
2012-10-20 13:08:57,750 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version24_0_]
2012-10-20 13:08:57,750 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [b] as column [title24_0_]
2012-10-20 13:08:57,750 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id24_0_]
2012-10-20 13:08:57,751 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version24_0_]
| Completed 1 spock test, 0 failed in 250ms

これで見れるようになる!のだけどselectした時のResultSetに含まれるログが大量に出力される。上の例では問題ないが、ResultSetの件数が多い場合はこれがノイズになる。

ResultSetのパラメータは出力されないようにしたい。上記のログが出力されているパッケージをみるとinsertの時はorg.hibernate.type.descriptor.sql.BasicBinderで、selectの時はorg.hibernate.type.descriptor.sql.BasicExtractorからログ出ていることがわかる。そこで以下のようにConfig.groovyを変更する。

log4j = {
    ...
    trace 'org.hibernate.type.descriptor.sql.BasicBinder'
    debug 'org.hibernate.SQL'
}

この状態でもう一度実行してみる。

| Running 1 spock test... 1 of 1
--Output from logの確認--
2012-10-20 13:07:53,797 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, title) 
    values
        (null, ?, ?)
2012-10-20 13:07:53,798 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:07:53,798 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - a
2012-10-20 13:07:53,801 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, title) 
    values
        (null, ?, ?)
2012-10-20 13:07:53,801 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:07:53,801 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - b
2012-10-20 13:07:53,804 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, title) 
    values
        (null, ?, ?)
2012-10-20 13:07:53,804 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:07:53,805 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - c
2012-10-20 13:07:53,849 [main] DEBUG org.hibernate.SQL - 
    select
        this_.id as id22_0_,
        this_.version as version22_0_,
        this_.title as title22_0_ 
    from
| Completed 1 spock test, 0 failed in 113ms

なかなか良い感じ。

あと元ネタのURLで

hibernate {
   ...
   use_sql_comments = true
}

というsqlコメントを出してくれるオプションがあるようだ。知らなかった。

注意としてDataSource.groovyの dataSource {...} の方ではなく hibernate {...} の方に記述する必要がある。dataSourceの方に記述する方法はないかと調べてみ見たが実装は以下のようになっており、use_sql_commentsに関する処理は無さそう。てことでhibernateに書かなければならない。

// grails 2.1.1での情報
// org.codehaus.groovy.grails.plugins.orm.hibernate.HibernatePluginSupport
// 157行目あたり
            if (ds.loggingSql || ds.logSql) {
                hibProps."hibernate.show_sql" = "true"
            }
            if (ds.formatSql) {
                hibProps."hibernate.format_sql" = "true"
            }

ということで諦めてhibernateの方に書いて実行してみる。

--Output from logの確認--
2012-10-20 13:27:10,397 [main] DEBUG org.hibernate.SQL - 
    /* insert test.Book
        */ insert 
        into
            book
            (id, version, title) 
        values
            (null, ?, ?)
2012-10-20 13:27:10,398 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:27:10,398 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - a
2012-10-20 13:27:10,401 [main] DEBUG org.hibernate.SQL - 
    /* insert test.Book
        */ insert 
        into
            book
            (id, version, title) 
        values
            (null, ?, ?)
2012-10-20 13:27:10,402 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:27:10,402 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - b
2012-10-20 13:27:10,404 [main] DEBUG org.hibernate.SQL - 
    /* insert test.Book
        */ insert 
        into
            book
            (id, version, title) 
        values
            (null, ?, ?)
2012-10-20 13:27:10,405 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-10-20 13:27:10,405 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - c
2012-10-20 13:27:10,462 [main] DEBUG org.hibernate.SQL - 
    /* criteria query */ select
        this_.id as id32_0_,
        this_.version as version32_0_,
        this_.title as title32_0_ 
    from
        book this_
2012-10-20 13:27:10,462 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id32_0_]
2012-10-20 13:27:10,463 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version32_0_]
2012-10-20 13:27:10,463 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [a] as column [title32_0_]
2012-10-20 13:27:10,463 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id32_0_]
2012-10-20 13:27:10,463 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version32_0_]
2012-10-20 13:27:10,463 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [b] as column [title32_0_]
2012-10-20 13:27:10,463 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id32_0_]
2012-10-20 13:27:10,463 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version32_0_]
| Completed 1 spock test, 0 failed in 135ms

でたが/* criteria query */とかでそんなに嬉しくないけど、出てないより出ていたほうが何かの助けるなるかもしれない。試しに

        println '=' * 100
        Book.withCriteria {
            eq 'title', 'a'
        }

        println '=' * 100
        Book.where {
            title == 'a'
        }.list()

        println '=' * 100
        Book.findByTitle('a')

        println '=' * 100
        Book.findAll('from Book as b where b.title = :title', [title: 'a'])

というコードを実行してみたら以下の様に出た。

====================================================================================================
2012-10-20 13:34:41,168 [main] DEBUG org.hibernate.SQL - 
    /* criteria query */ select
        this_.id as id40_0_,
        this_.version as version40_0_,
        this_.title as title40_0_ 
    from
        book this_ 
    where
        this_.title=?
2012-10-20 13:34:41,168 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - a
2012-10-20 13:34:41,169 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id40_0_]
====================================================================================================
2012-10-20 13:34:41,184 [main] DEBUG org.hibernate.SQL - 
    /* criteria query */ select
        this_.id as id40_0_,
        this_.version as version40_0_,
        this_.title as title40_0_ 
    from
        book this_ 
    where
        this_.title=?
2012-10-20 13:34:41,185 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - a
2012-10-20 13:34:41,185 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id40_0_]
====================================================================================================
2012-10-20 13:34:41,207 [main] DEBUG org.hibernate.SQL - 
    /* criteria query */ select
        this_.id as id40_0_,
        this_.version as version40_0_,
        this_.title as title40_0_ 
    from
        book this_ 
    where
        this_.title=? limit ?
2012-10-20 13:34:41,209 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - a
2012-10-20 13:34:41,210 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id40_0_]
====================================================================================================
2012-10-20 13:34:41,226 [main] DEBUG org.hibernate.SQL - 
    /* 
from
    Book as b 
where
    b.title = :title */ select
        book0_.id as id40_,
        book0_.version as version40_,
        book0_.title as title40_ 
    from
        book book0_ 
    where
        book0_.title=?
2012-10-20 13:34:41,227 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - a
| Completed 1 spock test, 0 failed in 177ms

なるほどHQLで便利は場面はあるかもしれない。てことでHibernateログを出すときは
Config.groovyに

log4j = {
    ...
    trace  'org.hibernate.type.descriptor.sql.BasicBinder'
    debug  'org.hibernate.SQL'
}

を追加。DataSource.groovyに

hibernate {
    ...
    format_sql = true
    use_sql_comments = true
}

を追加がお勧めかな。

grails2.1.1で追加されたfirst()、last()

grails2.1.1からドメインのメソッドにfirstとlastが追加されています。

http://grails.org/doc/latest/ref/Domain%20Classes/first.html
http://grails.org/doc/latest/ref/Domain%20Classes/last.html

使い方は

Book.first()
Book.first('title')
Book.first(sort: 'title')
Book.last()
Book.last('title')
Book.last(sort: 'title')

内部的にはlist()を使っていて、firstの方は昇順でソートして取得数1、lastは降順でソートして取得数1、という感じです。sortの引数を省略するとidでソートされる模様。

    ...
    D first(Map queryParams) {
        queryParams.max = 1
        queryParams.order = 'asc'
        if(!queryParams.containsKey('sort')) {
            def idPropertyName = persistentEntity.identity?.name
            if(idPropertyName) {
                queryParams.sort = idPropertyName
            }
        }
        def resultList = list(queryParams)
        resultList ? resultList[0] : null
    }
    ...
    D last(Map queryParams) {
        queryParams.max = 1
        queryParams.order = 'desc'
        if(!queryParams.containsKey('sort')) {
            def idPropertyName = persistentEntity.identity?.name
            if(idPropertyName) {
                queryParams.sort = idPropertyName
            }
        }
        def resultList = list(queryParams)
        resultList ? resultList[0] : null
    }
    ...