2010年3月4日木曜日

Grails on GAE/J - NetBeansを使って

ようやく動作確認が出来た。
Grails AppEngine plugin の英語の説明をよ〜く読めば出来るのだが、、正しく手順を踏むことが重要。

Grails AppEngine plugin の説明
http://www.grails.org/plugin/app-engine

これでようやく、Grails のアプリケーションを GAE/J で動かせる。



環境
Grails  1.2.0
Google App Engine SDK for Java  1.3.1
Grails AppEngine plugin  0.8.8
GORM-JPA Plugin  0.7

OS は Mac OS X Snow Leopard なので、JDK6
IDE は NetBeans 6.8 ファイルの多いフレームワークの開発に便利だから。

Grails プロジェクトを作成する前に、Grails、Google App Engine SDK for Java をパスの通りやすいところにインストールしておく。NetBeans で Grails を使用出来るようにしておく。



新規 Grails アプリケーションを作成する。 プロジェクト名は何でも良い。



・Grails AppEngine plugin の追加

プロジェクトを右クリックして、「Grails コマンド実行」を選択。



「一致するタスク」から "install-plugin" を選択し、「パラメーター」に "app-engine"と入力し、実行。

Grails コマンド : grails install-plugin app-engine



出力ウィンドウで、オブジェクト永続化に JPA か JDO のどちらを使用したいか聞いてくるので、"jpa⏎" と入力。(jdoも可能)



インストールが出来ると、出力ウィンドウに以下のように環境変数または grails-app/conf/BuildConfig.groovy ファイルに Google AppEngine SDK のパスを指定するように促される。これを行わないとGrailsコマンドを使用出来なくなるので注意。



No Google AppEngine SDK specified. Either set APPENGINE_HOME in your environment or specify google.appengine.sdk in your grails-app/conf/BuildConfig.groovy file

IDE での開発なので環境変数でのパスの指定は面倒なので、BuildConfig.groovy ファイルに以下のように1行追加する。

google.appengine.sdk = "Google AppEngine SDK のパス"


・GORM-JPA Plugin の追加

「Grails コマンド実行」で "install-plugin" を選んで、パラメーターに "gorm-jpa" と入力して実行。

Grails コマンド : grails install-plugin gorm-jpa




・Tomcat プラグインの削除

アプリケーションの実行は Google App Engine SDK の Jetty 上で行うので、Tomcat プラグインは不要。

Grails コマンド : grails uninstall-plugin tomcat




・ローカルホストで起動してみる

NetBeans の実行ボタンで起動出来る。

Grails コマンド : grails app-run



サーバーの起動まで少々時間がかかるが、出力ウィンドウに "[java] The server is running at http://localhost:8080/" と表示されれば、WEBブラウザーで確認してみる。




・BigTable での CRUD

GAE/J での BigTable への永続化は基本的にJDOを使用するが、Grails AppEngine plugin のインストール時に JPA を選択を選択し、GORM-JPA Plugin をインストールしてあるので、JPA での BigTable への永続化が可能になっている。
JDO ではなく JPA を利用する利点は、通常の Grails アプリケーション同様に GORM を利用できる点。
JDOの場合は PersistenceManager を介して永続化するので、コントローラー、サービスでのエンティティーの永続化の部分のコードがかなり異なってしまう。

CodeZine のチュートリアルをもとに BigTable での CRUD を試してみた。

CodeZine のチュートリアル - Grailsでデータベースを利用しよう
http://codezine.jp/article/detail/3868


・ドメインクラスの作成

Grails コマンドでドメインクラスを作成すれば良いが、Grails on GAE/J ではドメインクラスは何らかのパッケージに属していなければいけない。
"grails create-domain パッケージ名.クラス名" とする。

NetBeans ではGrails コマンドをわざわざ入力しなくても、プロジェクトウィンドウの 「ドメインクラス」 フォルダーを右クリックして 「新規」->「Grailsドメインクラス」を選び、ダイアログ入力してドメインクラスを作成出来る。



ドメインクラス作成のダイアログでは、パッケージを必ず入力する。
Grails on GAE/J の制約でもあるが、NetBeans では Grails コマンドでパッケージの指定を促す警告に応答出来ないで固まってしまうので、パッケージを必ず入力する。

Grails コマンド : grails create-domain パッケージ名.Board



生成されたドメインクラスを確認すると、GORM のドメインクラスではなく、JPA のエンティティークラスとして生成されている。
id 以外のプロパティーは GORM のドメインクラス同様に追加すれば良い。
JPA のエンティティークラスだが、Groovy なので setter/getter メソッドの定義は不要。

package notexytest



import javax.persistence.*;
// import com.google.appengine.api.datastore.Key;

@Entity
class Board implements Serializable {

    @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 Long id

 String name
 String title
 String content

    static constraints = {
     id visible:false
 }
}

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

