Grailsの関連の型での動作の違いを理解する

ドメイン間の関連の型をどの様に設定するかでの挙動の違いです。リファレンスに書いてあることですが、いろいろ動かしながら検証してみました。

ログとかだらだら長くて見にくいですが、面白いので貼り付けときました。

Bookドメイン

class Book {

    String title

    static belongsTo = [author: Author]

}

Authorドメイン

class Author {

    String name

    static hasMany = [books: Book]

}

このときのスキーマは以下のようになります(H2の場合)。

create table author (
    id bigint generated by default as identity,
    version bigint not null,
    name varchar(255) not null,
    primary key (id)
);

create table book (
    id bigint generated by default as identity,
    version bigint not null,
    author_id bigint not null,
    title varchar(255) not null,
    primary key (id)
);

相互に関連が設定されているのでmappingテーブルが作成されていません。

Authorでbooksというone-to-manyな関連を定義していますが、特に型を指定していない場合はデフォルトjava.util.Setが使われます。

ここでテストを実行してみます。

class AuthorSpec extends IntegrationSpec {

    def setup() {
        Author.withNewSession {
            def author = new Author(name: "dummy")
            author.addToBooks(new Book(title: "hoge"))
            author.addToBooks(new Book(title: "foo"))
            author.addToBooks(new Book(title: "bar"))
            author.save()
        }
    }

    def "addToBooksでbookを保存する"() {
        setup:
        def author = Author.findByName("dummy")

        when:
        author.addToBooks(new Book(title: "test"))
        author.save(flush: true)

        then:
        Book.findByTitle("test").author == author
    }

}

実際に実行しみるとテストメソッドでは以下のSQLが発行されます。

2012-07-18 16:52:19,360 [main] DEBUG hibernate.SQL  - 
    select
        this_.id as id1_0_,
        this_.version as version1_0_,
        this_.name as name1_0_ 
    from
        author this_ 
    where
        this_.name=? limit ?
2012-07-18 16:52:19,408 [main] DEBUG hibernate.SQL  - 
    select
        books0_.author_id as author3_1_1_,
        books0_.id as id1_,
        books0_.id as id0_0_,
        books0_.version as version0_0_,
        books0_.author_id as author3_0_0_,
        books0_.title as title0_0_ 
    from
        book books0_ 
    where
        books0_.author_id=?
2012-07-18 16:52:19,471 [main] DEBUG hibernate.SQL  - 
    insert 
    into
        book
        (id, version, author_id, title) 
    values
        (null, ?, ?, ?)
2012-07-18 16:52:19,507 [main] DEBUG hibernate.SQL  - 
    update
        author 
    set
        version=?,
        name=? 
    where
        id=? 
        and version=?
2012-07-18 16:52:19,520 [main] DEBUG hibernate.SQL  - 
    select
        this_.id as id0_0_,
        this_.version as version0_0_,
        this_.author_id as author3_0_0_,
        this_.title as title0_0_ 
    from
        book this_ 
    where
        this_.title=? limit ?

面白いのはauthor.addToBooksしたタイミンで一度、そのときの関連全てを取得するためのselectが走ることです。これは関連がjava.util.Setが定義されているからで、books内のエンティティがユニークであることを保証するために、関連がこのタイミングでレイジーにロードされます。試しに、重複した要素を追加してみます。

    def "addToBooksで重複したbookを追加する"() {
        setup:
        def author = Author.findByName("dummy")
        def book = Book.findByTitle("hoge")

        when:
        author.addToBooks(book)
        author.save(flush: true)

        then:
        author.books.size() == 3
    }

実行すると以下の様なSQLが実行されます。

2012-07-18 19:05:19,917 [main] DEBUG hibernate.SQL  - 
    select
        this_.id as id0_0_,
        this_.version as version0_0_,
        this_.name as name0_0_ 
    from
        author this_ 
    where
        this_.name=? limit ?
2012-07-18 19:05:20,009 [main] DEBUG hibernate.SQL  - 
    select
        this_.id as id1_0_,
        this_.version as version1_0_,
        this_.author_id as author3_1_0_,
        this_.title as title1_0_ 
    from
        book this_ 
    where
        this_.title=? limit ?
