torutkのブログ

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

「Grailsをマスターする:Grailsのイベント・モデル」を読みながら動かしてみる

Grails をマスターする: Grails のイベント・モデルの記事に沿ってGrailsの習得を進めます。

id:torutk:20090726の続きです。

ビルド・イベント

記事の記述にある、GRAILS_HOME/scripts/Clean.groovyを実際に見ると、内容が違っています。Grails 1.1で変更になったと思われます。

  • grails-1.1.1/scripts/Clean.groovy の内容
includeTargets << grailsScript("_GrailsClean")

setDefaultTarget("cleanAll")

grailsScript("_GrailsClean")は、別なスクリプトファイルを呼び出すのだろうと想像し、同ディレクトリを見ると、_GrailsClean.groovyというファイルがありました。その中には、

target ( cleanAll: "Cleans a Grails project" ) {
	clean()
	cleanTestReports()
}

という内容があり、この部分は記事にある記述とほぼ同じです。記事では、続いてcleanターゲットを追いかけているので、同じ_GrailsClean.groovyファイルに記述されているcleanターゲットを見てみると、

target ( clean: "Implementation of clean" ) {
    depends(cleanCompiledSources, cleanWarFile)
}

となっています。記事とは異なり、eventの呼び出しがありません。探してみると、_GrailsClean.groovyの最初の方に以下の記述があります。

includeTargets << grailsScript("_GrailsEvents")

これが、eventの呼び出しに関係するかは追いきれていません。

なお、プロジェクトディレクトリのscripts/Events.groovyを作成して記事にあるコードを記述すると、イベント時に呼び出しが行われるのはgrails-1.1.1でも同じでした。

イベントに関しては、記事が書かれたGrails-1.0と現在のGrails-1.1との間で随分と違いがあるようです。

ブートストラップイベント

記事のリストとgrails-1.1.1で生成されるgrails-app/conf/BootStrap.groovyの内容が少し変わっています。

class BootStrap {

     def init = { servletContext ->
     }
     def destroy = {
     }
} 

initメソッドに引数が増えています。

記事に従って、この2つのメソッドにprintln文を追加します。

class BootStrap {

     def init = { servletContext ->
         println "### 開始します"
     }
     def destroy = {
         println "### 終了します"
     }

} 

記事に従って、対話モードでサーバを起動・終了します。

trip-davisworld$ grails interactive
  :(中略)
--------------------------------------------------------
Interactive mode ready, type your command name in to continue (hit ENTER to run the last command):

コマンドを入力するよう促されるので、run-appと入力します。

run-app
Running script /home/toru/grails-1.1.1/scripts/RunApp.groovy
Environment set to development
Running Grails application..
### 開始します
Server running. Browse to http://localhost:8080/trip-davisworld
--------------------------------------------------------
Application loaded in interactive mode, type 'exit' to shutdown server or your command name in to continue (hit ENTER to run the last command):

サーバが起動する際、先ほど追加したprintln文による表示が行われました。
次にサーバを終了します。exitと入力します。