通常の Grails アプリケーションでは、コントローラークラスを作成し、コントローラークラスに scaffolding の指定をすれば良いのだが、これはうまく出来ない。うまく出来ない理由はあとで説明する。
CRUD のためのコントローラー、ビューを作成するため Grails コマンドの "generate-all" を実行する。パラメーターにはパッケージを含めたドメインクラス名を指定。

Grails コマンド : grails generate-all パッケージ名.Board



作成されたコントローラークラスを確認すると、通常の GORM の場合とほぼ同じコードが生成されているが、少し異なる点があり、エンティティーオブジェクトの save, delete が、ドメインクラスの withTransaction クロージャー内で行われている。
JPA でのエンティティーオブジェクトの永続化はトランザクション内で行わないといけないらしい。コントローラーで scaffolding 出来ない理由はここにあった。

save メソッド
    def save = {
        def boardInstance = new Board(params)
        Board.withTransaction {
             if(boardInstance.save(flush:true)) {
                 flash.message = "Board ${boardInstance.id} created"
                 redirect(action:show,id:boardInstance.id)
             }
             else {
                 render(view:'create',model:[boardInstance:boardInstance])
             }
        }
    }

save メソッドでは Grails AppEngine plugin がトランザクション処理のためのコードを生成してくれるのだが、なぜか、delete メソッドではトランザクションしていないので、以下のように変更した。

    def delete = {
        def boardInstance = Board.get( params.id )
        if(boardInstance) {
            Board.withTransaction {
                try {
                    boardInstance.delete(flush:true)
                    flash.message = "Board ${params.id} deleted"
                    redirect(action:list)
                }
                catch(org.springframework.dao.DataIntegrityViolationException e) {
                    flash.message = "Board ${params.id} could not be deleted"
                    redirect(action:show,id:params.id)
                }
            }
        }
        else {
            flash.message = "Board not found with id ${params.id}"
            redirect(action:list)
        }
    }


・ローカルで実行
コントローラーでメッセージを flash しているので、「Webアプリケーション」の "WEB-INF/appengine-web.xml" (プロジェクト/web-app/WEB-INF/appengine-web.xml)を編集して、"true" のコメントを外して、セッションを有効にする。
実行ボタンで実行し、実際に CRUD 操作してみる。






・GAE へのデプロイ

デプロイの前に GAE にデプロイ対象のアプリケーションを登録しておく。



「構成」の "Config.groovy" (プロジェクト/grails-app/conf/Config.groovy)に次のように1行追加し、アプリケーション名の指定をする。

google.appengine.application="アプリケーション名"



次に、Grailsコマンドでアプリケーションのバージョンを指定する。GAE では整数値でないといけない。

Grailsコマンド : grails set-version 1



さらに、Grailsコマンドでパッケージングする。

Grailsコマンド : grails app-engine package



それから実際にデプロイするのだが、Eclipse のように GAE プラグインが NetBeans には無いので、デプロイはターミナルでコマンドを実行する必要がある。

$ cd [プロジェクトディレクトリー]
$ [appengine-java-sdk]/bin/appcfg.sh update ./target/war

途中、Gmail のアドレス、パスワードを入力する必要がある。



ローカル同様に動作た。
気になるのは、flash したメッセージがページを遷移した後もずっと表示されてしまう。セッションのスコープの扱いがおかしいようだ。




・アプリケーションの停止と起動の問題

GAE/J のアプリケーションはしばらくアクセスが無いと停止してしまう。
Grails はそれなりに重厚なフレームワークであるので、再度アプリケーションを起動するのに30秒以上かかってしまうことがある。ということは GAE の制約でクリエストに対してタイムアウトしてしまい、エラー500が表示されてしまう。
再度ブラウザでリロードすれば問題無く表示されてりするのだが、それでは実用にならない。

そこで、cron 機能を使い、アプリケーション内の何らかのURLに定期的にリクエストを出して、アプリケーションが起動し続けるようにした。

Recashコントローラーを作成する。/recash にアクセスがあったら、"done."と言う文字列だけをレスポンスする。



class RecashController {
    def index = {
        render "done."
    }
}

「Webアプリケーション」の "WEB-INF" (web-app/WEB-INF)以下に "cron.xml" を作成し、以下のように記述する。




試してみたところ、2分間隔ではアプリケーションが停止し、cron 実行が頻繁にタイムアウトすることがあった。
1分間隔では実行がタイムアウトが1日数回で済んだ。
ただ、どのタイミングでアプリケーションが停止するかは不明。
1分間隔でアプリケーションを生かし続けても1日あたりのCPU時間は上限の1%未満で済んでいる。

1分間隔で cron 実行してもCPU時間をほとんど消費しない

再起動に伴う cron 実行の失敗も1日数回程度




cron.xml の記述が出来たら "grails app-engine package" してデプロイする。

0 件のコメント:

コメントを投稿