Grails Database Reverse Engineering Plugin - G* Advent Calendar 2012 -

G* Advent Calender 2012の12/6担当、@nobusue です。

どうも風邪を引いたらしく、体調がやばい事になってます。。。ということで、JGGUG合宿2012の成果まとめで勘弁してください。

Grailsは通常、モデルオブジェクトを作成し、そこからRDB(もしくはKVSなど任意のデータストア)にデータを永続化するという手順で利用します。しかしながら、既存のRDBがあり、そのデータを管理するUIをささっと作りたいというお話は割りとよく聞きます。

そんなときに便利なのが、Database Reverse Engineering Pluginです。
http://grails-plugins.github.com/grails-db-reverse-engineer/

ただ、こいつはちょっと癖があるので、使いこなすにはいくつかコツが必要です。ここでは、私が踏んだ地雷を公表しておきたいと思います。

Pluginのインストール

適当な新規Grailsアプリケーションを作成します。ここでは仮にtestとしました。
Grailsは2.1.1、Database Reverse Engineering Pluginは0.4で動作確認しています。
次に、次のコマンドでPluginをインストールします。

nobusue-MacBookPro:test nobusue$ grails install-plugin db-reverse-engineer
Plugin installed.

無事にインストールできました。そう、インストールはね。。。

DBの準備

今回はMySQL5を利用しました。みなさんのお手元では適当なDBを作成してください。(もちろんですがGrailsがサポート対応しているものにしてくださいね!)

データを自分で作るのが面倒だったので、MySQLのサイトで公開されているサンプルデータを利用させていただきました。このデータ、MySQLの記述検定にも使われているそうなので、一度見ておくとよいかもです。
http://dev.mysql.com/doc/index-other.html
今回はDB=worldにworld.sqlを取り込みました。

データソース設定

Grailsのデータソースを設定します。
[grails-app/conf/DataSource.groovy]

dataSource {
   url = 'jdbc:mysql://localhost/world'
   driverClassName = 'com.mysql.jdbc.Driver'
   username = 'root'
   password = 'password'
   dialect = org.hibernate.dialect.MySQL5InnoDBDialect
}

あと、PluginがJDBCドライバを参照できるように、依存関係を追加してやります。
[grails-app/conf/BuildConfig.groovy]

dependencies {
    runtime 'mysql:mysql-connector-java:5.1.20'
}

最後に、Plugin自体の設定を追加します。
[grails-app/conf/Config.groovy]

grails.plugin.reveng.jdbcDriverJarDep ='mysql:mysql-connector-java:5.1.20'

DB Reverse Engineering Plugin実行

さあ、これで準備完了です。DBサーバーが起動していることを確認して、Pluginを実行してみましょう!

nobusue-MacBookPro:test nobusue$ grails db-reverse-engineer --stacktrace
 Compiling 10 source files.| Error Error executing script DbReverseEngineer: : Compilation Failed (NOTE: Stack trace has been filtered. Use --verbose to see entire trace.)
: Compilation Failed
     at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:291)
     at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:106)
     at DbReverseEngineer$_run_closure1.doCall(DbReverseEngineer:51)
Caused by: java.lang.IllegalArgumentException: The includeAntRuntime=false option is not compatible with fork=false
     ... 3 more
 Error Error executing script DbReverseEngineer: : Compilation Failed

ということで、残念ながらPluginの実行が失敗します。どうもモデルオブジェクトの自動生成あたりで引っかかっている雰囲気ですが、細かいことを追求するのはやめておいて、ドキュメントにしたがってGrailsとPluginのバージョンを下げてみます。

改めて環境構築

ドキュメントによると、「Grails2.0+Plugin0.4で動かない場合は、いったんGrails1.3+Plugin0.3でモデルオブジェクトを生成し、そのアプリケーションをGrails2.0の環境にマイグレーションしろ」と書いてありました。もっと分かりやすいとこに書いといてくれよ。。。
ということで、Grails1.3.9をインストールし、再びアプリケーションを作成します。

Reverse Engeering PluginがMaven Centralを参照するのですが、Grails1.3のテンプレートでは無効化されているのでリポジトリを追加します。
[grails-app/conf/BuildConfig.groovy]

