Grails をマスターする: Ajax をほんの少し加えた多対多の関係

Web アプリケーションで多対多 (m:m) の関係を扱うには、さまざまな注意が必要になります。連載「Grails をマスターする」の今回の記事では、Scott Davis が Grails で m:m の関係をうまく実装する方法を説明します。GORM (Grails Object Relational Mapping) API とバックエンド・データベースではこの関係をどのように扱うか、そして Ajax (Asynchronous JavaScript + XML) を少し使うことでユーザー・インターフェースをいかに効率的なインターフェースにできるかを学んでください。

Scott Davis , Editor in Chief, AboutGroovy.com

Scott DavisScott Davis は国際的に知られた著者、講演者、そしてソフトウェア開発者です。彼の著書には、『Groovy Recipes: Greasing the Wheels of Java』、『GIS for Web Developers: Adding Where to Your Application』、『The Google Maps API』、『JBoss At Work』などがあります。



2008年 4月 15日

ソフトウェアの開発とは、現実の世界をコードでモデル化するということです。例えば、本には著者と出版社があるので、Grails アプリケーションで著者と出版社それぞれのドメイン・クラスを作成します。すると、後は GORM が各クラスに対応するデータベース・テーブルを作成し、scaffold 機能が基本 CRUD (Create/Retrieve/Update/Delete) の Web インターフェースを用意してくれます。

このプロセスでの次のステップは、クラス間の関係を定義することです。一般的な出版社が出版する本は 1 冊だけではないので、出版社と本の関係は単純な 1 対多 (1:m) の関係ということになります。つまり、「1」は Publisher で、「多」は出版されている Book です。この 1:m の関係を作成するには、Publisher クラスに static hasMany = [books:Book] という記述をします。Book クラスには static belongsTo = Publisher という記述を加え、出版社と本との関係にもう 1 つの側面を追加します。それは更新と削除のカスケードのことで、Publisher を削除すると、対応するすべての Book も同じく削除されることになります。

この連載について

Grails は、Spring や Hibernate などのよく知られた Java 技術に「Convention over Configuration」といった現代のプラクティスを盛り込んだ最新の Web 開発フレームワークです。Groovy で作成された Grails は既存の Java コードをシームレスに統合するだけでなく、スクリプト言語ならではの柔軟性と動的機能を与えてくれます。Grails を学んだら、Web 開発に対する今までの見方がまったく違ってくるはずです。

基礎となるデータベースで 1:m の関係をモデル化するのは簡単です。各テーブルには主キーの役割を果たす id フィールドがあります。そこで、GORM が publisher_id フィールドを book テーブルに追加すれば、この 2 つのテーブル間に 1:m の関係ができあがります。フロントエンドでも、Grails は 1:m の関係を難なく扱います。新しい Book を作成すると、自動的に生成された (scaffold された) HTML フォームがドロップダウン・コンボ・ボックスを提供して選択肢を既存の Publisher のリストに限定します。このような 1:m の関係は、この連載の第 1 回目から例を説明してきたとおりです。

今回は多少高度な関係として、多対多 (m:m) の関係に着目します。BookAuthor の関係は、BookPublisher の関係ほど簡単にはモデル化することはできません。1 冊の本に複数の著者がいる場合もあれば、1 人の著者が複数の本を出版している場合もあるからです。これは典型的な m:m の関係で、現実の世界をモデル化するという点では m:m の関係はかなり一般的なものです。例えば、1 人の個人が複数の当座預金口座を持つ一方、1 つの当座預金口座が複数の人々によって管理される場合もあります。1 人のコンサルタントが多数のプロジェクトに取り組んでいる一方、1 つのプロジェクトに多数のコンサルタントがいる場合もあります。この記事では、連載を通して開発を進めている Trip Planner アプリケーションを構築するなかで、Grails で m:m の関係を実装する方法を紹介します。Trip Planner アプリケーションに取り掛かる前に、まずは重要なポイントを理解してもらいたいので本の例についてもう少し説明させてください。

