本文へジャンプ

Grails最初の一歩

データベース自動生成&ScaffoldによるCRUD機能動的生成

まず、Grailsのもっとも得意なパターンである、データベース自動生成とScaffoldを使ったCRUD機能動的生成によるWebアプリケーション作成を行います。
Grailsでは、ドメインクラスをGroovyで定義することにより、データベースの自動生成とデータベースとのデータ読み書き処理を自動生成します。また、Scaffoldにより、データベースに対するCRUD機能を動的生成します。
最初の一歩としてアプリケーションlostfleetを作成します。

アプリケーション"lostfleet"

アプリケーション"lostfleet"は、とあるSF作品に登場する宇宙艦船と艦長の情報を管理するものとして作成します。艦船名、艦船種類、艦長のデータから構成します。

最初の枠組みを作成する

アプリケーションを作成する作業用ディレクトリを作成します。
~$ mkdir mygrails
~$ cd mygrails
mygrails$ 
作業用ディレクトリの下にカレントディレクトリを移動した後、最初の一歩として作成するアプリケーションの枠組みを、grailsコマンドのサブコマンドcreate-appを実行して作成します。
mygrails$ grails create-app lostfleet
Welcome to Grails 1.2.0 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails/grails

Base Directory: /home/torutk/mygrails
Resolving dependencies...
Dependencies resolved in 1240ms.
Running script /opt/grails/grails/scripts/CreateApp_.groovy
Environment set to development
    :(中略)
Executing tomcat-1.2.0 plugin post-install script ...
Plugin tomcat-1.2.0 installed
Plug-in provides the following new scripts:
------------------------------------------
grails tomcat
Found events script in plugin tomcat
Created Grails Application at /home/torutk/mygrails/lostfleet
mygrails$ 
生成されるディレクトリ(最上位)の中身は以下です。
mygrails$ ls lostfleet
application.properties
  grails-app  lib  scripts  src  target  test  web-app
mygrails$
アプリケーションの内容はまだ作成していませんが、ここでアプリケーションを実行します†1
mygrails$ cd lostfleet
lostfleet$ grails -Dserver.port=8087 run-app
Welcome to Grails 1.2.0 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails/grails

Base Directory: /home/torutk/mygrails/lostfleet
Resolving dependencies...
Dependencies resolved in 1428ms.
Running script /opt/grails/grails/scripts/RunApp.groovy
Environment set to development
Running Grails application..
Server running. Browse to http://localhost:8087/lostfleet
最後の行にある、Server running. が出れば正常に起動しています。WebブラウザからこのURLにアクセスします。

Grailsのデフォルト画面が表示されます。右側「Available Controllers:」の下が空白ですが、Grailsではアプリケーションを作成するとき、ここに作成したコントローラ名が列挙されるようになります。

Grailsアプリケーションの基本構造

Grailsで動くアプリケーションの基本構造は、ドメインクラス、コントローラクラス、ビューの3つから構成されます。また、共通ロジックを記述するサービスクラスもあります。
構成要素 記述方法 備考
ドメインクラス Groovyのクラス データベースの定義
コントローラクラス Groovyのクラス Webアプリケーションの制御を記述する
ビュー GSP 画面を定義する
サービスクラス Groovyのクラス 共通のロジック等を定義する

Scaffold機能

Scaffoldと呼ばれる、データベースのCRUD機能を動的生成するGrailsのフレームワークを利用すると、アプリケーションとして最低限プログラミングするのは、アプリケーションで使用するデータを定義するドメインクラスと、Scaffoldの使用を宣言するコントローラクラスの2つとなります。

ドメインクラスの作成

アプリケーションのドメイン情報として、艦船(Ship)、艦種(ShipSpec)、艦長(Captain)の3つのクラスを作成します。パッケージ名は省略することも可能ですが、原則アプリケーションにちなんでパッケージ名を付けます。ここではパッケージ名fleetを使用します。
まず、Shipクラスをドメインクラスとして作成します。ドメインクラスを作成するときは、grailsコマンドのサブコマンドcreate-domain-classを実行します。
lostfleet$ grails create-domain-class fleet.Ship
Welcome to Grails 1.2.0 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails/grails

Base Directory: /home/torutk/mygrails/lostfleet
Resolving dependencies...
Dependencies resolved in 1369ms.
Running script /opt/grails/grails/scripts/CreateDomainClass.groovy
Environment set to development
    [mkdir] Created dir: /home/torutk/mygrails/lostfleet/grails-app/domain/fleet
