uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

enumに継承を! traitとenumの妙な関係、もしくはGrailsのドメインクラスの選択フィールドを国際化表示するのにtraitが便利

Grailsドメインクラスにおいて、いくつかの候補の数値のいずれか、というフィールドを作成し、scaffoldで生成した画面からCRUD操作したいとします。

簡単なのは、こうですね。

class Domain {
  Integer something

  static constraints = {
     something inList:[1,2,3]
  }
}

しかし数値フィールドに量としてではなく個々の値にそれぞれの意味がある場合、たとえば、腹の状態を表すフィールドの値として、

  • 1: はらぺこ
  • 2:まんぷく
  • 3:こばらがへった

を表現するようなものだったとします。このとき、すくなくとも、Scaffoldの画面上では数値ではなく意味のわかる文字列で表示したり選択入力させたいわけです。しかしデータそのものを文字列にするのもDB上の表現をそうしたくないのでいやだとします*1
ということでenumの出番です。

class Domain {
  enum OnakaStatus {
     STARVATION(1),
     SATICEFIED(2),
     SOSO(3)
     private final int id;
     private OnakaStatus(int id) {
         this.id = id;
     }
     public int getId() {
        return id;
      }
  }
  OnakaStatus onakaStatus
  static constraints = {
//     onakaStatus inList:[OnakaStatus.STARVATION,OnakaStatus.SATICEFIED,OnakaStatus.SOSO]
// よく考えるとこんなconstraintはいらない。enumなんだから。
  }
}

"STARVATION","SATICEFIED","SOSO"を選択肢とするセレクト/オプションタグをscaffoldが生成し、FORMパラメータとしてはその文字列、DB中には対応する数値(1,2,3)が保存されます。ナイス! GORMナイス!
でもこれだと表示が日本語にならないし。"STERVATION"の代りに"はらぺこ"というenumの識別子を使えば表示が日本語にはなるが、FORMパラメータとしてその日本語文字列が使われるのは好きくない。ではこうしてみてはどうか。

class Domain {
  enum OnakaStatus {

     STARVATION(1, "はらぺこ"),
     SATICEFIED(2,"まんぷく"),
     SOSO(3,"こばらがへった")
     private final int id;
     private final String value
     private OnakaStatus(int id, String value) {
         this.id = id;
         this.value = value;
     }
     public int getId() {
        return id;
      }
     String toString() {
         return value
     }
  }
  OnakaStatus onakaStatus
}

一応これで識別子は英字のまま表示を日本語にできます。しかしながら、国際化対応ができません。
ぐぬぬ
そういう場合は、enumにorg.springframework.context.MessageSourceResolvableを実装させて、以下のメソッドを定義し、

   enum OnakaStatus implements org.springframework.context.MessageSourceResolvable {
     :
    public Object[] getArguments() { [] as Object[] }
    public String[] getCodes() { [ name() ] }
    public String getDefaultMessage() { "?-" + name() }

   }

この上でi18nメッセージをenumの要素をキーとして用意します。たとえば、grails-app/i18n/なんとか.propertiesに、

STARVATION=はらぺこ
SATICEFIED=まんぷく
SOSO=こばらが減った

でも、enumごとにメソッド追加するの〜! えー冗長。やだーやだー。
ぐぬぬぬ。
共通するメソッドを親クラスに切り出したいところですが、enumはクラスからの継承(extends)はJava/Groovyの言語仕様上許されておりません。interfaceからのimplementsなら可能なのだが。はっ!!
ということでtraitを使えばいいのです。

trait I18nEnum implements org.springframework.context.MessageSourceResolvable {
    public Object[] getArguments() { [] as Object[] }
    public String[] getCodes() { [ name() ] }
    public String getDefaultMessage() { "?-" + name() }
}

こういうtraitを用意して、enumはこうして

    enum OnakaStatus implements I18nEnum {
        STARVATION(1),
        SATICEFIED(2),
        SOSO(3)
        private int id;
        private OnakaStatus(int id) {
            this.id = id;
        }
        public int getId() {
            return id;
        }
    }