第 3 のクラス

本の例のデータベースでは 3 つのテーブルが m:m の関係を表します。そのうちの 2 つは当然のことながら BookAuthor で、もう 1 つのテーブルは BookAuthor という結合テーブルです。GORM はBookAuthor テーブルに外部キーを追加するのではなく、book_idauthor_idBookAuthor 結合テーブルに追加します。この結合テーブルでは、1 人の著者に対して複数の本を持つことができるだけでなく、1 冊の本に対して複数の著者を持つこともできます。さらに、複数の本を著した複数の著者を表現することも可能です。この結合テーブルには Author および Book 外部キーの固有の組み合わせごとに専用のレコードができるため、まさに制限のない柔軟性がもたらされます。すなわち、1 冊の本あたりの著者の数にも、1 人の著者あたりの著書の数にも制限がないということです。

Dierk Koenig はかつて私にこう言いました。「2 つのオブジェクトの関係が単純な多対多の関係だと考えているなら、ドメインをまだ十分に調べきれていないということですよ。なぜなら、属性とそれ自体のライフサイクルを持つ第 3 のオブジェクトをまだ見つけていないわけですからね」。実際、BookAuthor の関係は単純な結合テーブルの域を超えています。例えば、Dierk は『Groovy in Action』(Manning Publications、2007年1月) の主要な著者です。筆頭著者は、AuthorBook の関係で 1 つのフィールドとして表さなければなりません。さらに、共著者は表紙に特定の順序でリストされていること、各著者が本の特定の章に寄稿していること、そして各著者はおそらくその貢献度に応じて異なる報酬を受けているなど、その他にもさまざまな事実が関わってきます。このように、AuthorBook の関係には当初計画していたよりも微妙な意味合いがあります。現実の世界では、それぞれの著者が本との関係を明確な条件で詳説した契約にサインしているので、BookAuthor の関係をより明確に表現するにはファースト・クラス・オブジェクトである Contract クラスを作成すべきかもしれません。

簡単に言うと、m:m 関係のように見えるものでも、実際には 2 つの 1:m 関係であるということです。2 つのクラスが m:m の関係を持っているようであれば、さらに詳しく調べてこの 2 つの 1:m の関係を保有する第 3 のクラスを見つけ出し、そのクラスを明示的に定義しなければなりません。


航空会社と空港のモデル化

Trip Planner アプリケーションに話を戻して、早速ドメイン・モデルを再検討し、m:m の関係が隠れていないか調べることにしましょう。連載の最初の記事では、リスト 1 の Trip クラスを作成しました。

リスト 1. Trip クラス
class Trip { 
  String name
  String city
  Date startDate
  Date endDate
}

連載第 2 回目ではリスト 2 の Airline クラスを追加し、単純な 1:m の関係を説明しました。

リスト 2. Airline クラス
class Airline { 
  static hasMany = [trip:Trip]
  String name
  String frequentFlier
}

この 2 つの小さなクラスはその目的 (ポイントを説明するための単純なプレースホルダー) をとりあえずは果たしたものの、厳格なドメイン・モデルとしては持ちこたえられません。そこで元のクラスを修正して、もう少し強固にしたものを作り上げることにします。

Trip クラスを上記のように作成したのは、その時点では納得がいったからです。例えば、「シカゴへの旅行を計画している」、「来月の 15日から 20日までニューヨークに滞在する」などと言う場合、Tripの属性としては citystartDateendDate といったフィールドが当然のように思えました。しかしもう 1 度考えてみると、Trip はそれほど単純ではなさそうです。

