Grailsでレガシーデータベース(既に作られているリレーショナル・データベース)を扱うWebアプリケーションを作る際に出てくる課題の1つが、複合キーの扱いです。
Grailsの売りである(Ruby on Railsが発祥の地ですが)scaffoldを使おうとすると、主キーとして1つのカラムにユニークな整数キーを持たないテーブルに直面したとき、大きな壁にぶつかりました。
Grailsのドメイン・クラスとID
Grailsでは、ドメイン・クラス(データベースにマッピングされる永続化データを保持する)には、整数型で、インスタンスごとにユニークな値を保持するidフィールドがあることが前提となっています。ドメイン・クラスからデータベースのスキーマ(テーブル)を自動生成するパターンの場合、ドメイン・クラスが対応するテーブルには整数型のidカラムが生成されます。
レガシーデータベースをマッピングするドメイン・クラスの場合も、整数型のユニークな値を保持するidフィールドが必要となります。
- ドメイン・クラスが生成するテーブル
class Trip { String name String toString() { return name } }
上記のドメイン・クラスは、Grailsによって以下のテーブルが生成され、対応付けられます。
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
id | bigint(20) | NO | PRI | NULL | auto_increment |
version | bigint(20) | NO | |||
name | varchar(255) | NO |
-
- MySQLの場合
一方、レガシーデータベースとマッピングする場合は、
class Trip { String name static mapping = { table "trips" version false id column: 'trip_id', generator: 'increment' } }
と、整数型のユニークなidフィールドをテーブルのカラムと対応付けします。
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
trip_id | int(11) | NO | PRI | ||
name | varchar(255) | YES | NULL |
ここで、先に述べた複合キーが使用されていると、特定のカラムをidにマッピングすることができません。
複合キーを持つテーブルとドメイン・クラスの対応
飛行機は同じ便名が異なる日付で使用されるので、便名だけではユニークにならず、便名+出発日の2つのカラムで複合キーを取るテーブルがあったとします。
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
flight_name | varchar(8) | NO | PRI | ||
airline_id | int(11) | YES | NULL | ||
departure_date | date | NO | PRI |
これを、ドメイン・クラスに定義すると、
class Flight { String name int airline Date departureDate static mapping = { table 'flights' version false id composite: ['name', 'departureDate'], generator: 'assigned' name column: 'flight_name' airline column: 'airline_id' departureDate column: 'departure_date' } }
となります。GORMでは、複合キーに対応するドメイン・クラスの定義方法があります。
しかし、scaffoldでコントローラとビューを自動生成されたものを使う場合に問題が発生します。
- scaffoldで生成されるFlight Listの画面イメージ
id | Name | Airline | Departure Date |
---|---|---|---|
JJJ821 | 101 | 2009-08-08 00:00:00.0 | |
JJJ824 | 101 | 2009-08-08 00:00:00.0 | |
JJJ821 | 101 | 2009-08-09 00:00:00.0 |
id欄が空欄となり、詳細表示へのリンクがなくなってしまっています。これは、idに複合キーを指定した場合に現バージョンのGrails(1.1.1)がidをうまくハンドリングできないからと思われます。不具合にこれと同じ問題が上がっているようです。
http://jira.codehaus.org/browse/GRAILS-3723
次期リリース(1.2)では修正されるようですが、当面は何らかの方法で回避する必要があります。
回避方法について、Web上にいくつか回避方法が取り上げられています。
複合キーのデータ一覧から詳細表示
scaffoldで生成されるビューとコントローラに手を入れて回避するため、grails generate-allコマンドを実行します。
生成されるlist.gspにおいて、id欄の表示を生成している箇所を見ると、ドメイン・クラスのインスタンスのプロパティidを取得するコードになっています。
<td> <g:link action="show" id="${flightInstance.id}"> ${fieldValue(bean:flightInstance, field:'id')} </g:link> </td>
しかし、複合キーを指定したレガシーデータベースマッピングのドメイン・クラスでは、プロパティidを取得するとヌルが返ってきます。
回避策
2つ目の回避策のWeb記事にあった方法で対処してみました。
list.gspのコードは以下になります。
<td> <g:link action="show" params="[name:flightInstance.name, departureDate:flightInstance.departureDate]">詳細 </g:link> </td>
コントローラクラスの修正は以下になります。
def dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd") : def show = { def flightName = params['name'] def departureDate = params['departureDate'][0..9] def flightDate = dateFormat.parse(departureDate) def flightInstance = Flight.get( new Flight(name:flightName, departureDate:flightDate)) :