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でテーブル毎にシーケンスを自動生成する方法
むかし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]