私が住んでいるコロラド州デンバーは、ユナイテッド航空の拠点となっています。つまり、通常は目的地まで直行便で行けるということです。ただし、最終目的地に到着するまでに 1 回以上乗り継ぎがある場合もあれば、目的地が 1 つの都市だけではない場合もあります。例えば、「ボストンまで飛んで月曜日から金曜日まではクラスで講義を行い、土曜日には東海岸を離れる前にワシントン D.C. に立ち寄って会議で講演。家には日曜日の午後に帰る」というような計画です。幸運にも特定の都市への直行便が見つかって他のどの都市も経由しないとしても、この旅行には複数のフライト、すなわち往きのフライトと帰りのフライトが必要であることには変わりありません。したがって、1 回の Trip には複数の Flight が伴うことになります。リスト 3 に、TripFlight の関係を定義します。

リスト 3. Trip と Flight との間の 1:m の関係
class Trip{
  static hasMany = [flights:Flight]
  String name
}

class Flight{
  static belongsTo = Trip
  String flightNumber
  Date departureDate
  Date arrivalDate
}

belongsTo フィールドとの関係を設定するということは、Trip を削除すると関連するすべての Flight も削除されるということです。構築しているシステムが例えば航空管制を対象としたものだったとしたら、おそらくこれとは異なるアーキテクチャーにすると思います。あるいは、複数の乗客が同じフライトを共有するためのシステムを構築しているとしたら (1 便の Flight が複数の Passenger を持ち、1 人の Passenger が複数の Flight を持つことが可能なシステム)、フライトを特定の乗客の旅行に結び付けると問題になるはずです。しかし、ここでモデル化しようとしているのは、世界中の数百万もの乗客のために毎日飛んでいる何千便ものフライトではありません。この単純な例での Flight は、Trip をさらに詳しく記述するためだけのものです。Trip が私にとって重要でなくなれば、付随するそれぞれの Flight も同じく重要でなくなります。

Airline クラスについてはどうすればよいのでしょうか? 1 回の Trip に多数の異なる Airline が関係することも、1 社の Airline に多数の異なる Trip が関係することもあります。この 2 つのクラスの間に m:m の関係があることは確かですが、AirlineFlight に追加するのが適切だと思います (リスト 4 を参照)。1 社の Airline は複数の Flight を持てる一方、複数の Airline を持つ Flight は有り得ないからです (訳注: 共同運航便等の複雑な条件はここでは考慮していないようです)。

リスト 4. Airline と Flight との関連付け
class Airline{
  static hasMany = [flights:Flight]
  String name
  String iata
  String frequentFlier
}

class Flight{
  static belongsTo = [trip:Trip, airline:Airline]
  String flightNumber
  Date departureDate
  Date arrivalDate
}

上記のリストには注目すべき点がいくつかあります。まず、Flight に含まれる belongsTo フィールドの値が、単一の値から値のハッシュマップに変わっている点です。1 回の Trip に複数の Flight があるのと同様に、1 社の Airline にも複数の Flight があるからです。

次に注目する点として、Airline には iata フィールドを新たに追加しました。これは、IATA (International Air Transport Association) コード用のフィールドです。IATA では、航空会社ごとに固有のコード (ユナイテッド航空の UAL、コンチネンタル航空の COA、デルタ航空の DAL など) を指定しています (全 IATA コードのリストについては、「参考文献」を参照)。

最後の点は、もう 1 つアーキテクチャー上の変更点として Airline とマイレージ番号 (FrequentFlier) との間の関係を含めています。このシステムのユーザーは 1 人であるという前提なので、FrequentFlierAirline クラスの属性にしてもまったく問題ありません。マイレージ番号は、各航空会社から複数取得することはできないので、これが考えられる最も簡単なソリューションです。この Trip Planner の要件が変更されて複数のユーザーをサポートする必要が出てきた場合には、別の m:m の関係が浮かび上がってくることになります。1 人の乗客が複数のマイレージ番号を持っている場合もあれば、1 つの航空会社が複数のマイレージ番号を持っている場合もあるからです。本来ならば、この関係を管理する結合テーブルを作成するところですが、今のところはこの単純なソリューションのままでいきます。将来要件が変更された場合には、FrequentFlier フィールドに関する修正を行うということを心に留めておきます。


