先月から始まった連載「Grails をマスターする」の第 1 回目では、Grails という名前の新しい Web フレームワークを紹介しました。Grails にはモデル・ビュー・コントローラーによりコンサーン (関心事) を分離するという概念や「Convention over Configuration (設定より規約)」といった最近のプラクティスが採り入れられています。これらのプラクティスを組み込み scaffold 機能と結び付けた Grails では、最低限の機能を持った Web サイトをわずか数分で立ち上げることができます。
今回の記事では Grails によって容易になるもう 1 つの領域として、GORM (Grails Object Relational Mapping) API を使って永続性を実現する方法に焦点を当てます。最初にオブジェクト・リレーショナル・マッパー (ORM) を紹介し、1 対多の関係を作成する方法を説明します。次に、アプリケーションが「ガーベッジ・イン/ガーベッジ・アウト」シンドローム (ゴミのような情報を入力してもゴミのような情報しか得られないということ) に悩まされないようにするためのデータ検証について説明し、続いて Grails の ORM ドメイン固有言語 (DSL) の実例を紹介します。この DSL によって、POGO (Plain Old Groovy Object) を裏で永続化させる方法を微調整できます。そして最後に、いかに簡単にリレーショナル・データベースを交換できるかを実演します。格好の交換対象となるのは、JDBC ドライバーと Hibernate ダイアレクトを使ったあらゆるデータベースです。
リレーショナル・データベースは 1970年代後半から出回っていますが、ソフトウェア開発者は今でもリレーショナル・データベースからデータを効率的に出し入れするのに苦労しています。今日のソフトウェアがベースとしているのは、データベースの使用方法として最も人気のあるリレーショナル理論ではなく、オブジェクト指向の原則だからです。
ORM は、データベースとコードとの間で何度もデータを移し変える苦労を軽減するために急成長したプログラムです。この問題に対処するためによく使われる Java API としては Hibernate、TopLink、JPA (Java Persistence API) の 3 つが挙げられますが、どの 1 つとして完璧ではありません (「参考文献」を参照)。この問題はあまりにも根強いため、問題自体に「Object-Relational Impedance Mismatch (オブジェクトとリレーショナルのインピーダンス・ミスマッチ)」というキャッチフレーズが付いているほどです (「参考文献」を参照)。
GORM は Hibernate を覆うシン Groovy ファサードです (「Gibernate」より「GORM」のほうが言いやすいからだと思います)。つまり、既存の Hibernate の芸当はすべて変わらず有効だということですが (HBM マッピング・ファイルとアノテーションの両方が完全にサポートされるなど)、この記事では GORM がもたらす興味深い機能に焦点を当てます。
POGO をデータベース・テーブルに保存するという問題は軽く見られがちです。実際、1 つの POGO を 1 つのテーブルにマッピングするのであれば事は至って簡単で、POGO プロパティーはテーブルの列にぴったり対応します。しかし、互いに関連する 2 つの POGO を設定するなどしてオブジェクト・モデルを少しでも複雑にすると、途端に事が難航してきます。
その一例として、先月の記事で着手した Trip Planner の Web サイトを見てください。当然のことながら、このアプリケーションで重要な役割を果たすのは Trip という POGO です。テキスト・エディターで grails-app/domain/Trip.groovy (リスト 1 を参照) を開いてください。
リスト 1. Trip クラス
class Trip {
String name
String city
Date startDate
Date endDate
String purpose
String notes
}
|
リスト 1 の属性はそれぞれ簡潔かつ単純に Trip テーブルの対応するフィールドにマッピングされます。前回の記事で説明したように、Grails が起動すると grails-app/domain ディレクトリーに保存されたすべての POGO に、対応するテーブルが自動的に作成されます。Grails がデフォルトで使用するのは組み込みの HSQLDB データベースですが、この記事を読み終えるまでには、自分で選んだリレーショナル・デーベースを使えるようになっているはずです。
旅行には飛行機が付き物なので、道理として Airline クラス (リスト 2 を参照) も作成します。
リスト 2. Airline クラス
class Airline {
String name
String url
String frequentFlyer
String notes
}
|
ここで、この 2 つのクラスをリンクさせなければなりません。Xyz 航空を利用したシカゴへの旅行をスケジュールするには、Java コードでの場合と同じように Groovy でそれを表現します。つまり、Trip クラスに Airline プロパティーを追加するということです (リスト 3 を参照)。この手法は、Object Composition (オブジェクトの合成) と呼ばれます (「参考文献」を参照)。
リスト 3. Trip クラスへの Airline プロパティーの追加
class Trip {
String name
String city
...
Airline airline
}
|
ソフトウェア・モデルとしてはこれで十分ですが、リレーショナル・データベースでは多少異なる手法を採ります。テーブル内のすべてのレコードには主キーと呼ばれる一意に決まる ID があるので、Trip テーブルに airline_id フィールドを追加すればレコード同士をリンクできるようになります (この例では、「Xyz 航空」のレコードを「シカゴ旅行」のレコードにリンクします)。これは 1 対多の関係と呼ばれます。ただし、1 つの航空会社には複数の旅行が関連付けられるはずです (1 対 1 および多対多の関係の例は、Grails のオンライン・マニュアルに記載されています。「参考文献」を参照してください。)。
このデータベース・スキーマ案には 1 つだけ問題があります。データベースの標準化 (「参考文献」を参照) には成功したかもしれませんが、テーブル内の列がソフトウェア・モデルと一致しなくなってしまったからです。そこで Airline フィールドを AirlineId フィールドに置き換えたとすると、実装の詳細 (データベースで POGO を永続化しているということ) がオブジェクト・モデルに漏れてしまうことになります。Joel Spolsky は著書のなかで、これを Law of Leaky Abstractions (抽象化の破綻の法則) と呼んでいます (「参考文献」を参照)。
GORM では、オブジェクト・モデルを Groovy でつじつまが合うように表現できるようにすることによって、この抽象化の破綻という問題を軽減しています。GORM は裏でリレーショナル・データベースの問題に対処してくれますが、この後すぐに説明するように、デフォルト設定は簡単に上書きすることができます。GORM はデータベースの詳細を隠す不透明な抽象化層ではなく、半透明の階層として、型にとらわれずに正しいことを行おうとします。その一方で、その振る舞いをカスタマイズする必要がある場合にはそれを邪魔することはしません。そのため、オブジェクト・モデルとリレーショナル・モデル、双方の長所を生かすことができます。
Trip POGO には既に Airline プロパティーを追加しているので、1 対多の関係を完成するために今度は Airline POGO に hasMany 設定を追加します (リスト 4 を参照)。
リスト 4. Airline で作成する 1 対多の関係
class Airline {
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
}
|
静的 hasMany 設定とは Groovy ハッシュマップのことで、キーは trip、値は Trip クラスとなります。Airline クラスに他にも 1 対多の関係を設定する必要がある場合は、キーと値のペアをコンマで区切ったリストを大括弧のなかに入れてください。
ここで、新しい 1 対多の関係が実際にどのように機能するかを調べため、grails-app/controllers に簡単な AirlineController クラスを作成します (リスト 5 を参照)。
リスト 5. AirlineController クラス
class AirlineController {
def scaffold = Airline
}
|
前回の記事を思い出してください。def scaffold は実行時に、Grails に対して基本メソッドである list()、save()、edit() を作成するよう動的に指示します。さらに、GSP (GroovyServer Page) ビューも作成するよう Grails に動的に指示を与えます。したがって、必ず TripController と AirlineController の両方に def scaffold が含まれるようにしてください。また、grails generate-all と入力して作成した GSP 成果物 (trip または airline ディレクトリー) が grails-app/views に 1 つでも残っている場合は、その成果物を削除してください。この例では、Grails がコントローラーとビュー両方の scaffold を動的に作成することが必須だからです。
ドメイン・クラスとコントローラーはこれで用意できたので、ここで Grails を起動します。grails prod run-app と入力してアプリケーションを実動モードで実行してください。万事順調に行けば、以下のメッセージが表示されます。
Server running. Browse to http://localhost:8080/trip-planner |
ブラウザーには、AirlineController と TripController 両方のためのリンクが表示されているはずです。
AirlineController
をクリックして、Xyz Airlines の詳細を入力します (図 1 を参照)。
図 1. 1 対多の関係: 「1」の側
アルファベット順に表示されるフィールドが好みでないとしても気にしないでください。この設定は次のセクションで上書きするからです。
今度は新しい旅行を作成します (図 2 を参照)。Airline のコンボ・ボックスに注目してください。ここには Airline テーブルに追加するすべてのレコードが表示されます。主キーの「漏えい」についてはご心配なく。次のセクションで、よりわかりやすいラベルを追加する方法を説明します。
図 2. 1 対多の関係: 「多」の側
これまでの説明で、Airline POGO (静的 hasMany) にヒントを追加すると、裏でテーブルが作成される仕組みにも、フロントエンドで生成されるビューにも影響することがわかったはずです。ドメイン・オブジェクトを修飾するこの Naked Objects パターン (「参考文献」を参照) は、Grails 全体で広範に使用されます。この情報を直接 POGOに追加することで、外部 XML 構成ファイルが不要になります。すべての情報を一箇所に集めることは、生産性にとって何よりのメリットです。
例えば、Airline クラスに toString メソッドを追加するだけで、コンボ・ボックスには主キーが漏えいによって表示されることがなくなります (リスト 6 を参照)。
リスト 6. Airline への toString メソッドの追加
class Airline {
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
|
これで、今後は航空会社の名前がコンボ・ボックスに表示されるようになります。しかし、ここで何よりも感心させられる点は、Grails がまだ実行中であれば Airline.groovy を保存するだけで変更内容が反映されるということです。ブラウザーで新しい Trip を作成して実際に確認してみてください。ビューは動的に生成されるため、サーバーをリブートする必要なく、正しいビューが表示されるまでテキスト・エディターとブラウザーの間を素早く行ったり来たりできます。
次はアルファベット順に並んだフィールドの順序付けを修正します。それには、static constraints ブロックという別の構成を POGO に追加します。このブロックに、リスト 7 に記載した順でフィールドを追加してください (これらの制約は、テーブル内の列の順序には影響しません。影響されるのはビューだけです)。
リスト 7. Airline でのフィールド順序の変更
class Airline {
static constraints = {
name()
url()
frequentFlyer()
notes()
}
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
|
上記の変更を Airline.groovy ファイルに保存してからブラウザーで新しい航空会社を作成すると、フィールドはリスト 7 で指定した順序で表示されます (図 3 を参照)。
図 3. カスタマイズしたフィールド順序
POGO でフィールド名を 2 回も入力させられるのでは DRY (Don't Repeat Yourself) の原則 (「参考文献」を参照) に違反しているじゃないか、と責めるのはちょっと待ってください。フィールド名を別のブロックに抽出しているのには正当な理由があるからです。リスト 7 の static constraints ブロックに含まれる括弧は、そのうち空ではなくなります。
static constraints ブロックはフィールドの順序を指定するだけでなく、検証ルールを設定するためにも使えます。例えばこのブロックで、String フィールドに長さの制限を設けたり (デフォルトは 255 文字)、String の値を特定のパターン (E メール・アドレスや URL) に確実に一致させることができます。さらに、フィールドをオプションあるいは必須にすることもできます。有効な検証ルールの完全な一覧については、Grails のオンライン・マニュアル (「参考文献」にリンクを記載) を参照してください。
リスト 8 に、この制約ブロックに検証ルールを追加した Airline クラスを示します。
リスト 8. Airline へのデータ検証の追加
class Airline {
static constraints = {
name(blank:false, maxSize:100)
url(url:true)
frequentFlyer(blank:true)
notes(maxSize:1500)
}
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
|
変更した Airline.groovy ファイルを保存して、ブラウザーで新しい航空会社を作成してください。検証ルールに違反している場合には警告メッセージが表示されます (図 4 を参照)。
図 4. 検証による警告
警報メッセージは、grails-app/i18n ディレクトリーにある messages.properties ファイルで変更することができます。デフォルト・メッセージは複数の言語でローカライズされていることに注意してください (クラス別、フィールド別のカスタム・メッセージを作成する方法については、Grails オンライン・マニュアルの検証に関するセクションを参照してください)。
リスト 8 に記載された制約のほとんどはビュー層にしか影響しませんが、パーシスタンス層に影響する制約もいくつかあります。例えば、データベース内の name 列には 100 文字の長さ制限が設けられています。notes フィールドは、(255 文字を超えるフィールドの場合) ビュー内で入力フィールドからテキスト域に変わるだけでなく、VARCHAR 列から TEXT 列、CLOB列、または BLOB 列にも変わるようになっています。このような制約を左右するのは、裏で使用しているデータベースの種類とその Hibernate ダイアレクト次第です。つまり、もう当たり前のように聞こえるかもしれませんが、上書きすることができるのです。
Hibernate のデフォルトを上書きするには、HBM マッピング・ファイルまたはアノテーションという便利な構成方法を使用できますが、Grails では Naked Object 様式の 3 つ目の方法を用意しています。それは、static mapping ブロックを POGO に追加してデフォルトのテーブル名とフィールド名を上書きするという方法です (リスト 9 を参照)。
リスト 9. GORM DSL の使用
class Airline {
static mapping = {
table 'some_other_table_name'
columns {
name column:'airline_name'
url column:'link'
frequentFlyer column:'ff_id'
}
}
static constraints = {
name(blank:false, maxSize:100)
url(url:true)
frequentFlyer(blank:true)
notes(maxSize:1500)
}
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
|
このようなマッピング・ブロックが特に役立つのは、既存のレガシー・テーブルを新しい Grails アプリケーションで使用する場合です。ここでは基本的なことにしか触れませんが、ORM DSL を使用すると、テーブル名とフィールド名の単純な再マップだけでなく遥かに多くのことを行えます。その例として、列ごとにデフォルト・データ型を上書きしたり、主キーを作成する方法を調整したり、さらには複合主キーを指定したりすることまで可能です。他にも Hibernate キャッシュ設定を変更したり、外部キーの関連用に使用するフィールドを調整するなど、さまざまなことを行えます。
重要な点は、これらの設定はすべて POGO という 1 つの場所に集約されるということです。
これまでに行ったすべての作業は、個々のクラスをカスタマイズすることに重点が置かれていました。ここで、一歩離れて全体的な変更を加えることにします。すべてのドメイン・クラスで共有されるデータベース固有の構成は、grails-app/conf/DataSource.groovy という 1 つの共有ファイル (リスト 10 を参照) に保存されます。このファイルをテキスト・エディターで開き、ファイル全体に目を通してください。
リスト 10. DataSource.groovy
dataSource {
pooled = false
driverClassName = "org.hsqldb.jdbcDriver"
username = "sa"
password = ""
}
hibernate {
cache.use_second_level_cache=true
cache.use_query_cache=true
cache.provider_class='org.hibernate.cache.EhCacheProvider'
}
// environment specific settings
environments {
development {
dataSource {
dbCreate = "create-drop" // one of 'create', 'create-drop','update'
url = "jdbc:hsqldb:mem:devDB"
}
}
test {
dataSource {
dbCreate = "update"
url = "jdbc:hsqldb:mem:testDb"
}
}
production {
dataSource {
dbCreate = "update"
url = "jdbc:hsqldb:file:prodDb;shutdown=true"
}
}
} |
dataSource ブロックでは、データベースに接続するために使用する driverClassName、username、および password を変更することができます。hibernate ブロックではキャッシュ設定を調整できます (Hibernate のエキスパートでない限り、ここでの調整は必要ありません)。しかしまさに興味深いことが起こるのは、environments ブロックです。
前回の記事で説明したように、Grails の実行モードには開発 (development)、テスト (test)、実動 (production) の 3 つがあります。grails prod run-app と入力すると、Grails は production ブロックに含まれるデータベース設定を使用します。username と password を環境別に調整したいのであれば、ただ単に dataSource ブロックの設定を個々の environment ブロックにコピーして、これらの設定の値を変更すればいいだけの話です。すると、environment ブロックの設定が dataSource ブロックの設定を上書きします。
url 設定は JDBC 接続ストリングです。実動モードでは、HSQLDB はファイル・ベースのデータ・ストアを使用します。一方、開発およびテスト・モードでは HSQLDB はメモリー内のデータストアを使用します。前回、サーバーが再起動しても Trip レコードを維持したいのであれば実動モードで実行するように言いましたが、これで、開発モードとテスト・モードで同様のことを行う方法もわかったはずです。つまり、production ブロックの url 設定をコピーすればいいだけです。もちろん、Grails に DB2 や MySQL、あるいはその他の従来からのファイル・ベースのデータベースを指定するという方法でも、レコードの消失という問題は解決できます (DB2 および MySQL の場合の設定はこの後すぐに説明します)。
dbCreate の値によっても、異なる環境で振る舞いを変えることができます。dbCreate は基礎となる hibernate.hbm2ddl.auto 設定の別名で、この設定により裏で Hibernate がテーブルを管理する方法が決まります。dbCreate が create-drop に設定されている場合、Hibernate は起動時にテーブルを作成し、シャットダウン時に破棄します。この値を create に変更すると、Hibernate は新しいテーブルを作成し、必要に応じて既存のテーブルを変更しますが、再起動されるたびにすべてのレコードが削除されることになります。実動モードでのデフォルト値、update では、再起動が行われてもすべてのデータが維持されるとともに、必要に応じてテーブルが作成または変更されます。
レガシー・データベースに対して Grails を使用している場合には、できるだけ dbCreate 値をコメント・アウトしてください。この値をコメント・アウトすれば、Hibernate がデータベース・スキーマに影響を与えることはありません。これによって、基礎となるデータベースにデータ・モデルを同期させる作業が必要になることにもなりますが、誰が許可もなくデータベース・テーブルを変更し続けているのか突き止めようと躍起になっている DBA からの怒りの E メール件数は劇的に減るはずです。
独自のカスタム環境を追加するのは簡単です。例えば会社のプログラムがベータ版だとした場合、DataSource.groovy 内の他のブロックの隣に beta ブロックを作成します (データベース関連ではない設定の場合、grails-app/conf/Config.groovy にenvironments ブロックを追加することもできます)。これで grails -Dgrails.env=beta run-app と入力すれば、Grails が beta モードで起動することになります。
dbCreate 設定を使用して Hibernate にテーブルを管理させる場合、まずデータベースを作成してログインし、JDBC ドライバーを lib ディレクトリーにコピーして DataSource.groovy で設定を調整するという簡単な 3 段階のプロセスで、Grails に新しいデータベースを指定することができます。
データベースとユーザーの作成手順は、製品によってまちまちです。DB2 の場合にはステップバイステップのチュートリアルがあるので、オンラインでこれに従ってください (「参考文献」を参照)。データベースとユーザーを作成したら、DataSource.groovy を調整してリスト 11 に示す値を使用するようにします (このリストに示した値は、データベースには trip という名前が付けられているという前提に従っています)。
リスト 11. DataSource.groovy の DB2 用の設定
driverClassName = "com.ibm.db2.jcc.DB2Driver"
username = "db2admin"
password = "db2admin"
url = "jdbc:db2://localhost:50000/trip"
|
MySQL が既にインストールされている場合は、リスト 12 のステップに従って root ユーザーとしてログインし、trip データベースを作成します。
リスト 12. MySQL データベースの作成
$ mysql --user=root
mysql> create database trip;
mysql> use trip;
mysql> grant all on trip.* to grails@localhost identified by 'server';
mysql> flush privileges;
mysql> exit
$ mysql --user=grails -p --database=trip
|
データベースとユーザーを作成したら、リスト 13 に示す値を使用するように DataSource.groovy を調整します。
リスト 13. DataSource.groovy の MySQL 用の設定
driverClassName = "com.mysql.jdbc.Driver"
username = "grails"
password = "server"
url = "jdbc:mysql://localhost:3306/trip?autoreconnect=true"
|
データベースの作成、JDBC ドライバー JAR の lib ディレクトリーへのコピー、そして DataSource.groovy での値の調整が済んだら、もう 1 度 grails run-app と入力します。これで、Grails は HSQLDB 以外のデータベースを使用するようになります。
GORM のツアーはこれで完了です。このツアーを通して ORM とは何か、そして検証およびテーブルの関係を管理する方法、HSQLDB を任意のデータベースに置き換える方法が十分に理解できたはずです。
この連載の次回の記事で焦点を当てるのは、Web 層です。GSP およびさまざまな Groovy TagLib の詳細を掘り下げるとともに、GSP を個々の部分 (複数のページで再利用できるマークアップのフラグメント) に分割する方法を紹介します。さらには、scaffold によって生成されるビューに使われるデフォルト・テンプレートをカスタマイズする方法も説明します。
それまではゆっくり、時間をかけて Grails をマスターしてください。
学ぶために
- 連載「Grails をマスターする」: この連載の他の記事を読んで、Grails とこのフレームワークを使って実現可能なすべての内容をよく理解してください。
-
Grails: Grails の Web サイトにアクセスしてください。
-
Grails Framework Reference Documentation: Grails のバイブルとなります。
- 『Groovy Recipes』(Pragmatic Bookshelf、2008年3月): Scott Davis の最新の著作で Groovy と Grails の詳細を学んでください。
-
Hibernate、TopLink、JPA: よく使われている Java オブジェクト・リレーショナル API です。
-
オブジェクトとリレーショナルのインピーダンス・ミスマッチ: オブジェクト・リレーショナル・マッピングに伴う問題のことです。
-
オブジェクトの合成: 単純なオブジェクトを複雑なオブジェクトに組み合わせます。
-
データベースの標準化: データが異常にならないようにリレーショナル・データベース・テーブルを設計する手法です。
-
抽象化の破綻: Joel Spolsky 曰く、「完全な抽象化は、ある程度破綻しやすい」ものです。
-
ネイキッド・オブジェクト: ドメイン・オブジェクトを修飾するアーキテクチャー・パターンについて読んでください。
-
DRY: 情報は重複すべきでないという点に重点を置くプロセス理念です。
- 「Hello World: Learn the basic features and concepts of DB2 for Linux, UNIX, and Windows」(developerWorks、2006年12月): デモと hello world の演習を記載したこの基本的なチュートリアルで、DB2 for Linux, UNIX, and Windows の概要を把握してください。
- 連載「実用的な Groovy」: developerWorks のこの連載記事では、実用的な Groovy の使用方法を探り、それぞれの方法をいつ、どんな場合に適用するかを伝授しています。
-
Groovy: Groovy の Web サイトで、このプロジェクトの詳細を学んでください。
-
AboutGroovy.com: Groovy に関する最新のニュースと記事へのリンクが記載されています。
-
モデル・ビュー・コントローラー: Grails はこの人気の高いアーキテクチャー・パターンに従っています。
-
テクノロジーのブックストア: この技術やその他の技術に関する本を参照してください。
-
developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が、豊富に用意されています。
製品や技術を入手するために
議論するために
-
developerWorks blogs から developerWorks コミュニティーに加わってください。