あらすっきり*2

enum継承できないのでメソッドを共有させることが本来はできないわけですが、implementsはでき、試したところtraitからもできた。なのでenumメソッドを共有化できました。めでたい。Groovy便利。

Java8のデフォルトメソッドでどうかは不明。

*1:文字列にした上で、フィールドはtransientにしておいて、beforeSaveとかのタイミングで値を適切にさしかえるという手はある。でも国際化の問題はある。

*2:もっとすっきりさせようと、idとgetIdをtraitに移動させてみたり、@InheritConstructorなども試してみたがうまくいかなかった。

XML形式のlog4j設定(log4j.xml)をGrailsで使用する

Grailsではロギングの設定がConfig.groovyで設定できますが、なんらかの理由でXML形式の設定(log4j.xml)で行ないたいことがあるかもしれません。しかし、その場合log4j.xmlをどこに置くか、という問題があり得ます。

この問題を説明します。まずGrailslog4jXML設定を使用するには、springの設定ファイルresouces.groovyで、以下のようにlog4jConfigurerを設定します。

GRAILS_PROJ/grails-app/conf/spring/resources.groovy

beans = {
    log4jConfigurer(org.springframework.beans.factory.config.MethodInvokingFactoryBean){
        targetClass = "org.springframework.util.Log4jConfigurer"
        targetMethod = "initLogging"
        // arguments = "grails-app/conf/log4j.xml"  // ★1
        arguments = "classpath:log4j.xml"  // ★2
    }
}

問題は、★1のようにカレントディレクトリ相対で指定すると、warにしてtomcatなどにデプロイした状態では、tomcatが走行しているときのカレントディレクトリ($CATARINA_BASE)からの相対になり、読み取ることができないということです。だからといって、絶対指定も一般にはできないでしょう。

この解決策ですが、まずlog4j.xmlgrails-app/conf配下に置いた上で、(★2)のようにxmlclasspath配下にあると指定します。run-appの際には、grails-app/confはクラスパスに入っていますのでこれで動作します。

ただし、warにしたときには、conf配下は特にはクラスパスにはいっているわけではないので(Config.groovyなどはコンパイルされてclasses配下に移動している)、warの作成時にgrails-app/conf/log4j.xmlを適切なclasspathの通っているところ、たとえば以下ではWAR中の$WEB-INF/classes/配下にコピーするように、BuildConfigに以下の設定を追加します。

GRAILS_PROJ/grails-app/conf/BuildConfig.groovyに追加。

grails.war.resources = { stagingDir, args ->
    println "$stagingDir, $args"
    copy(file: "grails-app/conf/log4j.xml",
         todir: "${stagingDir}/WEB-INF/classes/")
}

なおログファイルをそもそもどこに置くか、という設定はlog4j.xmlに記入しておく必要があります。XML設定の場合、grailsのConfig.groovyで設定するように、developmentのときとprod/testのときに場所を切り替えたりということはたぶんできないので、適当に絶対パスで指定することになるでしょう。

ちなみに、Wiki - Log4j XML Pluginというプラグインもあり、XMLでの設定に対応する、markupbuilderっぽい記法で設定することができるようです。これを使えば、もっとうまくいくのかもしれませんが試してません。

(周知) 予告: JGGUG大忘年会LT大会と、LondonのG*なカンファレンス行ってきた報告!+合宿の報告もあるよ! #jggug

今週末は、JGGUG忘年会です。あしたビアバッシュのピザなどを予約しますんで、参加希望のかたは今日中にぜひどうぞ。

JGGUG大忘年会LT大会と、LondonのG*なカンファレンス行ってきた報告!+合宿の報告もあるよ! - 日本Grails/Groovyユーザーグループ | Doorkeeper
建物とフロアは同じですが、会議室が通常と違いますので注意を。

LTネタ考え中。

Grailsで、tomcatを起動するというだけの目的でファンクショナルテストを設定する方法