都市か、それとも空港か

今度は City をクラスへ追加しますが、それ以前に、追加するかしないかを考えなければなりません。「シカゴまで飛行機で行く」という表現でも、厳密に言うと、到着するのは空港です。シカゴに行くと言っても、オヘア空港とミッドウェイ空港のどちらに到着するのでしょうか。ニューヨークへのフライトには、ラガーディア空港に到着する便もあれば、JFK 空港に到着する便もあります。したがって、単なる City フィールドではなく、Airport クラスが必要なことは明らかです。リスト 5 に、Airport クラスを示します。

リスト 5. Airport クラス
class Airport{
  static hasMany = [flights:Flight]
  String name
  String iata
  String city
  String state
  String country
}

ご覧のようにリスト 5 にも iata フィールドが含まれています。今度はこのフィールドで、例えばデンバー国際空港の DEN、シカゴ・オヘア空港の ORD、シカゴ・ミッドウェイ空港の MDW などが示されます。ここで、State クラスを作成して単純な 1:m の関係を設定したり、あるいはさらに高度な手段として都市、州、国をカプセル化する Location クラスを作成するという考えに至るかもしれません。最終的な仕上げについては、時間があるときの課題としてお任せすることにします。

次に、AirportFlight クラスに追加します (リスト 6 を参照)。

リスト 6. Airport と Flight との関連付け
class Flight{
  static belongsTo = [trip:Trip, airline:Airline]
  String flightNumber
  Date departureDate
  Airport departureAirport
  Date arrivalDate
  Airport arrivalAirport
}

上記では、暗黙的に belongsTo フィールドを使う代わりに、明示的に departureAirport および arrivalAirport フィールドを作成しています。ユーザー・インターフェースの表示には何の変わりもなく、フィールドはすべてコンボ・ボックスを使って表示されますが、クラス間の関係には微妙ながらも重要な違いがあります。それは、Airport を削除しても関連する Flight は削除されない一方、Trip または Airline を削除すると Flight も削除されるという点です。ここで両方の手段を紹介している理由は、各種のクラスを関連付けるさまざまな方法を説明するためです。実際には、クラスに厳格な参照整合性を維持させるか (つまり、すべてのクラスに削除カスケードを設定)、それよりも疎な関係を許可するかは個人の判断によります。


多対多の関係の実際の作用

これで、オブジェクト・モデルは現実の世界にかなり近いものになりました。私は年に何度も旅行します。旅行で使用する航空会社も到着する空港もさまざまですが、これらの関係をすべて 1 つに結び付けるのが、Flight です。

基礎となるデータベースを調べてみると、MySQL の show tables コマンドの出力 (リスト 7) に示されているように、期待した通りのテーブル以外には何もありません。

リスト 7. 作成されたテーブル
mysql> show tables;
+----------------+
| Tables_in_trip |
+----------------+
| airline        | 
| airport        | 
| flight         | 
| trip           | 
+----------------+

airline、airport、そして trip テーブルの列はすべて、対応するドメイン・クラスのフィールドと一致しています。flight テーブルは結合テーブルで、このテーブルが他のテーブルとの複雑な関係を表します。この flight テーブルに含まれるフィールドは、リスト 8 のとおりです。

リスト 8. flight テーブルのフィールド
mysql> desc flight;
+----------------------+--------------+------+-----+
| Field                | Type         | Null | Key |
+----------------------+--------------+------+-----+
| id                   | bigint(20)   | NO   | PRI |
| version              | bigint(20)   | NO   |     |
| airline_id           | bigint(20)   | YES  | MUL |
| arrival_airport_id   | bigint(20)   | NO   | MUL |
| arrival_date         | datetime     | NO   |     |
| departure_airport_id | bigint(20)   | NO   | MUL |
| departure_date       | datetime     | NO   |     |
| flight_number        | varchar(255) | NO   |     |
| trip_id              | bigint(20)   | YES  | MUL |
+----------------------+--------------+------+-----+