repositories {
    mavenLocal()
    mavenCentral()

Pluginのインストール時はバージョン指定をお忘れなく。

grails install-plugin db-reverse-engineer 0.3

grails-app/conf/Config.groovy および grails-app/conf/DataSource.groovy は、Grails2.0のときと同様に修正します。

改めてDB Reverse Engineering Plugin実行

> grails db-reverse-engineer
Running script /Users/nobusue/.grails/1.3.9/projects/test13/plugins/db-reverse-engineer-0.3/scripts/DbReverseEngineer.groovy
Environment set to development
     [copy] Copied 3 empty directories to 1 empty directory under /Users/nobusue/.grails/1.3.9/projects/test13/resources
     [copy] Copying 1 file to /Users/nobusue/.grails/1.3.9/projects/test13/resources
     [copy] Copied 3 empty directories to 2 empty directories under /Users/nobusue/.grails/1.3.9/projects/test13/resources
    [mkdir] Created dir: /Users/nobusue/.grails/1.3.9/projects/test13/plugin-classes
  [groovyc] Compiling 13 source files to /Users/nobusue/.grails/1.3.9/projects/test13/plugin-classes
    [mkdir] Created dir: /Users/nobusue/work/grails/test13/target/classes
  [groovyc] Compiling 7 source files to /Users/nobusue/work/grails/test13/target/classes
    [mkdir] Created dir: /Users/nobusue/.grails/1.3.9/projects/test13/resources/grails-app/i18n
[native2ascii] Converting 13 files from /Users/nobusue/work/grails/test13/grails-app/i18n to /Users/nobusue/.grails/1.3.9/projects/test13/resources/grails-app/i18n
     [copy] Copying 1 file to /Users/nobusue/work/grails/test13/target/classes
     [copy] Copied 2 empty directories to 2 empty directories under /Users/nobusue/.grails/1.3.9/projects/test13/resources
     [echo] Starting database reverse engineering, connecting to 'jdbc:mysql://localhost/world' as 'root' ...
     [echo] Finished database reverse engineering

おおっ、無事に City.groovy, Country.groovy, CountryLanguage.groovy が生成されました。CountryLanguageは複合キーが定義されているため、hashCode()も自動生成されています。すばらしい。

UI作成

モデルオブジェクトさえ手に入ればこっちのもの。試しにscaffoldして動かしてみます。

grails generate-all xxx.City

すると、scaffoldは問題なく終了しますが、生成されたUIを実行するとエラーが出てしまいます。原因はMySQLのテーブル"CountryCode"がフィールド"String countryCode"にマッピングされており、これを改めてGORMに通すとCOUNTRY_CODEをさしてしまうためでした。
対策としては、モデルオブジェクトにmappingを追加するか、フィールド名をcountrycodeのようにcamel caseでないものに修正する必要があります。今回は後者の対応で対応することで、めでたくUIから操作できるようになりました。

次にCountryですが、mapping追加でviewから操作することはできました。

static mapping = {
     id name: "code", generator: "assigned"
     version false
     surfaceArea column: "surfacearea"
     indepYear column: "indepyear"
     lifeExpectancy column: "lifeexpectancy"
     localName column: "localname"
     governmentForm column: "governmentform"
     headOfState column: "headofstate"         
}

しかし、UI上ではリスト表示の際にID列がないため、editのリンクが機能しません。

同じく、CountryLanguageはテーブル名がcamel caseになるので、こちらもmappingを修正して対応します。

static mapping = {
     table "countrylanguage"
     id composite: ["countryCode", "language"]
     version false
     countryCode column: "countrycode"
     isOfficial column: "isOfficial"
}

ここで気づいたのが、idはマッピングされているのに、scaffoldで生成したviewのID列には表示されていないということ。何か対策があるんでしょうが、調べ切れていないのでとりあえず宿題ということにさせてください。
あと、元のテーブルにはリレーションが設定されているのですが、モデルオブジェクトの関連には反映されていないですね。Pluginの実行時に何か設定が必要なのでしょうか?

まとめ

ちょっと癖のあるDB Reverse Engineering Pluginですが、ベースとなっているHibernate Toolsのことを理解していればもうちょっと使いこなせるかもしれないです。素直なテーブルなら使えると思いますので、ぜひお試しください。(そしてレポートを。。。)

追記

2012/12/6現在でReverse Engineering Plugin 0.5がリリースされてました。。。こちらも確認せねば。。。

次は @nobeans さんです!