Grailsでコントローラの試験を、モックを使わず実際の通信を使って試験したいケースがある。たとえば、Rest APIを開発する場合、Rest APIはブラウザを使用しなくても簡単に呼び出せるものなので、ユニットテストがしたいです。もちろんGrailsにはコントローラをモックで試験する機能があるが、rest APIを呼び出せば済むものを、偽物ですます必然性があまりないと個人的には思う(もちろん試験実施時のtomcat立ち上げ下げのための実行時間を短縮したい、などの関心はありうるが)。

このためにはファンクショナルテストを実行する必要がある(unit/integrationではtomcatはたちあがらない設定になっている)。しかし、Grailsでは、ファンクショナルテストは単にtest/functional配下にテストコードを置くだけでは実行できず、一つなんでもいいからファンクショナルテスト用プラグインを導入しておく必要がある。

Gebの機能は特に使いたくないとしても、たとえばHttpClientなどでRESTの試験だけしたいという場合に、以下の手順でGebを形だけでも導入することで、ファンクショナルテストを行うことができるようになる。

(1) BuildConfig.groovyのdependenciesセクションに以下を追加

      dependencies {
               :
          test "org.gebish:geb-spock:0.10.0"
      }

(2) BuildConfig.groovyのpluginsセクションに以下を追加

      plugins {
                :
        test ":geb:0.10.0"
      }

(3) BuildConfig.groovyでfolkedモードをfalseに設定する

     grails.project.fork = [
          :
         test: false, // ←
         run: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256, forkReserve:false],
         war: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256, forkReserve:false],
         console: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256]
     ]

(4) テストコードをGRAILS_PROJECT/test/functional配下に作成。

ここでのテストコードは、Gebの機能を使う必要はなく、HttpClientなどを使用した通常のSpockコードで良い(もちろんGeb使っても良い)。

(5) テスト実行

     $ grails test-app functional:

上記はbook of gebより。

Grails/Groovyでのカバレッジ取得に関してのTIPS

プリミティブ最適化を抑制することでブランチカバレッジをましなものに

Grailsでは、コードカバレッジは以下で取得できる。

しかし、上記を使用した場合、分岐網羅(ブランチカバレッジ)は多くの場合、期待する値が取得できない。この理由の1つは、Groovy 1.8以降で導入されたプリミティブ最適化によって、型がプリミティブかどうかによっての条件分岐を行うコードがGroovyのコード生成器によってバイトコード上生成されているためである。Test Code Coverage Pluginはバイトコードレベルでカバレッジ情報を収集するので、ソース上に表われない暗黙の分岐をカバレッジ率の分母に計上してしまう。そしてその値は一般には100%にすることが困難である。

本来Groovyにおいて、プリミティブ最適化を抑止するためのコマンドラインオプション「--disableopt int」が存在する。しかし、GrailsではGroovyをコマンドラインから起動するわけではなくオプションが指定できない。

この問題の対処の一つは、この記事のリンク先にある、「renataogarcia/disableOptimizationsTransformation · GitHub」のコンパイル結果jarである
DisableOptimizationsTransformation-0.1-SNAPSHOT.jar」をダウンロードし、クラスパスに通すことである。こうすればGrails上でのGroovyの最適化が抑制されるため、ブランチカバレッジが正確に測定できる可能性がたかまる(ただし、ブランチカバレッジが期待する値にならない理由はこれだけが原因とは限らないことに注意*1 )。
なお上記jarをクラスパスに通す方法として以下が考えられる。

  1. 環境変数CLASSPATHに設定
  2. GRAILS_PROJECT/libs配下に置く
  3. jarをpom化してローカル/どこまのmaven repoに置き、testディペンデンシーに記述する

どの方法でも良いが、「試験では最適化抑制をし、プロダクトコードでは最適化する」ということはリスクになるため、テスト時に常に抑制するのは怖い気がする(プロダクトコードでも最適化抑制するなら別だが)。なので、テストのとき常に、ではなく、カバレッジ判定のときだけ一時的な指定をするという意味で1でも良いかもしれない。