Flight を新規に作成するために scaffold された HTML ページ (図 1 を参照) には、関連するすべてのテーブルそれぞれに対応したコンボ・ボックスが表示されます。

図 1. フライト追加用に scaffold された HTML ページ
フライト追加用に scaffold された HTML ページ

ユーザー・インターフェースの微調整

これまで m:m の関係について、クラスとデータベース・テーブルを使用してこの関係をモデル化する方法を重点に説明を進めてきましたが、これは科学であると同時に芸術でもあることを理解してください。Grails 開発者であれば、Grails の持つ数々の細かな機能を利用して関係の振る舞いと副次作用を改善することができます。ここからはユーザー・インターフェースに目を向けて、m:m 関係の表示を巧妙にカスタマイズする方法を説明します。

前のセクションで説明したように、Grails はデフォルトでは選択フィールドによって 1:m の関係を表示します。この表示方法は手始めとしては悪くはありませんが、状況が変われば他の HTML コントロールが必要になってくることもあります。選択フィールドに表示されるのは現行の値のみで、選択可能なすべての値を確認するにはドロップダウン・リストを表示しなければなりません。画面の表示域が限られているとしたら、これが最善の方法でしょうが、すべての選択肢を表示したほうが適切なソリューションであると判断する場合もあります。選択可能なすべての値を表示し、選択を 1 つの値だけに制限するにはラジオ・ボタンが最適です。一方、選択可能なすべての値のなかから複数の値を選択できるようにするにはチェック・ボックスを使用します。

いずれのコントロールにしても、限られた数の選択項目を表示するには有効ですが、何百、あるいは何千もの可能な値には対応しきれません。例えば、世界の約 650 の航空会社をエンド・ユーザーに表示しなければならない場合、このような量に対処できる標準 HTML コントロールは 1 つもありません。しかし、ここが開発者の判断力の見せ所です。このアプリケーションの場合、私だったら 650 すべての航空会社を表示することはしません。今まで私が使用した航空会社はせいぜい 10 社くらいのものなので、選択フィールドを使って航空会社の選択肢を表示すれば、とりあえずは事が足りるはずです。

Grails がどのように Airline の選択フィールドを作成するかを調べるため、grails generate-views Flight と入力して、grails-app/views/flight/create.gsp を見てください。選択フィールドは、<g:select> タグを使用した 1 行のコードで生成されます (Grails の TagLibs について馴染みのない方は、先月の記事を参照してください)。リスト 9 に、コード内で使用されている <g:select> フィールドを示します。

リスト 9. 実際の <g:select> フィールド
<g:select optionKey="id" 
          from="${Airline.list()}" 
          name="airline.id" 
          value="${flight?.airline?.id}" ></g:select>

これがどのようにレンダリングされるかを調べるには、Web ブラウザーで 表示 > ソース を選択してください (リスト 10 を参照)。

リスト 10. レンダリングされた選択フィールド
<select name="airline.id" id="airline.id" >
<option value="1" >UAL - United Airlines</option>
<option value="2" >DAL - Delta</option>
<option value="3" >COA - Continental</option>
</select>

<g:select> タグの optionKey 属性が、1 対多の関係の 1 に対応するクラスのどのフィールドを多に対応するフィールドの値に格納するかを指定します。Airline テーブルの主キー (airline.id) は、flight テーブルでは外部キーとして表示されます。選択フィールドでは airline.id が選択肢の値であることに注目してください (Airline.toString() メソッドは表示値に対して呼び出されます)。選択肢のソート順を変更する場合には、GORM 呼び出しを Airline.list() から Airline.listOrderByIata()Airline.listOrderByName()、あるいは他の任意のフィールドに変更してください。


