torutkのブログ

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

「Grailsをマスターする:Ajaxをほんの少し加えた多対多の関係」を読みながら動かしてみる

Grails をマスターする: Ajax をほんの少し加えた多対多の関係の記事に沿ってGrailsの習得を進めます。

id:torutk:20090719の続きです。

ドメインクラスFlightを追加し、多対多のモデルを構築する

今までの記事で扱っていたサンプルアプリケーションtrip-plannerのドメインモデルは、TripとAirlineの1対多の関係でした。記事にあるクラス間の関係をUMLクラス図で表現してみると以下になります。(記事中ではUMLは登場しない)
http://yuml.me/diagram/scruffy/class/%5BTrip%5D%3C*-1%3E%5BAirline%5D.png

今回の記事では、Trip、Airline以外に、Flight、Airportクラスを導入し、より本格的なドメインモデルを作成します。記事では、順番に1つずつクラスを追加しコードを記述し、最終的に以下のクラス関係となります。(記事中ではUMLは登場しない)
http://www.yuml.me/diagram/scruffy/class/%5BTrip%5D++1-*%3E%5BFlight%5D%2C%5BAirline%5D++1-*%3E%5BFlight%5D%2C%5BFlight%5D-departure%201%3E%5BAirport%5D%2C%5BFlight%5D-arrival%201%3E%5BAirport%5D%2C%5BFlight%5D%3C*-1%5BAirport%5D.png

前回の記事で、scaffoldを無効にし、ビューをカスタマイズしているので、今回の記事については、新たに作り直した方がよいと思います。また、前のプロジェクトが残っていると、同じプロジェクト名で複数ディレクトリがあり混乱するので、プロジェクト名を変えるか前のものを削除しておきます。以下、前のプロジェクト /home/torutk/work/trip-planner/をそっくり削除し、新たにプロジェクトを作ります。

プロジェクトの新規作成

今まで動かしてきたtrip-plannerを削除した後、新たにtrip-plannerアプリケーションを作成します。

~$ cd work
work$ grails create-app trip-planner
    :
work$ cd trip-planner
trip-planner$

前のプロジェクトが生成したものが残っているので、grails cleanコマンドを実行しておきます。

trip-planner$ grails clean
   :(中略)
   [delete] Deleting: /home/torutk/.grails/1.1.1/projects/trip-planner/resources/web.xml
   [delete] Deleting directory /home/torutk/.grails/1.1.1/projects/trip-planner/classes
   [delete] Deleting directory /home/torutk/.grails/1.1.1/projects/trip-planner/resources
   [delete] Deleting directory /home/torutk/.grails/1.1.1/projects/trip-planner/test-classes
trip-planner$ 
ドメインクラスの作成

Trip、Flight、Airline、Airportの4つのドメインクラスを作成します。

trip-planner$ grails create-domain-class Trip
    :
trip-planner$ grails create-domain-class Flight
    :
trip-planner$ grails create-domain-class Airline
    :
trip-planner$ grails create-domain-class Airport
    :
trip-planner$ 


ドメインクラスを記事にあるように記述します。

FlightとAirport間の関連のエラーと対処

さて、記事通りにドメインクラスを作成して、いったんgrailsを起動します。すると、Airportクラスに関してエラーが発生しました。

trip-planner$ grails run-app
    :(中略)
org.codehaus.groovy.grails.exceptions.GrailsDomainException: 
Property [flights] in class [class Airport] is a bidirectional 
one-to-many with two possible properties on the inverse side. 
Either name one of the properties on other side of the relationship 
[airport] or use the 'mappedBy' static to define the property that
 the relationship is mapped with. Example: static mappedBy = [flights:'myprop']

エラーメッセージから、AirportクラスとFlightクラスの関連の記述が間違っているようです。

  • Airportクラスのコードは以下です。
class Airport {
    static hasMany = [flights:Flight]
    String name
    String iata
    String city
    String state
    String country
}
  • Flightクラスのコードは以下です。
class Flight {
    static belongsTo = [trip:Trip, airline:Airline]
    String flightNumber
    Date departureDate
    Airport departureAirport
    Date arrivalDate
    Airport arrivalAirport
}

このFlightクラスに、2つのAirportクラスのプロパティ(departureAirportとarrivalAirport)があり、1対多の双方向関連の定義で逆方向からたどることができないのが原因です。試しに、arrivalAirportをコメントアウトするとエラーが発生しなくなります。

この件については、Grails1.1のリファレンスドキュメント 5.2.1.2 One-to-many項に説明がありました。

1:多関係の多側に同じ型のプロパティが2つある場合、mappedByを用いてコレクションがどちらをマッピングすればよいかを指定しなければならない。

そこで、Airportクラスに以下の記述を追加します。

    static mappedBy = [flights:"departureAirport"]
使用するデータベースをMySQLに変更する

標準のHSQLDBでは、Grailsが生成したテーブル等を見るのが困難なので、MySQLを使用するようにgrails-app/conf/DataSource.groovyを修正します。