なお、上記の設定前後でgrails cleanを実行した方がよい。

*1:具体的にはコントローラのためのAST変換などの適用結果として、分岐文が追加されているときがある。しかし、本ノウハウを適用してノイズを除去した状態にすれば、「ここには分岐はないはず」「ここにラインカバレッジ上は到達しているのに、ここに来てないはずはない」といった類推によって、ブランチカバレッジを目視確認で把握可能である場合も多くなる。経験上。

Dockerを使ってGrails開発

Grails開発でdockerを使用するためのDockerfileを、備忘録兼ねて晒します。開発中に使うものです。運用用は、別のものになるでしょうかね。
onesysadmin/docker-grailsを元にして、Proxy設定を行い、いくつかの工夫をしております(プロキシ設定については、Dockerコンテナに透過的プロキシ設定とかをすれば不要なのかも)。
https://blog.linode.com/wp-content/uploads/2014/01/docker.pnghttp://www.simplicityitself.com/wp-content/uploads/2013/04/grails.png

利点

Grails開発でDockerを使う利点は以下の通り。

  • 準備が簡単。Grailsのインストールはもとより、gvmやJDKのインストールすらいらない。grails wrapperなどもいらないわけだ。
  • 環境を汚さない。JDKのインストールをしなくてすむ、Postgresなどをコンテナに封入して連携する、などによる。
  • 設定含めた開発時実行環境を開発メンバー間でシェアできる
  • Dockerはlinuxなので、開発環境windows、実行環境linuxによる差異をなくせる。windowsの場合、boot2dockerなどを使って仮想マシン上でlinuxを動かすことになる(MacOSXではDocker 1.3のboot2dockerでホストのフォルダの共有とVOLUME対応(-v指定による共有)がされているそう]だが、まだ試していない。)。

Dockerfileの準備

まず、onesysadmin/docker-grailsをcloneするなりダウンロードするなりして、使用したいGrailsのバージョンのDockerfileを、Grailsプロジェクトのトップディレクトリにコピーします。

Dockerfileの修正

以下に修正点を示します。修正したDockerfile全体はこちらのgistに

Proxyの設定

開発環境がファイアウォール背後でproxyの設定が必要になる場合は以下のように設定します。今回の場合、プロキシは以下において必要になります。

ENV PROXY_HOST 10.2.3.4
ENV PROXY_PORT 18080
ENV http_proxy http://${PROXY_HOST}:${PROXY_PORT}/
ENV https_proxy http://${PROXY_HOST}:${PROXY_PORT}/

ENTRYPOINT, EXPOSEの設定

ENTRYPOINT [ "/usr/local/bin/gvm-exec.sh", "grails", "-Dgrails.work.dir=/app/.grails" ]
EXPOSE 8080

gvm-exec.shや後で出てくるgvm-wrapper.shは「onesysadmin/docker-grails」の親であるイメージ「onesysadmin/gvm」が入れてくれているスクリプトです。

RUN関係

RUN gvm-wrapper.sh install grails 2.4.4 && \
    gvm-wrapper.sh flush archives && \
    cd /app && \
    gvm-exec.sh grails -Dgrails.work.dir=/app/.grails add-proxy client --host="${PROXY_HOST}" --port="${PROXY_PORT}" && \
    gvm-exec.sh grails -Dgrails.work.dir=/app/.grails set-proxy client
RUN mv /root/.grails/ProxySettings.groovy /root/.grails/ProxySettings.groovy.bak && \
    sed -e "s/'http.nonProxyHosts':''/'http.nonProxyHosts':'localhost'/" /root/.grails/ProxySettings.groovy.bak > /root/.grails/ProxySettings.groovy

proxyの設定がポイント。interactiveにしたときでrun-appしてstop-appして再起動したときにportが所有されつづけてしまう問題(address already in useとなる)の回避のため、nonProxyにlocalhostを設定します。

GRAILS_PROJECT/grails-app/conf/BuildConfigの修正