大量の選択肢に Ajax で対処する方法

デフォルトの選択コントロールは現実的な数の航空会社を表示するには妥当な選択ですが、空港となるとそうはいきません。私が 1 年に利用する空港の数は 40 から 50 にのぼりますが、経験から言って、選択フィールドに 15 から 20 を超える選択項目を表示しようとすると途端に厄介になってきます。

幸いなことに、空港の IATA コードは業界で広範に使用されています。フライトを調べるときにも、フライトの予約をしたときの予約票にも、IATA コードが登場します。航空券自体に示されるのも、このコードです。しがって、ユーザーに数百もの空港のリストをスクロールさせるのではなく、IATA コードを入力するよう求めるのが理にかなっています。

この記事の最初で紹介した Book の例を思い返してください。Amazon.com のホーム・ページには、在庫にあるすべての本を表示する 1 つの大きな選択フィールドがあるでしょうか。答えはノーです。このホーム・ページにあるのはテキスト・フィールドで、このフィールドには本のタイトル、著者、あるいは ISBN (International Standard Book Number) でさえ入力できるようになっています。Trip Planner では、これと同じ手法を空港の選択に適用することにします。

コントロールを選択フィールドからテキスト・フィールドに変更するのは簡単です。しかしソリューションの具体的方法に話を進める前に、コントロールをテキスト・フィールドにしたときの動作について説明したいと思います。iata フィールドはフリー・フォームのテキスト・フィールドですが、ユーザーが入力する値を何でも受け入れるというわけには行きません。(このアプリケーションは、名前のスペルを誤っても警告しませんが、IATA コードの入力を誤った場合は警告する必要があります。) しかも、入力に対するフィードバックは即座に行われるようにする必要があります。HTML フォーム全体を送信するたびに無効な値の入力を警告されるという事態が何度も繰り返されることほど苛立たしいことはないからです。

したがって、1 つのフィールドへの入力値の妥当性を検証するためだけの目的や、選択可能な何千もの空港の IATA コードを毎回クライアントにダウンロードするためにフォーム全体を往復させることは避けなければなりません。そこでソリューションとなるのは、データをサーバーに残し、フォーム全体をまとめてのリクエストではなく、個々のフィールドに対して細かな HTTP リクエストを行うことです。この手法は、Ajax (Asynchronous JavaScript + XML) リクエストと呼ばれます (Ajax の概要については「参考文献」を参照)。

この Grails アプリケーションを Ajax 対応のアプリケーションにするには、Ajax リクエストを受け入れるように AirportController をカスタマイズし、Ajax リクエストを行うようにビューをカスタマイズする必要があります。まずは AirportController から取り掛かることにします。

AirportController にはすでに Airport のリストを返すためのクロージャーと個々の Airport を表示するためのクロージャーが scaffold されています。しかし、これらの既存のクロージャーは値を HTML として返すので、データをそのまま処理せずに返す新しいクロージャーをこれから追加します。POGO をシリアライズするという方法もありますが、このアプリケーションのクライアントは Web ブラウザーです。不幸にも、(これは Mozilla Foundation によく聞いてもらいたいことですが) Web ブラウザーが使う言語は JavaScript で、Groovy ではありません。

Ajax の x からわかるように、XML を返すという手段があります。grails.converters パッケージを AirportController にインポートすれば、たった 1 行で XML を返すことができます (リスト 11 を参照)。

リスト 11. コントローラーから XML を返す方法
import grails.converters.*

class AirportController {
  def scaffold = Airport
  
  def getXml = {
    render Airport.findByIata(params.iata) as XML
  }  
}