Created DomainClass for Ship
    [mkdir] Created dir: /home/torutk/mygrails/lostfleet/test/unit/fleet
Created Tests for Ship
lostfleet$ 
自動生成された、grails-app/domain/fleet/Ship.groovyは以下です。
package fleet

class Ship {

    static constraints = {
    }
}
このドメインクラスに、プロパティとして、艦名(name)、艦種(spec)、艦長(captain)を追記します。
package fleet

class Ship {

    static constraints = {
    }

    String name
    ShipSpec spec
    Captain captain
}
同様に、艦種(ShipSpec)、艦長(Captain)のドメインクラスを作成します。
lostfleet$ grails create-domain-class fleet.ShipSpec
    : (省略)
lostfleet$ grails create-domain-class fleet.Captain
    : (省略)
lostfleet$
ドメインクラス艦種
package fleet

class ShipSpec {

    static constraints = {
    }

    String name
}
ドメインクラス艦長
package fleet

class Captain {

    static constraints = {
    }

    String name
}

コントローラクラスの作成

ドメインクラスそれぞれと対になるコントローラクラスを作成します。grailsコマンドのサブコマンドcreate-controllerにドメインクラス名を指定して実行します。

ドメインクラス艦船のコントローラクラス"ShipController"

lostfleet$ grails create-controller fleet.Ship
Welcome to Grails 1.2.0 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails/grails

Base Directory: /home/torutk/mygrails/lostfleet
Resolving dependencies...
Dependencies resolved in 1468ms.
Running script /opt/grails/grails/scripts/CreateController.groovy
Environment set to development
    [mkdir] Created dir: /home/torutk/mygrails/lostfleet/grails-app/controllers/fleet
Created Controller for Ship
    [mkdir] Created dir: /home/torutk/mygrails/lostfleet/grails-app/views/ship
Created Tests for Ship
lostfleet$
自動生成された、grails-app/controllers/fleet/ShipController.groovyは以下です。
package fleet

class ShipController {

    def index = { }
}
ScaffoldによるCRUD機能動的生成を行うよう書き換えます†2
package fleet

class ShipController {
    def scaffold = true
}
ドメインクラス艦種および艦長のコントローラクラスを同様に作成します。
lostfleet$ grails create-controller fleet.ShipSpec
    : (省略)
lostfleet$ grails create-controller fleet.Captain
    : (省略)
lostfleet$
同様にScaffold宣言に書き換えます。
艦種コントローラクラス
package fleet

class ShipSpecController {

    def scaffold = true
}

艦長コントローラクラス
package fleet

class CaptainController {

    def scaffold = true
}

実行

ここで、再度 grailsを実行します。なお、実行中のgrailsを止めるには、実行したコンソールからCtrl+Cを入力すれば、シャットダウン処理が走ります。


Available Controllers: の下に、作成したコントローラクラスが3つ列挙されています。
ここで、fleet.ShipSpecControllerをクリックすると、ShipSpecの一覧画面に飛びます。

まだ、何も作成していないのでリストは空欄です。そこで、「New ShipSpec」をクリックして新しいShipSpecデータの作成を行います。

Name欄に入力し、Createをクリックすると、ShipSpecのデータが1件作成できます。

ShipSpec Listをクリックして、再びShipSpecの一覧画面を表示させると、作成したShipSpecのデータが1件表示されます。

同じように、Homeに戻って、ここで、fleet.CaptainControllerをクリックすると、Captainの一覧画面に飛びます。New Captainをたどって、Captain作成画面で1件のデータを作成します。

これで、Shipデータを作成するために必要なShipSpecとCaptainが1件ずつ作成できたので、いよいよShipデータを作成します。Homeに戻って、今度は、fleet.ShipControllerをクリックします。まだデータは空なので、見出し行にプロパティ名が並んだ表示となっています†4

ここで、NewShipをクリックします。

ドメインクラスShipは、プロパティにShipSpec、Captainを持ちますが、新規作成画面では、この値はドロップダウンリストで選択する形式になっており、現在作成されているShipSpecおよびCaptainのデータが候補としてドロップダウンリストに登場します。しかし、このドロップダウンリストの表示はデフォルトではクラス名とIDとなっています†3。最初の一歩としてはここはそのまま艦船名を入力してShipデータを1件作成します。

Ship Listをクリックすると、Shipデータの一覧に1件データが追加されているのが分かります。


注記に記載した日本語化、他のドメインクラスをプロパティとしたときの自然な名前による表示等の設定をしたときのShip一覧の画面は以下となります。



†1 デフォルトのデータベース