2012-07-18 19:05:20,036 [main] DEBUG hibernate.SQL  - 
    select
        books0_.author_id as author3_0_1_,
        books0_.id as id1_,
        books0_.id as id1_0_,
        books0_.version as version1_0_,
        books0_.author_id as author3_1_0_,
        books0_.title as title1_0_ 
    from
        book books0_ 
    where
        books0_.author_id=?

author.addToBooks(book)で要素を追加したタイミングで関連を取得していますが、すでに追加されている要素なので、重複して追加されずsave()しても特に何も起きません。これとは別に明示的にSortedSetで宣言して関連先のcompareToを実装して、ソート済み状態にすることも出来ます。

次にAuthorドメインでbooksの型を明示的にListに設定してみます。

class Author {

    String name
    
    List books

    static hasMany = [books: Book]

}

こんどは以下の様なスキーマが生成されるようになります。

create table author (
    id bigint generated by default as identity,
    version bigint not null,
    name varchar(255) not null,
    primary key (id)
);

create table book (
    id bigint generated by default as identity,
    version bigint not null,
    author_id bigint not null,
    title varchar(255) not null,
    books_idx integer,
    primary key (id)
);

books_idxというbooksの順序を保存するためのカラムが追加されているのが特徴的です。こいつの動作を検証するテストを書いてみます。

def "booksに追加したり、ソートしたり、削除したり"() {
    setup:
    def author = Author.findByName("dummy")

    when:
    println "新規にBookを追加 ${'>' * 100}"
    author.addToBooks(new Book(title: "test"))
    author.save(flush: true)

    then:
    Author.withNewSession { Author.get(author.id).books*.title } == ["hoge", "foo", "bar", "test"]

    when:
    println "ソートして保存 ${'>' * 100}"
    author.books.sort { it.title }
    author.save(flush: true)

    then:
    Author.withNewSession { Author.get(author.id).books*.title } == ["bar", "foo", "hoge", "test"]

    when:
    println "要素を削除して保存 ${'>' * 100}"
    author.books.remove(1).delete()
    author.save(flush: true)

    then:
    Author.withNewSession { Author.get(author.id).books*.title } == ["bar", "hoge", "test"]
}

ちょっとださいけどログがみやすいようにprint仕込んでます。あとthenで検証する際に1次キャッシュを明示的に使用しないようにwithNewSessionでクエリを実行しています。これを実行すると以下の様なログになりました(バインドされている値も一緒にだしているのでかなり長めです...)。

2012-07-19 23:34:07,569 [main] DEBUG org.hibernate.SQL - 
    select
        this_.id as id1_0_,
        this_.version as version1_0_,
        this_.name as name1_0_ 
    from
        author this_ 
    where
        this_.name=? limit ?
2012-07-19 23:34:07,569 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - dummy
2012-07-19 23:34:07,575 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id1_0_]
2012-07-19 23:34:07,577 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version1_0_]
2012-07-19 23:34:07,578 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [dummy] as column [name1_0_]
新規にBookを追加 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2012-07-19 23:34:07,605 [main] DEBUG org.hibernate.SQL - 
    select
        books0_.author_id as author3_1_1_,
        books0_.id as id1_,
        books0_.books_idx as books5_1_,
        books0_.id as id5_0_,
        books0_.version as version5_0_,
        books0_.author_id as author3_5_0_,
        books0_.title as title5_0_ 
    from
        book books0_ 
    where
        books0_.author_id=?