このソリューションの唯一の問題は、Groovy にとって XML がネイティブでないのと同様に JavaScript にとってもネイティブではないという点です。GORM のようなオブジェクト・リレーショナル・マッパーには、データを (リレーショナル・データベースに保存された) ネイティブ以外のフォーマットから Groovy にシームレスに変換するという利点があります。この演習の JavaScript 版では、Groovy データを JSON (JavaScript Object Notation) に変換しますが (「参考文献」を参照)、ありがたいことに、JSON への変換は XML の場合と同じく 1 行で行うことができます。リスト 12 では多少のエラー処理を getJson クロージャーに追加していますが、それ以外は getXml クロージャーとまったく同じです。

リスト 12. コントローラーから JSON を返す方法
def getJson = {
  def airport = Airport.findByIata(params.iata)
  
  if(!airport){
    airport = new Airport(iata:params.iata, name:"Not found")
  }
  
  render airport as JSON
}

JSON への変換が有効であることを検証するには、Web ブラウザーに http://localhost:9090/trip/airport/getJson?iata=den と入力してください。リスト 13 に記載するレスポンスが表示されるはずです (ブラウザーで 表示 > ソース を選択しなければ JSON によるレスポンスが表示されない場合もあります)。

リスト 13. JSON によるレスポンス
{"id":1,"class":"Airport","city":
   "Denver","country":"US","iata":
"DEN","name":"Denver International Airport","state":"CO"}

航空会社のリストを返すためのプロセスも同じく簡単で、render Airline.list() as JSON という 1 行だけで済みます。

JSON が生成されるようになったので、今度はこれを使用する番です。それには departureAirport の既存の <g:select> をコメント・アウトし、リスト 14 に記載する 4 行のコードに置き換えます。

リスト 14. 選択フィールドからテキスト・フィールドへの置換
<div id="departureAirportText">[Type an Airport IATA Code]</div>
<input type="hidden" name="departureAirport.id" value="-1" 
   id="departureAirport.id"/>          
<input type="text" name="departureAirportIata" id="departureAirportIata"/>
<input type="button" value="Find" onClick="get('departureAirport')"/>

最初の行は、読み取り専用の表示域です。この表示域には id があることに注意してください。ID は、HTML DOM (Document Object Model) 全体で固有でなければなりません。JSON 呼び出しの結果をすぐに書き出すため、ハンドル departureAirportText を使用します。

フォームの送信時に、入力や選択などのフォーム・コントロールはサーバーに送られますが、<div> はサーバーに送られません。この隠しテキスト・フィールドは、フォーム全体がサーバーに送信されるときに Airportid を格納する場所となります。