grails.project.dependency.resolutionの「mavenLocal()」を以下のように修正します。

        mavenLocal("/app/.m2")

これで依存jarやプラグインが毎回ダウンロードされるのを防ぐことができます*1。これがポイント。

コンテナのビルド

$ cd /path/to/grails/project
$ docker build -t you/project .

Grailsコンテナ実行(例)

$ docker run -it -p 8080:8080 -v /path/to/grails/project:/app you/project:latest # grailsインタラクティブ起動
$ docker run -d -p 8080:8080 -v /path/to/grails/project:/app you/project:latest run-app # grails run-app実行

ホストの8080ポートにアクセスするとコンテナの8080(Grailsのデフォルトポート)にアクセスできます。

DBコンテナとの連携

必要に応じて。以下はPostgresを前提とします。
Postgresのコンテナは、以下を参考に「orchardup/postgresql/」を使用します。

以降、通常のDB接続の設定(ドライバ追加のためにBuildConfig.groovyのdependenciesに「 runtime 'postgresql:postgresql:9.1-901.jdbc4'」を追加するなど)を行なった上での設定です。

DataSource.groovyの修正

driverClassNameやDB認証などの修正
dataSource {
    pooled = true
    // jmxExport = true
    driverClassName = "org.postgresql.Driver"
    username = System.getenv("DB_ENV_POSTGRESQL_USER")
    password = System.getenv("DB_ENV_POSTGRESQL_PASS")
}
JDBC URLの設定

必要な環境について、URLを設定します。

            url = "jdbc:postgresql://${System.getenv('DB_PORT_5432_TCP_ADDR')}:${System.getenv('DB_PORT_5432_TCP_PORT')}/docker"

「docker」はorchardup/postgresqlが初期的にCREATEしてくれるデータベース名です。別の名前にしたいときは、orchardup/postgresqlの起動時にDB名を指定できるので、それにあわせたものを使えば良いでしょう。

DB連携したGarilsコンテナ実行(例)

$ docker run -d -p 5432:5432 -e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker --name pg orchardup/postgresql:latest
$ docker run -d -p 8080:8080 -v /path/to/grails/project:/app --link pg:db  you/project:latest run-app

おまけ

ENTRYPOINTを指定しているので、bashなどを起動したい場合は、

$ docker run -it -v /path/to/grails/project:/app --link pg:db  --entrypoint "bash" you/project:latest

こうかな。

まとめ

上記は、Grailsに限らず、開発時にdockerを使用する場合のパターンの一つになると思うが、ホストのディレクトリのマッピング(-v)を駆使してソース含めほとんどのファイルを外に置くコンセプトなので、単独実行するのと使用感も制約もあまりかわらない。しかし、このパターンだと、リモート利用、つまりdockerクライアントを実行するマシンとdockerデーモンが動作するマシンが別の場合には利用できない。つまりdockerデーモンを動かしているホストのlinuxマシンにログインできるアカウントがないと利用できない。dockerは単体でNFSや高速rsyncみたいなものを提供しないのが残念である。できればもっと開発用PaaSっぽく使えるのに!(ADDじゃ遅そうだし動的更新ができない)*2

このパターンであれば、Eclipse/GGTSなどをdockerで実行するのもそんなに難しくはない。

*1:なお、grails.project.dependency.resolver = "maven"にしているのにもかかわらず、/root/.grails/ivy-cacheが作成され何かが入っている。理由不明

*2:将来的にはFUSEで対応されるかもしれないんだって。やったー。

PHPのようにGrailsを使う方法

GrailsアプリケーションではGSPのビューは以下の方法で表示できます(他にもあるかも)。

  • コントローラーアクションの結果として表示
    • renderで指定したビュー
    • コントローラ名とアクション名から定まるデフォルトのビュー
  • ビューを直接表示する
    • URLMappingでURLパターンからビューを指定する

しかし、「ビューだけ」の、GSPの集合だけで機能するサイトは作れません。JSPで言うと、JSPだけのServlet無しのWebサイト、あるいはPHPの埋め込みページだけで構成するようなサイトが作れないのです。

