torutkのブログ

ソフトウェア・エンジニアのブログ

レガシーデータベースの複合キーとscaffold

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

一方、レガシーデータベースとマッピングする場合は、

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))
     :