dataSource {
    pooled = true
    driverClassName = "com.mysql.jdbc.Driver"
    username = "grail"
    password = "holly"
    url = "jdbc:mysql://calandre:3306/trip"
}
:(中略)
environments {
    development {
        dataSource {
            dbCreate = "create-drop" // one of 'create', 'create-drop','update'
        }
    }

環境(開発モード/試験モード/実動モード)でJDBCのURLを変えない場合、先頭のdataSource{}内にurlを記述しても大丈夫でした。

なお、mysqlJDBCコネクタを、trip-planner/libにコピーしておきます。

scaffoldのコントローラを作成する

上述で作成したドメインクラス Trip, Flight, Airline, Airportに対応する各コントローラクラスを作成します。すべてscaffoldで作成します。

trip-planner$ grails create-controller Trip
    :
trip-planner$ grails create-controller Flight
    :
trip-planner$ grails create-controller Airline
    :
trip-planner$ grails create-controller Airport
    :
trip-planner$ 
  • grails-app/controller/TripController.groovy
class TripController {
    def scaffold = Trip
}
  • grails-app/controller/FlightController.groovy
class FlightController {
    def scaffold = Flight
}
  • grails-app/controller/AirlineController.groovy
class AirlineController {
    def scaffold = Airline
}
  • grails-app/controller/AirportController.groovy
class AirportController {
    def scaffold = Airport
}

scaffoldのためのドメインクラスの調整

scaffoldでCRUD機能のコントローラ処理、ビュー処理を自動生成する際、項目の表示順番や、関連するドメインクラスのインスタンスの表示をIDから名前に変更するといった調整を行います。

Airline.groovy
class Airline {

    static constraints = {
        name()
        iata()
        frequencyFlyer()
    }

    static hasMany = [flights:Flight]
    String name
    String iata
    String frequencyFlyer

    String toString() {
        return iata + " - " + name
    }
}
Airport.groovy
class Airport {

    static constraints = {
        name()
        iata()
        city()
        state()
        country()
    }

    static hasMany = [flights:Flight]
    static mappedBy = [flights:"departureAirport"]

    String name
    String iata
    String city
    String state
    String country

    String toString() {
        return iata + " - " + name
    }
}
Flight.groovy
class Flight {

    static constraints = {
        flightNumber()
        airline()
        departureDate()
        departureAirport()
        arrivalDate()
        arrivalAirport()
        trip()
    }

    static belongsTo = [trip:Trip, airline:Airline]
    String flightNumber
    Date departureDate
    Airport departureAirport
    Date arrivalDate
    Airport arrivalAirport
}
Trip.groovy
class Trip {

    static constraints = {
        name()
    }

    static hasMany = [flights:Flight]
    String name

    String toString() {
        return name
    }
}

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

記事の主題であるAjaxの導入がここから始まります。

Web画面のドロップダウンリストの問題

Flightの作成画面で、Airportの選択肢として全空港の一覧(世界中では数百)がドロップダウンリストに表示されますが、ドロップダウンリストを使う場合、せいぜい十数個までが使い勝手の許容範囲です。

この記事の主張は確かにうなずけます。よくあるWeb画面で、年月日の選択で日付が1から31までのドロップダウンリストになっているものがありますが、使いづらいと毎回思います。31個でも使いにくいのが数十、数百もあっては大変です。

この問題を、記事ではAjaxを用い、ドロップダウンリストからテキストフィールドに変更し、IATAコードで入力するように対処します。
Ajaxを用いるためには、サーバー側でAjaxリクエストを受け付けるようAirportコントローラの修正とAjaxリクエストを行うようFlightビューを修正します。

AirportController.groovyの修正

Ajaxリクエストを受け付けるために、JavaScriptがデータを扱いやすいJSON(JavaScript Object Notation)形式で返すようにします。
具体的には、import文の追加と、getJsonメソッドを追加します。

  • import文の追加
import grails.converters.*
  • getJsonメソッドの追加
    def getJson = {
        def airport = Airport.findByIata(params.iata)
        if (!airport) {
            airport = new Airport(iata:params.iata, name:"Not found")
        }
        render airport as JSON
    }

ブラウザから、JsonリクエストをURLで記述して表示します。なお、記事の記述ではURLが間違っています。tripではなくtrip-plannerです。iata=の後には、データベースに作成済みのAirportのiataを指定します。

http://localhost:8080/trip-planner/airport/getJson?iata=hnd

ブラウザ(FirefoxIE)でこのURLを開くとファイルを表示するアプリケーションの選択かダウンロードするかのダイアログが表示されます。いったんファイルに保存してUTF-8が表示できるエディタで内容を確認します。

以下、確認例です。

{"class":"Airport","id":"17","city":"東京","country":"日本","flights":[],"iata":"HND","name":"東京国際空港","state":"東京都"}

curlコマンドが利用可能であれば、簡単です。なければ、wgetコマンドでファイルに保存した内容を見る方法があります。

~$ curl http://localhost:8080/trip-planner/airport/getJson?iata=hnd
{"class":"Airport","id":"17","city":"東京","country":"日本","flights":[],"iata":"HND","name":"東京国際空港","state":"東京都"}
~$
Flightのビューの修正

ビューは現在scaffoldによる動的生成となっていますが、修正を加えるためにはビューをGSPファイルとして事前に生成します。

trip-planner$ grails generate-views Flight
  :
  • grails-app/views/flight/create.gspの修正

エラー発生

Flightのビュー(grails-app/views/flight/create.gsp)を修正したところ、次のエラーが発生しました。

Error 500: Error processing GroovyPageView: Grails tag [g:form] was not closed
Servlet: grails
URI: /trip-planner/grails/flight/create.dispatch
Exception Message: Grails tag [g:form] was not closed
Caused by: Error processing GroovyPageView: Grails tag [g:form] was not closed
Class: /WEB-INF/grails-app/views/flight/create.gsp
At Line: [0] 

原因究明はこれから・・・