2012-07-19 23:34:07,605 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,608 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id5_0_]
2012-07-19 23:34:07,609 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,609 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,609 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [hoge] as column [title5_0_]
2012-07-19 23:34:07,609 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,609 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id1_]
2012-07-19 23:34:07,612 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [books5_1_]
2012-07-19 23:34:07,612 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id5_0_]
2012-07-19 23:34:07,612 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,612 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [foo] as column [title5_0_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id1_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [books5_1_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id5_0_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [bar] as column [title5_0_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id1_]
2012-07-19 23:34:07,613 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [books5_1_]
2012-07-19 23:34:07,643 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, title, author_id, books_idx) 
    values
        (null, ?, ?, ?, ?)
2012-07-19 23:34:07,643 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 0
2012-07-19 23:34:07,643 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - test
2012-07-19 23:34:07,644 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 1
2012-07-19 23:34:07,645 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [INTEGER] - 3
2012-07-19 23:34:07,668 [main] DEBUG org.hibernate.SQL - 
    update
        author 
    set
        version=?,
        name=? 
    where
        id=? 
        and version=?
2012-07-19 23:34:07,669 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,669 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - dummy
2012-07-19 23:34:07,669 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 1
2012-07-19 23:34:07,669 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 0
2012-07-19 23:34:07,673 [main] DEBUG org.hibernate.SQL - 
    update
        book 
    set
        author_id=?,
        books_idx=? 
    where
        id=?
2012-07-19 23:34:07,673 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,673 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [INTEGER] - 3
2012-07-19 23:34:07,673 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 4
2012-07-19 23:34:07,688 [main] DEBUG org.hibernate.SQL - 
    select
        author0_.id as id1_0_,
        author0_.version as version1_0_,
        author0_.name as name1_0_ 
    from
        author author0_ 
    where
        author0_.id=?
2012-07-19 23:34:07,688 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,689 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [version1_0_]
2012-07-19 23:34:07,689 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [dummy] as column [name1_0_]
2012-07-19 23:34:07,692 [main] DEBUG org.hibernate.SQL - 
    select
        books0_.author_id as author3_1_1_,
        books0_.id as id1_,
        books0_.books_idx as books5_1_,
        books0_.id as id5_0_,
        books0_.version as version5_0_,
        books0_.author_id as author3_5_0_,
        books0_.title as title5_0_ 
    from
        book books0_ 
    where
        books0_.author_id=?
2012-07-19 23:34:07,693 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,694 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id5_0_]
2012-07-19 23:34:07,697 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,697 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,697 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [hoge] as column [title5_0_]
2012-07-19 23:34:07,697 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id1_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [books5_1_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id5_0_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [foo] as column [title5_0_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id1_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [books5_1_]
2012-07-19 23:34:07,698 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [bar] as column [title5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id1_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [books5_1_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [4] as column [id5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [test] as column [title5_0_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [4] as column [id1_]
2012-07-19 23:34:07,699 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [books5_1_]
ソートして保存 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2012-07-19 23:34:07,744 [main] DEBUG org.hibernate.SQL - 
    update
        author 
    set
        version=?,
        name=? 
    where
        id=? 
        and version=?
2012-07-19 23:34:07,746 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 2
2012-07-19 23:34:07,746 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - dummy
2012-07-19 23:34:07,746 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 1
2012-07-19 23:34:07,746 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 1
2012-07-19 23:34:07,747 [main] DEBUG org.hibernate.SQL - 
    update
        book 
    set
        author_id=?,
        books_idx=? 
    where
        id=?
2012-07-19 23:34:07,747 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,747 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [INTEGER] - 0
2012-07-19 23:34:07,747 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 3
2012-07-19 23:34:07,747 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,747 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [INTEGER] - 2
2012-07-19 23:34:07,748 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 1
2012-07-19 23:34:07,757 [main] DEBUG org.hibernate.SQL - 
    select
        author0_.id as id1_0_,
        author0_.version as version1_0_,
        author0_.name as name1_0_ 
    from
        author author0_ 
    where
        author0_.id=?
2012-07-19 23:34:07,758 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,758 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [version1_0_]
2012-07-19 23:34:07,758 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [dummy] as column [name1_0_]
2012-07-19 23:34:07,759 [main] DEBUG org.hibernate.SQL - 
    select
        books0_.author_id as author3_1_1_,
        books0_.id as id1_,
        books0_.books_idx as books5_1_,
        books0_.id as id5_0_,
        books0_.version as version5_0_,
        books0_.author_id as author3_5_0_,
        books0_.title as title5_0_ 
    from
        book books0_ 
    where
        books0_.author_id=?
2012-07-19 23:34:07,760 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,761 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id5_0_]
2012-07-19 23:34:07,762 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,762 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,762 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [hoge] as column [title5_0_]
2012-07-19 23:34:07,762 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,762 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id1_]
2012-07-19 23:34:07,762 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [books5_1_]
2012-07-19 23:34:07,762 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [foo] as column [title5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [id1_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [books5_1_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [bar] as column [title5_0_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,763 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id1_]
2012-07-19 23:34:07,764 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [books5_1_]
2012-07-19 23:34:07,764 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [4] as column [id5_0_]
2012-07-19 23:34:07,764 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,764 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,764 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [test] as column [title5_0_]
2012-07-19 23:34:07,764 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,764 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [4] as column [id1_]
2012-07-19 23:34:07,765 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [books5_1_]
要素を削除して保存 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2012-07-19 23:34:07,789 [main] DEBUG org.hibernate.SQL - 
    update
        author 
    set
        version=?,
        name=? 
    where
        id=? 
        and version=?
2012-07-19 23:34:07,790 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 3
2012-07-19 23:34:07,790 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - dummy
2012-07-19 23:34:07,790 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 1
2012-07-19 23:34:07,790 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [4] as [BIGINT] - 2
2012-07-19 23:34:07,790 [main] DEBUG org.hibernate.SQL - 
    update
        book 
    set
        author_id=?,
        books_idx=? 
    where
        id=?
2012-07-19 23:34:07,791 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,791 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [INTEGER] - 1
2012-07-19 23:34:07,791 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 1
2012-07-19 23:34:07,791 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,791 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [INTEGER] - 2
2012-07-19 23:34:07,791 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - 4
2012-07-19 23:34:07,792 [main] DEBUG org.hibernate.SQL - 
    delete 
    from
        book 
    where
        id=? 
        and version=?
2012-07-19 23:34:07,793 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 2
2012-07-19 23:34:07,793 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - 0
2012-07-19 23:34:07,810 [main] DEBUG org.hibernate.SQL - 
    select
        author0_.id as id1_0_,
        author0_.version as version1_0_,
        author0_.name as name1_0_ 
    from
        author author0_ 
    where
        author0_.id=?
2012-07-19 23:34:07,812 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,812 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [version1_0_]
2012-07-19 23:34:07,812 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [dummy] as column [name1_0_]
2012-07-19 23:34:07,815 [main] DEBUG org.hibernate.SQL - 
    select
        books0_.author_id as author3_1_1_,
        books0_.id as id1_,
        books0_.books_idx as books5_1_,
        books0_.id as id5_0_,
        books0_.version as version5_0_,
        books0_.author_id as author3_5_0_,
        books0_.title as title5_0_ 
    from
        book books0_ 
    where
        books0_.author_id=?
2012-07-19 23:34:07,815 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - 1
2012-07-19 23:34:07,816 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id5_0_]
2012-07-19 23:34:07,818 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,818 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,818 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [hoge] as column [title5_0_]
2012-07-19 23:34:07,818 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,818 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [id1_]
2012-07-19 23:34:07,818 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [books5_1_]
2012-07-19 23:34:07,818 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [bar] as column [title5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [3] as column [id1_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [books5_1_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [4] as column [id5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [0] as column [version5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [test] as column [title5_0_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [1] as column [author3_1_1_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [4] as column [id1_]
2012-07-19 23:34:07,819 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - found [2] as column [books5_1_]

これからわかったことはaddToBooksのタイミングで一度関連をレイジーに取りに行く、その時のクエリにidxでorderbyしていない、これはgrails?側でソートきかせている?あと、ソートしたり、要素を消したりするとidxのupdateが走るといったとこでしょうか。これは意図せず大量のUPDATE文走らせそうで注意。

こんどはHibernateのBag型をつかってみます。Grailsでこれを使うにはCollection型で宣言する必要があります。

class Author {

    String name

    Collection books

    static hasMany = [books: Book]

}

んで以下のテストを実行。

def "addToBooksでbookを保存する"() {
    setup:
    def author = Author.findByName("dummy")

    when:
    author.addToBooks(new Book(title: "test"))
    author.save(flush: true)

    then:
    Author.withNewSession { Author.get(author.id).books*.title } as Set == ['hoge', 'foo', 'bar', 'test'] as Set
}

実行ログ。

2012-07-21 14:53:23,117 [main] DEBUG org.hibernate.SQL - 
    select
        this_.id as id1_0_,
        this_.version as version1_0_,
        this_.name as name1_0_ 
    from
        author this_ 
    where
        this_.name=? limit ?
2012-07-21 14:53:23,168 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        book
        (id, version, author_id, title) 
    values
        (null, ?, ?, ?)
2012-07-21 14:53:23,196 [main] DEBUG org.hibernate.SQL - 
    update
        author 
    set
        version=?,
        name=? 
    where
        id=? 
        and version=?
2012-07-21 14:53:23,214 [main] DEBUG org.hibernate.SQL - 
    select
        author0_.id as id1_0_,
        author0_.version as version1_0_,
        author0_.name as name1_0_ 
    from
        author author0_ 
    where
        author0_.id=?
2012-07-21 14:53:23,218 [main] DEBUG org.hibernate.SQL - 
    select
        books0_.author_id as author3_1_1_,
        books0_.id as id1_,
        books0_.id as id3_0_,
        books0_.version as version3_0_,
        books0_.author_id as author3_3_0_,
        books0_.title as title3_0_ 
    from
        book books0_ 
    where
        books0_.author_id=?

特徴はaddToBooksしたタイミングで、関連を引っ張るselectが走りません。これはBag型がソート、重複を管理しないためです。

ということで改めてSet、List、Collectionを比較してみると、特に制約がないならCollectionを使うとパフォーマンスが一番良い。

ソート順を保持する必要があるなら、SetでSortedSetを使用するか、Listを使う必要がある。Listは仕組み的に必ず関連を一度すべて取得しなければならないが、Setのほうが回避方法がある。

def "addToBooksでbookを保存する"() {
    setup:
    def author = Author.findByName("dummy")

    when:
    def book = new Book(title: "test")
    book.author = author
    book.save(flush: true)

    then:
    Author.withNewSession { Author.get(author.id).books*.title } as Set == ['hoge', 'foo', 'bar', 'test'] as Set
}

といった具合にauthor経由でカスケードさせるのではなく単にbookを保存すれば、余計なクエリーが走らないため、パフォーマンスが良い。Listは上記のように保存できない。

とそれぞれ特徴があるので、用途に合わせて使う必要がありますが、きちんと理解していないと、意図せず大量の関連を取得するSELECTや、順番を保持するために大量のUPDATEが走ることになるので注意が必要です。

GrailsのTaglib使っててid属性がどこかへいってしまう

htmlのid属性と、grailsのlink生成のためのドメインIDが混在していてややこしいのですが、例えばg:link

<a href="/appname/book/list" id="book-list">book list</a>

というようなHTMLを期待して

<g:link controller="book" action="list" id="book-list">book list</g:link>

といういうふうにg:linkを使うと実際には

<a href="/appname/book/list/book-list">book list</a>

となってしまいます。これをg:linkで回避するにはidではなくelementIdを指定します。

<g:link controller="book" action="list" elementId="book-list">book list</g:link>

そうすると期待する

<a href="/appname/book/list" id="book-list">book list</a>

になります。

g:formの場合はまた、ちょっと動作が違っていて

<g:form controller="book" action="create" id="create-book">
  ...
</g:form>

ではなく

<g:form url="[controller: 'book', action: 'create']" id="create-book">
  ...
</g:form>

というふうにurl属性を使用すると回避できます。なぜかこちらidのままでおk(なんで統一されていなの?)。

全部調べてませんが、他のタグでも困ったらリファレンス参照するか、grailsのtaglibのソースを探ると良いです。

Grailsでテーブル毎にシーケンスを自動生成する方法

参考 http://grails.1312388.n4.nabble.com/One-hibernate-sequence-is-used-for-all-Postgres-tables-td1351722.html

むかしJIRAにもチケット上がっていたみたいですが、
http://jira.grails.org/browse/GRAILS-3138
GrailsとうかHibernateの問題なのでcloseされた模様。

ということで、GrailsというよりHibernateの話ではあるのですがPostgreSQLDialectなど、getNativeIdentifierGeneratorClassにデフォルトのSequenceGeneratorが使用されている場合

...
public Class getNativeIdentifierGeneratorClass() {
    return SequenceGenerator.class;
}
...

というような実装になっており、とくにマッピングの指定など行わないと全テーブル共通でidの採番がhibernate_sequenceから行われてしまいます(嬉しいことないよね?)。

これをテーブル毎にシーケンスが作成されるようにしたいということでカスタムのDialectとSequenceGeneratorを作成します。こんなん(groovyで書いています)。

class MyDialect extends PostgreSQLDialect {

    @Override
    Class<?> getNativeIdentifierGeneratorClass() {
        TableNameSequenceGenerator.class
    }

    static class TableNameSequenceGenerator extends SequenceGenerator {

        @Override
        void configure(Type type, Properties params, Dialect dialect) {
            if (!params.getProperty(SEQUENCE)) {
                String tableName = params.getProperty(PersistentIdentifierGenerator.TABLE)
                if (tableName) {
                    params.setProperty(SEQUENCE, "${tableName}_id_seq")
                }
            }
            super.configure(type, params, dialect)
        }

    }

}

こいつをDataSource.groovyのdialectに自分が作ったMyDialectを差し替えればおk。

dataSource {
    driverClassName = "org.postgresql.Driver"
    dialect = org.yamkazu.portgresql.MyDialect
    dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
    url = "jdbc:postgresql:grails"
    username = "grails"
    password = "grails"
}

bookとauthorというドメインの場合以下の様な感じで、author_id_seq、book_id_seqが生成されます。

grails=# \ds
             List of relations
 Schema |     Name      |   Type   | Owner  
--------+---------------+----------+--------
 public | author_id_seq | sequence | grails
 public | book_id_seq   | sequence | grails
(2 rows)

2012/07/13修正 DataSource.groovyの追記方法とシーケンス名の指定を少し修正しました

GrailsのValidationMessageでクラス名とかプロパティ名のi18n対応をどこでやっているのか

grails2.1.0での情報。

org.codehaus.groovy.grails.validation.AbstractConstraint#rejectValueWithDefaultMessageで処理している。

...
final Locale locale = LocaleContextHolder.getLocale();
final Class<?> constrainedClass = (Class<?>) args[1];
final String fullClassName = constrainedClass.getName();

String classNameCode = fullClassName + ".label";
String resolvedClassName = messageSource.getMessage(classNameCode, null, fullClassName, locale);
final String classAsPropertyName = GrailsNameUtils.getPropertyName(constrainedClass);

if (resolvedClassName.equals(fullClassName)) {
    // try short version
    classNameCode = classAsPropertyName+".label";
    resolvedClassName = messageSource.getMessage(classNameCode, null, fullClassName, locale);
}

// update passed version
if (!resolvedClassName.equals(fullClassName)) {
    args[1] = resolvedClassName;
}

String propertyName = (String)args[0];
String propertyNameCode = fullClassName + '.' + propertyName + ".label";
String resolvedPropertyName = messageSource.getMessage(propertyNameCode, null, propertyName, locale);
if (resolvedPropertyName.equals(propertyName)) {
    propertyNameCode = classAsPropertyName + '.' + propertyName + ".label";
    resolvedPropertyName = messageSource.getMessage(propertyNameCode, null, propertyName, locale);
}

// update passed version
if (!resolvedPropertyName.equals(propertyName)) {
    args[0] = resolvedPropertyName;
}
...

プロパティ名の例で行くと、 fullClassName + '.' + propertyName + ".label"があればそれ、なければ classAsPropertyName + '.' + propertyName + ".label"でmessageSourceを元に解決を行う。

例えばorg.yamkazu.Book#titleというプロパティがあったらmessages.propertiesに

org.yamkazu.Book.title.label=hogehoge

と書くか

book.title.label=hogehoge

のどちらかで解決できる。

Groovy2.0で追加された@NotYetImplemented

元ネタ http://blog.andresteingress.com/2012/03/04/using-notyetimplemented-in-test-cases/

@NotYetImplementedとそのテストメソッドは失敗しなくてはいけなくて、逆に成功してしまうと検証エラーとして扱われるみたい。

class EchoService {

    def echo(arg) {
        // Not Yet Implemented
    }

}

というプロダクトコードがあって、以下の様なテスコトード。Spockでやってみた。

import groovy.transform.NotYetImplemented
import spock.lang.Specification

class NotYetImplementedSpec extends Specification {

    @NotYetImplemented
    def "失敗しないとダメ"() {
        given:
        def service = new EchoService()

        when:
        def result = service.echo('hello')

        then:
        result == 'hello'
    }

}

これを実行するとグリーンになる。この状態で実装してみる。

class EchoService {

    def echo(arg) {
        arg
    }

}

テスト実行する。成功してしまうと

junit.framework.AssertionFailedError: Method is marked with @NotYetImplemented but passes unexpectedly
	at org.yamkazu.NotYetImplementedSpec.失敗しないとダメ(NotYetImplementedSpec.groovy:8)

という感じ。

あと現時点(2012/7/6)でGroovy2.0でSpockを使う場合はSNAPSHOTバージョンを使う必要があります。
http://code.google.com/p/spock/wiki/SpockVersionsAndDependencies

Groovy2.0で追加されたMatcher#matchesPartially

元ネタ http://mrhaki.blogspot.fr/2012/06/groovy-goodness-partial-matches.html

APIはこのへん。
http://groovy.codehaus.org/groovy-jdk/java/util/regex/Matcher.html#matchesPartially()

なかなか説明が難しいのですが、とある文字列があって、その文字列に続く文字によってマッチする可能性があるものをtrueとして扱い、もうその文字列に続く文字がなんでもあってもマッチする可能性がないというものをfalseとして扱うという感じでしょう。

def regex = /(090|080)-\d{4}-\d{4}/

def matcher = '090-1234-5678' =~ regex
assert matcher.matchesPartially()

matcher = '080-0987-6543' =~ regex
assert matcher.matchesPartially() 

matcher = '090-09' =~ regex
assert matcher.matchesPartially()

matcher = '0' =~ regex
assert matcher.matchesPartially()

matcher = '07' =~ regex
assert matcher.matchesPartially() == false

matcher = '090-98765' =~ regex
assert matcher.matchesPartially() == false

Groovy2.0でListに追加されたwithDefault、withEagerDefault、withLazyDefault

withDefaultはwithLazyDefaultのエイリアスなので機能的にはwithEagerDefault、withLazyDefaultが追加されました。今までmapには似たようなのがありましたが、今回Listにも追加されました。

それぞれ要素を取得した際の要素のパディング方法や、nullの扱いが若干違います。

まずwithLazyDefaultから。要素を取得したタイミングで初期化されnullの要素が特別な意味(初期化対象)を持っています。

def items = [1, 2].withLazyDefault { it * it }

assert items == [1, 2]

assert items[4] == 16
// 4を取ると間の要素をnullでパディング
assert items == [1, 2, null, null, 16]

assert items[3] == 9
// パディングされたnullの要素を取得するとそのタイミングで初期化されている
assert items == [1, 2, null, 9, 16]

// 自分でnullを入れてみる
items[1] = null
// 自分で入れたnullでも取得するとそのタイミングで初期化される
assert items[1] == 1
assert items == [1, 1, null, 9, 16]

次にwithEagerDefault。先ほどと違いパディングする際にその間の値も初期化されるのと、nullがnullとして扱われる点がEagerと異なります。

def items = [1, 2].withEagerDefault { it * it }

assert items == [1, 2]

assert items[4] == 16
// 4を取ると間の要素もあわせて初期化される
assert items == [1, 2, 4, 9, 16]

// 自分でnullを入れてみる
items[1] = null
// 自分で入れたnullはnullとして扱われる
// withLazyDefaultと違いnullを取得する際にその要素が初期化されたりしない
assert items[1] == null
assert items == [1, null, 4, 9, 16]