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

Grails 2.1 から標準でインストールされているCache Pluginを試す

http://grails.org/plugin/cachegrails 2.1からデフォルトでインストールされています。実態としてはSpringのCache Abstractionを薄くラップしたような形です。Serviceのメソッドや、Controller、またはGSPのレンダリングをキャッシュできます。

メソッドに付与する使い方は非常にシンプルで、grails.plugin.cacheパッケージにある以下の3つのアノテーションを使用します。

  • @Cacheable
  • @CacheEvict
  • @CachePut

それぞれ個別に見ていきます。

@Cacheable

このアノテーションを付与するとメソッドの戻り値をキャッシュします。例えば以下のような形です。

class BookService {

    @Cacheable('books')
    Book get(id) {
        Book.get(id)
    }

}

@Cacheableにはvalue属性を設定しますが、これはキャッシュする情報に任意の名前を設定します。この名前毎にキャッシュ領域が作られることになります。

また、@Cacheableはkey属性を持ちます。これは省略可能で、省略した場合は自動的にメソッドの仮引数がkeyに設定されます。

上記の例の

    @Cacheable('books')
    Book get(id) {

    @Cacheable(value = 'books', key = '#id')
    Book get(id) {

と同じ意味になります。#idとしているようにkeyの指定はSpEL(Spring Expression Language)が使用できます。これを使用することで仮引数がオブジェクトの場合にプロパティをたぐっていき#obj.idといったような指定も可能です。詳しくはSpringのドキュメントを参照してください。

簡単なテストをGrailsのIntegrationTestで書いてみます。

class Book {
    String title
}
import grails.plugin.spock.IntegrationSpec

class BookServiceSpec extends IntegrationSpec {

    def bookService

    def grailsCacheManager

    def cleanup() {
        grailsCacheManager.destroyCache('books')
    }

    def "キャッシュ追加の動作確認"() {
        given:
        def book = new Book(title: 'dummyTitle').save(flush: true)
        book.discard() // hibernateの1次キャッシュから取得しないようdiscard()する。

        and: "キャッシュには何も保存されていない"
        assert booksCache.size() == 0

        when:
        def cachedBook = bookService.get(book.id)

        then:
        cachedBook.title == 'dummyTitle'

        and: "キャッシュ取得したオブジェクトがキャッシュされているはず"
        booksCache == [(book.id): cachedBook]

        when: "直接永続化されている情報を変更する"
        cachedBook.discard() // book.save()した際にbookがhibernateの1キャッシュに乗るで二重ロードにならないようcachedBookはdiscard()する。
        book.title = 'newTitle'
        book.save(flush: true)

        then: "キャッシュには影響がない"
        booksCache[book.id].title == 'dummyTitle'

        when: "メソッドを経由してちゃんとキャッシュした情報が取得されるか確認"
        cachedBook = bookService.get(book.id)

        then: "キャッシュの値は変更されていない"
        cachedBook.title == 'dummyTitle'
    }

    private getBooksCache() {
        grailsCacheManager.getCache('books').getNativeCache()
    }

}

まずは

class BookServiceSpec extends IntegrationSpec {
    ...
    def grailsCacheManager
    ...
    private getBooksCache() {
        grailsCacheManager.getCache('books').getNativeCache()
    }

}

について。これはSpringのCacheManagerインタフェースを継承したGrialsCacheManagerインタフェースの実装がSpringBeanとして登録されます。Cache Pluginは cache-ehcache、cache-redisといったプラグインと併用することで拡張可能ですが、とく指定しなければデフォルトではGrailsConcurrentMapCacheManagerが使われます。名前の通りConcurrentHashMapを使用した単純な実装です。

上記のコードはgrailsCacheManagerにGrailsConcurrentMapCacheManagerのインスタンスがイジェクトされ、grailsCacheManager.getCache('books').getNativeCache()でbooksをキャッシュしているConcurrentHashMapのインスタンスを取得できます。getNativeCache()の戻り値はObjectで定義されており、GrailsConcurrentMapCacheManagerの場合、ConcurrentHashMapのインスタンスになるだけで、他の実装を使用する場合はきっと違う何かがとれるでしょう(よくわからない)。

テストコードの方ではbookService.getを初めて呼び出したタイミングで、その取得されたオブジェクトがキャッシュされます。以後、何度 bookService.getを呼び出しても、@CacheEvict使ってキャッシュをクリアするか、@CachePutを使ってキャッシュを更新するまで、そのキャッシュの値が使用されます(cache-ehcache、cache-redisといった連携プラグインで独自のクリアの仕組みがある可能性はある)。

@CacheEvict

@CacheEvictはキャッシュをクリアする仕組みです。以下のように使います。

class BookService {
    ...
 
    @CacheEvict('books')
    void clear(id) {
    }

    @CacheEvict(value = 'books', allEntries = true)
    void clearAll() {
    }

}

一つ目のメソッドclear(id)を呼び出すと'books'キャッシュのkeyがidのキャッシュをクリアします。keyの指定は先程と一緒で、上記は以下のように定義しているのと同じです。

    @CacheEvict(value = 'books', key = '#id')
    void clear(id) {
    }

2つ目のメソッドでは@CacheEvictの属性にallEntries = trueを指定しています。これを指定すと特定のkeyのキャッシュ削除ではなく、そのキャッシュすべてを削除します。上記では'books'のキャッシュすべてが削除されます。

テストを書いてみます。

    def "キャッシュ削除の動作確認"() {
        given:
        def book1 = new Book(title: 'title1').save(flush: true)
        def book2 = new Book(title: 'title2').save(flush: true)

        and: "キャッシュには何も保存されていない"
        assert booksCache == [:]

        when: "値を一度取得してキャッシュさせる"
        bookService.get(book1.id)
        bookService.get(book2.id)

        then: "キャッシュにbook1、book2が入っているはず"
        booksCache == [(book1.id): book1, (book2.id): book2]

        when: "book1をキャッシュから削除"
        bookService.clear(book1.id)

        then: "キャッシュからbook1が削除されている"
        booksCache == [(book2.id): book2]
    }

    def "キャッシュ全削除の動作確認"() {
        given:
        def book1 = new Book(title: 'title1').save(flush: true)
        def book2 = new Book(title: 'title2').save(flush: true)

        and: "キャッシュには何も保存されていない"
        assert booksCache == [:]

        when: "値を一度取得してキャッシュさせる"
        bookService.get(book1.id)
        bookService.get(book2.id)

        then: "キャッシュにbook1、book2が入っているはず"
        booksCache == [(book1.id): book1, (book2.id): book2]

        when: "キャッシュを全削除"
        bookService.clearAll()

        then: "キャッシュすべて削除されている"
        booksCache == [:]
    }

@CachePut

最後は@CachePutで、これは@CacheEvictと、@Cacheable組み合わせたような動作で、指定されたkeyに対するキャッシュをクリアし、keyに対して戻り値をキャッシュします。以下のように使用します。

class BookService {
    ....

    @CachePut(value = 'books', key = '#book.id')
    Book put(book) {
        book
    }

}

テストを書いてみます。

    def "キャッシュ更新の動作確認"() {
        given:
        def book = new Book(title: 'dummyTitle').save(flush: true)

        and: "キャッシュには何も保存されていない"
        assert booksCache == [:]

        when:
        def cachedBook = bookService.get(book.id)

        then: "キャッシュ取得したオブジェクトがキャッシュされているはず"
        booksCache == [(book.id): book]
        booksCache[book.id].title == 'dummyTitle'

        when: "キャッシュの値を更新する"
        cachedBook.title = 'updatedTitle'
        bookService.put(cachedBook)

        then: "キャッシュの値が更新されているはず"
        booksCache[book.id].title == 'updatedTitle'
    }

ちなみに対象のkeyのキャッシュが存在しない場合は、単に新たにキャッシュされるだけです。

    def "現在キャッシュされていない状態でのキャッシュ更新"() {
        given:
        def book = new Book(title: 'dummyTitle').save(flush: true)

        and: "キャッシュには何も保存されていない"
        assert booksCache == [:]

        when: "キャッシュがない状態でPUTする"
        bookService.put(book)

        then: "キャッシュされている"
        booksCache == [(book.id): book]
    }

condition属性で条件を指定する

@Cacheable、@CacheEvict、@CachePutのアノテーションはすべてcondition属性を持っています。名前から推測できるように、@Cacheable、@CacheEvict、@CachePutが動作する条件を指定できます。これはkeyと同様にSpELが使用できます。以下のように使います。

class Author {
    String name
}
class AuthorService {

    @Cacheable(value = 'authors', condition = "#name.length() > 5")
    def get(name) {
        Author.findByName(name)
    }

}

この例では仮引数のnameの長さが5文字以上の場合キャッシュします。
テストを書いてみます。

class AuthorServiceSpec extends IntegrationSpec {

    def authorService
    def grailsCacheManager

    def cleanup() {
        grailsCacheManager.destroyCache('authors')
    }

    def "conditionでキャッシュ条件を指定する"() {
        given:
        def author1 = new Author(name: "4444").save(flush: true)
        def author2 = new Author(name: "55555").save(flush: true)
        def author3 = new Author(name: "666666").save(flush: true)
        def author4 = new Author(name: "7777777").save(flush: true)

        when:
        authorService.get(author1.name)
        authorService.get(author2.name)
        authorService.get(author3.name)
        authorService.get(author4.name)

        then: "名前が5文字以上のAuthorのみキャッシュされている"
        authorsCache == [(author3.name): author3, (author4.name): author4]
    }

    private getAuthorsCache() {
        grailsCacheManager.getCache('authors').nativeCache
    }

}

アノテーションのvalueの型はStringではなくString[]である

これは何を意味するかというという、複数にキャッシュをすると複数にキャッシュに対して操作ができます。例えば複数キャシュを同時に削除するには以下のようにします。

class AuthorService {
    ....
    @CacheEvict(value = ['authors', 'books'], allEntries = true)
    def clearAll() {}

}

'authors'のキャッシュと、'books'のキャッシュが同時に消えます。テストを書いてみます。

    def "複数のキャッシュを同時に削除する"() {
        given: "authorのキャッシュを準備"
        def author = new Author(name: "xxxxxx").save(flush: true)
        authorService.get(author.name)

        and: "authorがキャッシュされていること"
        assert authorsCache == [(author.name): author]

        and: "bookのキャッシュを準備"
        def book = new Book(title: 'yyyy').save(flush: true)
        bookService.get(book.id)

        and: "bookがキャッシュされていること"
        assert booksCache == [(book.id): book]

        when: "authorsとbooksのキャッシュを同時に削除"
        authorService.clearAll()

        then: "両方のキャッシュが削除されていること"
        authorsCache == [:]
        booksCache == [:]
    }

ではでは、長くなりましたがおわり。

GrailsでJSONの独自Marshallerを登録する

元ネタ
http://compiledammit.com/2012/08/16/custom-json-marshalling-in-grails-done-right/

有名な話なのかもしれませんが、独自Marshaller登録できるんですね。知らなかった。

GrailsではJSONレスポンス書き出し方に代表的な2つのやり方が存在します。
http://grails.org/doc/latest/guide/single.html#xmlAndJSON

  • renderメソッド使う方法
  • 自動Marshallingする方法

以下の様なドメインがあったとして

class Author {

    String name
    String email

    static hasMany = [books: Book]
}
class Book {

    String title

    static belongsTo = Author

}

これを後者の方はこんなかんじに書けます。

render Author.list() as JSON

実際に出力されるJSONは以下のようになります。

[
  {
    "class":"org.yamkazu.Author",
    "id":1,
    "books":[{"class":"Book","id":2},{"class":"Book","id":1}],
    "name":"test1",
    "email":"test1@example.com"
  },{
    "class":"org.yamkazu.Author",
    "id":2,
    "books":[],"name":"test2",
    "email":"test2@example.com"
  }
]

as JSONを使うと手軽な分、classとか必要ない情報が含まれてしまったり、例えばemailとかユーザは見せてはいけないのに、ドメインに含まれているため出力されてしまうといったように、気軽にas JSONすると良からぬ事態を引き起こしかねません。また関連ドメインのプロパティはidしか情報が出力されないといった制約もあります。

そこで独自のMarshallerです。grails.converters.JSON#registerObjectMarshallerのメソッドで登録できます。
f:id:yamkazu:20120831231539p:plain

一番手軽なのはクラスと、Closureを引数に取るやつです。こいつをBootstrapでも登録してもいいし、最初に紹介したURLのように独自のSpringBeanで登録させても良いです。

今回はemailを外部に暴露したくなかったのと、classは不要なので除去、あと関連先のbookのtitleまで含みたいので以下の様に登録してみました。

JSON.registerObjectMarshaller(Author) { Author author ->
    return [
        id: author.id,
        name: author.name,
        books: author.books.collect { Book book ->
            [
                id: book.id,
                title: book.title
            ]
        }
    ]
}

これで出力されるJSONは以下のようになります。

[
  {
    "id":1,
    "name":"test1",
    "books":[{"id":2,"title":"title1"},{"id":1,"title":"title2"}]
  },{
    "id":2,
    "name":"test2",
    "books":[]
  }
]

複数箇所で該当ドメインクラスのJSONレンダリングを必要とするといった場合や、絶対に外に漏れていけないプロパティがある場合に有効な手段じゃないでしょうか!

Mountain Lionで~/.MacOSX/environment.plistがきかない

http://d.hatena.ne.jp/y_sumida/20120805/1344134360
をみてて~/.MacOSX/environment.plistでグローバルに設定できたよなと思いつつ、ローカルの環境を確認してみるとうまく動いていない...

Mountain Lion以前までは以下の方法で回避していたのですが
https://blogs.oracle.com/katakai/entry/netbeans_and_java_for_mac2
どうもMountain Lionからは~/.MacOSX/environment.plistを広なくなった模様(前からサポート停止されてた?)。いろいろぐぐってみると/etc/launchd.confに書けとのこと。

デフォルトではファイルすら存在しないので、ファイルを作ってOS再起動。

setenv JAVA_HOME /Library/Java/JavaVirtualMachines/1.7.0.jdk/Contents/Home
setenv _JAVA_OPTIONS -Dfile.encoding=UTF-8

すると

$ groovy -version
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
Groovy Version: 2.0.1 JVM: 1.7.0_05 Vendor: Oracle Corporation OS: Mac OS X

_JAVA_OPTIONS拾ってくれました!Intellij IDEA上で日本語テスト名もこれで大丈夫でした。

2013年5月13日追記

http://piyopiyoducky.net/blog/2013/04/13/java-system-properties-setting-and-character-encoding/

によるとJava7からLANGの設定を拾ってくれるため/etc/launchd.confに以下のように書けば良いとのこと。

setenv LANG ja_JP.UTF-8

GrailsでドメインクラスにMapのプロパティを定義するとどうのようなスキーマになるか

class MapHolder {

    Map mapValues

}

schema-exportすると以下になりました。

create table map_holder (
    id bigint generated by default as identity,
    version bigint not null,
    primary key (id)
);

create table map_holder_map_values (
    map_values bigint,
    map_values_idx varchar(255),
    map_values_elt varchar(255) not null
);

デフォルトだとMapの型がString,Stringの値を管理できる模様。

Grailsでone-to-manyの関連をラッパークラス型で定義するとどの様なスキーマになるか

class WrapperValueHolder {

    static hasMany = [stringValues: String, integerValues: Integer, booleanValues: Boolean]

}

schema-exportしてみる。

create table wrapper_value_holder (
    id bigint generated by default as identity,
    version bigint not null,
    primary key (id)
);

create table wrapper_value_holder_boolean_values (
    wrapper_value_holder_id bigint,
    boolean_values_boolean boolean
);

create table wrapper_value_holder_integer_values (
    wrapper_value_holder_id bigint,
    integer_values_integer integer
);

create table wrapper_value_holder_string_values (
    wrapper_value_holder_id bigint,
    string_values_string varchar(255)
);

普通に使えました。

※2012/7/23 修正
プリミティブ型と書いてましたが、プリミティブ型ではないので文言を修正しました