exit
Stopping Grails server...
### 終了します
--------------------------------------------------------
Command [RunApp completed in 36455ms
--------------------------------------------------------
Interactive mode ready, type your command name in to continue (hit ENTER to run the last command):

対話モードによる起動・終了は、JavaVMを起動しっぱなしにするので、コマンドからgrails run-appでサーバを起動するより早く上がります。開発時には、頻繁にサーバを再起動するので、この方法を使うとストレスが少なくなります。

grails interactiveでの疑問
  • run-appではdevelopment modeになるが、production modeで実行するには?

ブートストラップ中に処理を入れる

Hotelクラスを作成して起動時にデータベースレコードを挿入するという流れですが、前回までのレガシーデータベースマッピングではなく、Grailsドメインクラスから自動生成する方法を使うので、いったん新しいプロジェクトとして起こします。

trip-plannerアプリケーションの退避と新規生成

今あるtrip-plannerアプリケーションはcleanコマンドで生成物を削除してから、ディレクトリ名を変えておき、新たにtrip-plannerアプリケーションを生成します。

work$ cd trip-planner
trip-planner$ grails clean
  :
trip-planner$ cd ..
work$ mv trip-planner trip-planner.bak
work$ grails create-app trip-planner
  :(中略)
Installing plug-in hibernate-1.1.1
You currently already have a version of the plugin installed [hibernate-1.1.1]. Do you want to upgrade this version? (y, n)
y
   [delete] Deleting directory /home/torutk/.grails/1.1.1/projects/trip-planner/plugins/hibernate-1.1.1
    [mkdir] Created dir: /home/torutk/.grails/1.1.1/projects/trip-planner/plugins/hibernate-1.1.1
    [unzip] Expanding: /home/torutk/.grails/1.1.1/plugins/grails-hibernate-1.1.1.zip
 into /home/torutk/.grails/1.1.1/projects/trip-planner/plugins/hibernate-1.1.1
Executing hibernate-1.1.1 plugin post-install script ...
Plugin hibernate-1.1.1 installed
Created Grails Application at /home/torutk/work/trip-planner
work$ cd trip-planner
trip-planner$

ここで、過去に同じアプリケーション名を作成していると、上記にあるように、[hibernate-1.1.1]プラグインを上書きするか聞いてきます。とりあえずyを入力してみると、$HOME/.grailsの中にプロジェクト名のディレクトリが作られて、その中に何かが展開されているようです。

プロジェクトディレクトリ以外にも必要なものがあると、バージョン管理等で問題が出るので、ためしに作成したディレクトリを別な場所にコピーし、別ユーザで実行してみました。すると、そのユーザのホームディレクトリ下にhibernateプラグインが見つからないと警告メッセージが出て、ネットワークからダウンロードして展開する動きをしました。問題はないと思われます。

grailsがファイル生成時にSubversionへの追加処理を行う

記事にあるように、プロジェクトディレクトリ下のscripts/Events.groovyにCreatedFileイベントハンドラを記述しておきます。

eventCreatedFile = { fileName ->
    "svn add ${fileName}".execute()
    println "### ${fileName} was just added to subversion."
}
プロジェクトディレクトリのSubversionへの登録とチェックアウト

まずgrailsで生成したディレクトリ一式をSubversionへ登録します。
以下の例は、同一マシン上にあらかじめsvnserveを起動してリポジトリを作成した場合の例です。

work$ svn import trip-planner svn://localhost/path/to/snvrep/trunk/trip-planner
work$

importしたディレクトリは削除か別名に退避しておき、あらたにリポジトリからチェックアウトします。

work$ svn co svn://localhost/path/to/svnrep/trunk/trip-planner trip-planner
work$
Hotelドメイン・クラスの生成
trip-planner$ grails create-domain-class Hotel
  :(中略)
### /home/torutk/work/trip-planner/grails-app/domain/Hotel.groovy was just added to subversion.
Created DomainClass for Hotel
### /home/torutk/work/trip-planner/test/unit/HotelTests.groovy was just added to subversion.
Created Tests for Hotel
trip-planner$

さきに作成したイベントハンドラが起動しています。

trip-planner$ svn status
A       test/unit/HotelTests.groovy
?       scripts/Events.groovy
A       grails-app/domain/Hotel.groovy

確かに追加(svn add)されているのが分かります。

記事に沿って、Hotelクラスにプロパティnameを追記します。

HotelControllerクラスの生成

scaffoldでHotelドメインクラスのコントローラを作成します。

DataSource.groovyのデータベースを変更

デフォルトのHSQLDBからMySQLに変更します。
また、mysqlJDBCコネクタをプロジェクトディレクトリのlib下にコピーします。
設定方法は、id:torutk:20090718の日記を参照。

grailsサーバの起動とデータベース

grailsを起動し、mysqlでデータベースを見ると、起動前にはないテーブルhotelが増えています。内容も2件のデータが入っています。

ドメインクラスのタイムスタンプ

Hotelクラスに、以下のプロパティを追加します。

Date dateCreated
Date lastUpdated

次に、scaffoldのテンプレートを修正するために、grails install-templatesを実行します。

src/templates/scaffoldingディレクトリにある、create.gspとedit.gsp
を修正します。

これで、Hotelを作成するとdateCreated, lastUpdatedにタイムスタンプが自動で登録され、Hotelを更新するとlastUpdatedにタイムスタンプが更新されるようになります。

dateCreated, lastUpdatedの名前は規約か?

確認するために、xdateCreated, xlastUpdatedとプロパティ名を修正してみました。Hotel.groovyとcreate.gsp、およびedit.gspをそれぞれ変更し実行すると、作成時にエラーとなりました。

[class Hotel]クラスのプロパティ[xdateCreated]にnullは許可されません。
[class Hotel]クラスのプロパティ[xlastUpdated]にnullは許可されません。

どうやら、この名前でないといけないようです。

タイムスタンプのカスタマイズ

dateCreated, lastUpdatedは、String型でないとエラーとなってしまいます。例えばテキストでデータベースに格納すると言った場合、記事にあるように、beforeInsert, beforeUpdateメソッドを定義するとよさそうです。

class Hotel {
    static constraints = {
        name()
        modifiedTime(nullable:true)
    }

    String name
    String modifiedTime

    def beforeInsert = {
        def time = new Date()
        modifiedTime = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(time)
    }

    def beforeUpdate = {
        def time = new Date()
        modifiedTime = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(time)
    }
}

毎回SimpleDateFormatをnewしているのはちょっといただけないですが、後日groovyでstaticフィールドの使用方法を調べて確認することにします。