左のコマンドで実行すると、実行環境が開発モードとなり、作成したデータは毎回クリアされます。実行環境のモードとデータベースの使い方は、grails-app/conf/Datasource.groovyファイルに定義されます。デフォルトで生成されるDatasource.groovyの実行環境モードとデータベースの定義は以下です。
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"
    }
  }
実行環境には、開発モード/試験モード/製品モードの3つがあります。development { ... } が開発モードの定義、test { ... } が試験モード、production { ... } が製品モードの定義を記述しています。HSQLDBを使用し、開発モードではデータをメモリに保存し、実行する度にデータベースを破棄して新規生成(create-drop)します。一方、製品モードではデータをファイルに保存し、実行するときには前のデータを破棄しません(update)。
製品モードで実行するときは、
$ grails -Dserver.port=8087 prod run-app
と明示的に指定します。デフォルトでは開発モードとして実行されます。




†2 scaffold宣言

scaffold宣言をコントローラクラスに記述するとき、デフォルトの記述であるdef index = {} を残したままにすると、エラーになってしまいます。従って、デフォルトで生成されたdef index = {} は削除します。
また、def scaffold = true とした場合、scaffold処理は記述したコントローラクラスの名前と規約上対応するドメインクラスに対して行われます。コントローラクラスがfleet.ShipControllerならscaffold処理対象は、fleet.Shipが適用されます。もし、別な名前のドメインクラスであれば、true の代わりにドメインクラス名を記述します。




†3 クラス名:ID を分かりやすい名前にする

ドメインクラスのプロパティに他のドメインクラスを指定したとき、デフォルトのクラス名:IDと表記されます。データベース構造としては、ドメインクラス毎にテーブルが作成されるため、別ドメインクラスのプロパティの値は別テーブルのレコードを参照するための外部キーとなると推測できます。
しかし、アプリケーションとし人間がデータを作成する際はIDではなく、分かりやすい名前で表示されてほしいところです。
分かりやすい名称にする方法の1つは、プロパティに指定したドメインクラスにtoStringメソッドをオーバーライドすることです。
例えば、ShipSpecクラスの定義に以下を追加します。
String toString() {
    return name
}




†4 表示列の並び替え

Scaffoldで動的生成される画面では、Shipの場合、列の並び順がとして先にNameが来て欲しいところが、Specが先になってしまっています。
順番を制御する方法の1つに、ドメインクラスのstatic constraintsに並び順を指定するものがあります。ドメインクラスShipのソースファイルgrails-app/domain/fleet/Ship.groovyのstatic constraints行を以下に修正します。
static constraints = {
    name()
    spec()
    captain()
}




コラム1 日本語化

データを11件以上作成してみた方は気付いたと思いますが、ここまで各画面は基本は英語で表示されてたのに、ある部分だけ日本語が表示されています。Grailsでは、国際化対応が標準で用意されています。Javaのリソースバンドル機能を使っているので、プロパティファイルを編集することで日本語化が実現できます。
grails-app/i18nディレクトリの下に、messages_ja.properties というファイルが存在します。これが日本語ロケールで実行するときに参照される日本語情報となります。ここに定義がない場合、デフォルトの表示(英語)となる仕組みです。そこで、messages_ja.propertiesに定義を追加します。

  • ドメインクラス(データ)名の日本語化
ship.label=艦船
shipSpec.label=艦種
captain.label=艦長
ドメインクラス名の先頭を小文字とし、.labelを追加したキー名となるようです。
  • メニュー・ボタンの日本語化