departureAirportIata というテキスト・フィールドは、ユーザーが IATA コードを入力する場所です。名前と ID の両方に同じ値を指定するというのは DRY (Don't Repeat Yourself) の原則に即しているようには思えませんが、HTML の構造上、こうする必要があります。名前はフォームが送信されるときにサーバーに送られるもので、ID は getJson クロージャーを呼び出すときの基準として使用するものだからです。

最後の行はボタンです。このボタンをクリックすると、get という JavaScript 関数が呼び出されます。get 関数の実装はこの後すぐに記載します。とりあえず、図 2 にこの新しいフォームがどのように表示されるかを示します。

図 2. 改良後のフォーム
改良後のフォーム

Prototype を使った Ajax 呼び出し

Grails には Prototype という名前の JavaScript ライブラリーが付属しています (「参考文献」を参照)。Prototype は主要なすべてのブラウザーで互換性のある Ajax 呼び出しを行う共通の方法となります。get 関数は、ブラウザーに入力された URL を単純に組み立ててから、サーバーに対して非同期のコールバックを行います。この呼び出しが成功する (HTTP 200 が返される) と、update 関数が呼び出されます。リスト 15 では、Ajax 呼び出しに Prototype を使っています。

リスト 15. Prototype を使用した Ajax 呼び出し
<g:javascript library="prototype" />     
<script type="text/javascript">
  function get(airportField){
    var baseUrl = "${createLink(controller:'airport', action:'getJson')}"
    var url = baseUrl + "?iata=" + $F(airportField + "Iata")
    new Ajax.Request(url, {
      method: 'get',
      asynchronous: true,
      onSuccess: function(req) {update(req.responseText, airportField)}
    })
  }
  
  ...
</script>

update 関数は JSON 呼び出しの結果を読み取って <div> の表示を更新し、隠しフィールドの値を空港の主キーが見つかった場合はこの主キーに変更し、見つからなかった場合には -1 に変更します。リスト 16 に、update 関数を示します。

リスト 16. JSON データによるフィールドの更新
function update(json, airportField){
  var airport = eval( "(" + json + ")" )
  var output = $(airportField + "Text")
  output.innerHTML = airport.iata + " - " + airport.name
  var hiddenField = $(airportField + ".id")
  airport.id == null ? hiddenField.value = -1 : hiddenField.value = airport.id
}

図 3 は、いくつかの Ajax 呼び出しが成功した後の Flight フォームの例です。

図 3. Ajax 呼び出しによって値が設定されたフォーム
Ajax 呼び出しによって値が設定されたフォーム

クライアント・サイドでの検証

最後に、クライアント・サイドで簡単な検証を行って、departureAirportarrivalAirport の不正な値がサーバーに送信されないことを確実にする必要があります (ユーザーに選択フィールドやラジオ・ボタン、あるいはチェック・ボックスを提示する場合には、不正な値を入力するのは文字通り不可能ですが、ここではユーザーがフリー・フォームのテキストを入力できるようにしているため、ユーザーが入力した内容のチェックについては多少慎重にならなければなりません)。

g:form タグに onSubmit を追加してください。

<g:form action="save" method="post" onsubmit="return validate()" >

validatetrue を返すとフォームはサーバーに送信され、false を返すと送信はキャンセルされます。この validate 関数はリスト 17 のとおりです。

リスト 17. validate 関数
function validate(){
  if( $F("departureAirport.id") == -1 ){
    alert("Please supply a valid Departure Airport")
    return false
  }
  
  if( $F("arrivalAirport.id") == -1 ){
    alert("Please supply a valid Arrival Airport")
    return false
  }
  
  return true
}

選択フィールドをテキスト・フィールドに変えるのは少々面倒な作業だとお考えなら、私の考えも同じです。この変更は自分の作業を楽にするためのものではなく、エンド・ユーザーにとっての便宜を図るための変更です。その一方、Grails に用意された scaffold 機能が手始めの作業をほとんど代わりに行ってくれたので、あちこちで多少の微調整を行うのは苦にはなりません。scaffold 機能は完成品になるように意図されているわけではなく、パズルのもっと面白い部分に専念できるよう、退屈でつまらない作業をすべて取り除くことを目的としています。


まとめ

この記事を読んで、多対多の関係の単なる構造だけでなく、この関係を作成する上での微妙な部分も理解していただけたことを願います。削除をカスケードさせなければならない場合もあれば、その必要がない場合もあります。2 つのクラスの関係が単純な m:m に思えたら、そこには必ずと言っていいほど、見つけるべき第 3 のクラスが隠れています。

表示に関して言うと、m:m 関係にはデフォルトで選択フィールドが使用されますが、他にも手段はあります。選択肢が少ない場合には、ラジオ・ボタンとチェック・ボックスを使うことを検討してください。また、数多くの選択肢がある場合にはテキスト・フィールドと Ajax が功を奏します。

来月は、フライトを対話型 Google Mapsに配置する方法、さらに Grails のサービスについても紹介します。それまで、Grails を楽しみながらマスターしてください。

参考文献

学ぶために

製品や技術を入手するために

  • Grails: Grails の最新リリースをダウンロードしてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Web development, Open source
ArticleID=309162
ArticleTitle=Grails をマスターする: Ajax をほんの少し加えた多対多の関係
publish-date=04152008