そうなのはおそらく意図的であり、それがWeb開発の黎明期にアンチパターンとしてみなされてきたので排除されているのでしょう。上記のような制約「GSPはコントローラからの画面遷移を経て表示される」があることのメリットは、GSP内で使用できるデータの由来を明確化できること、そして、画面遷移のリンク情報をページの中にばらまくのではなく、コントローラのコードで指定することでしょう。

ただ、そうだとしても、以下のような開発手順をとりたいときがあります。

  • 開発時に、まずHTMLでのモックアップがある(デザイナーや企画者が作成するなど)
  • それをGSPに一括変換する
  • 適宜以下を実施する
    • FORMの部分をGSPタグ<g:form>でおきかえる
    • レイアウトをSiteMeshでおきかえる
    • 静的リソースはアセットにして…

現状のGrailsでは、上記の手順にはなりません。ページの持つ機能をコントローラ・アクションの階層に分割し、そしてコントローラアクションの結果としてのページ群に対応させ…、というトップダウンの機能設計をせざるを得ず、直接画面確認に行けないのです。

本記事では、この問題を解決するために、ページ間遷移について個別のコントローラ無しGSPの集合としてアプリを作る、つまり「PHPのようにGrailsを使う」方法を紹介します。

DispatchController

showPageというアクションを1つだけ持った以下のコントローラDispatchControllerを用意します。

package common

class DispatchController {

    /** grails-app/view/pages配下に配置された、コントローラに所属しない
     * (コントローラのアクション名と一致していることを期待せず、リンク
     * をたどることやメニュー選択の結果として表示されることを想定して作
     * 成されている)ページを表示する。
     */

    def showPage() {
        render view:"/pages/${params.pageName}"
    }
   
}

URLMappingに以下を追加します。

        "/pages/$pageName"(controller: "dispatch", action: "showPage")
        "/"{
            controller = "dispatch"
            action = "showPage"
            pageName = "index"
        }


これで準備は終わり。あとは

配下に、たとえば

というGSP(htmlの拡張子をgspにリネーム)を置けば、

      <a href="${request.contextPath}/pages/a">..</a>

というようなリンクで相互にGSPビューを表示できるようになります。views/pages/index.gspがトップページになります。
あもちろん、<g:link>とか使っても良いです。

まとめ

コントローラ・アクションの編成の方法論というのは、Grails初学者にとっては難しいところです。

Scaffoldではドメイン/アクションをベースにしたビューを生成してくれますが、Webサイトというものが一般には「DBテーブルエディタ」ではない以上、この構造は必ずしも一般的ではなく、参考にならないときもあります。

テーブル単位のCRUDに限らない、コントローラ・アクションの階層をどう編成すべきか。この問いに、なんとか解を出さないと、index.gsp以外の1ページを表示することすらままならないかもしれないわけです。デフォルトのindex.gspのようにページごとにURLMapping追加していってもいいんですが、面倒だし不毛ですよね。

多数のページから呼ばれ得る多数のコントローラの関係があるとき、むしろコントローラ/アクションの階層をページ集合とは独立した機能群として設計する方がやりやすいときもあるでしょう。

このPHPぽいアプローチならば、動くサイトを拡張しながら、コントローラを都度考えていくことができます。また、それとは別に、Grailsの初心者にとってのとっつきは、はるかに良くなると思います。

もちろん、この技法と通常のコントローラベースのアプローチを組合せても全く問題ありません。

問題もありえます。この方法だけで、大規模サイトとか業務システムのサイトの全体をつくるべきではないでしょう。単機能の1ページサイトでも不要でしょう。でも、ヘルプページとか、社長のご挨拶ページとか、サイトマップとか、そういうページを含むサイト全体を作るために、この技法を織り交ぜて適用するのが有用なことがあるでしょう。

ということで、選択肢の一つとして心にとめておいておくと便利なのではないかと思い、紹介させてもらいました。