これらはデフォルト(英語)のプロパティファイル messages.propertiesに以下のように定義があります。
default.list.label={0} List
default.add.label=Add {0}
default.new.label=New {0}
default.create.label=Create {0}
default.show.label=Show {0}
default.edit.label=Edit {0

default.button.create.label=Create
default.button.edit.label=Edit
default.button.update.label=Update
default.button.delete.label=Delete
default.button.delete.confirm.message=Are you sure?
これらプロパティの日本語定義をmessages_ja.propertiesに追加します。
default.list.label={0}一覧
default.add.label={0}の追加
default.new.label={0}の新規作成
default.create.label={0}の作成
default.show.label={0}の詳細
default.edit.label={0}の変更

default.button.create.label=作成
default.button.edit.label=変更
default.button.update.label=更新
default.button.delete.label=削除
default.button.delete.confirm.message=本当によろしいですか?
  • ドメインクラス(データ)のプロパティ(カラム)名の日本語化
データの一覧、詳細表示で、プロパティの名称は、ドメインクラスに定義したプロパティの変数名となっています。これも、国際化対応されているので、日本語で表示させることができます。
ドメインクラス Ship の場合
ship.id.label=ID
ship.name.label=艦船名
ship.spec.label=艦種
ship.captain.label=艦長 
ドメインクラス名の先頭を小文字にした名前に、.プロパティの変数名.labelを追加したものがキー名となります。
ドメインクラス ShipSpecの場合
shipSpec.id.label=ID
shipSpec.name.label=艦種名
ドメインクラス Captainの場合
captain.id.label=ID
captain.name.label=名前




コラム2 データベース
Grailsに標準搭載されるHSQLDBは、OpenOfficeのDBエンジンとしても採用されているオープンソースのDBエンジンです。
デフォルトでは、製品モードで実行したときに、ファイルにデータを保存しますが、それ以外のモードではメモリ上にデータを保持するのでプロセスを終了すると保存したデータは消えてしまいます。
製品モードのとき、データは、prodDb.scriptsというファイルに保存されます。テキストファイルで、中身を見るとSQL文でテーブル作成とデータのインサートが記述されています。

データベースは、Grails標準搭載のHSQLDB以外にも、grailsが使用しているORマッピング・フレームワークのhibernateが対応している幾種類かのDBエンジン(DBMS)を使用できます。対応するDBMS一覧はhibernateのドキュメントに記載されています。




コラム3 タイトルロゴ

各ページの頭には、grailsホームページへのリンクを持つGRAILSのロゴが表示されています。これは、grails-app/views/layouts/main.gspに記述されているので、これを変更することで任意の表示をすることができます。デフォルトのタイトルロゴの定義は以下となっています。
<div id="grailsLogo" class="logo">
  <a href="http://grails.org">
    <img src="${resource(dir:'images',file:'grails_logo.png')}" alt="Grails" border="0" />
  </a>
</div>
これを、例えば以下のように変更します。画像ファイルの置き場所は、web-app/images/の下です。
<div id="lostfleetLogo" class="logo">
  <a href="/lostfleet">
    <img src="${resource(dir:'images',file:'lostfleet_logo.png')}" alt="Lost Fleet" border="0" />
  </a>
</div>




コラム4 ドメインクラス間の関連(1:1)

lostfleetアプリケーションでは、艦船(Ship)と艦長(Captain)は1:1の関係としてモデリングしています。ただし関係の方向は艦船から艦長の片方向です。ドメインクラスのソースコードを見れば、ShipクラスはCaptainを属性(フィールド)に定義しているが、CaptaionクラスはShipへの関連はまったく記述がないことが分かります。
双方向の関係とするにはどうしたらよいでしょうか。オブジェクト指向プログラミングでは、オブジェクトの関連は片方向にするのが常套です。しかし、データベースアプリケーションでは、互いに関連を持つ情報を表示させた際、どちらからも関連先が見たいところです。艦長一覧・詳細を見たときに、どの艦船か見えないのはアプリケーション機能としては片手落ちです。
ここで、Grailsが裏で生成するデータベース構造を模式的に表現すると以下になります。
TABLE SHIP
+----------+--------------+
|ID        |BIGINT        |
|VERSION   |BIGINT        |
|NAME      |VARCHAR(255)  | String型のプロパティはそのままテーブルに値が保持される
|CAPTAIN_ID|BIGINT        |  他のドメインクラス型のプロパティは、そのIDがテーブルに保持する
|SPEC_ID   |BIGINT        |
+----------+--------------+
TABLE CAPTAIN
+----------+--------------+
|ID        |BIGINT        |
|VERSION   |BIGINT        |
|NAME      |VARCHAR(255)  |
+----------+--------------+
ここで、ドメインクラスCaptainにもShipを属性として定義すると、TABLE CAPTAIONにSHIP_IDが追加されてしまいます。すると、データの作成にミスがあると、Shipの属性で指すCaptainとCaptainの属性で指すShipが不一致となり得ます。これはまずいので、双方向の関係を定義するときは、もう一方のドメインクラスには普通の属性ではなく、static hasOne という定義を使います。
Captainドメインクラスに、Shipへの1:1関連を通常の属性ではなくstatic hasOneによる定義で追記したコードを以下に示します。
package fleet

class Captain {

    static constraints = {
        name()
        ship()
    }

    static hasOne = [ ship: Ship ]

    String name
    
    String toString() {
        return name
    }
}
こうすることで、1:1の双方向関係において、整合性を維持することが容易になります。




コラム5 検索機能