はじめにPlayframeworkとGoogleAppEngineの組み合わせについて、集めた情報などをまとめます。
Playframeworkはバージョン1系と2系があり、現在は2系がメインストリームとなってしまっていますが、ここでは1系の情報を扱います。
使用バージョンは下記のとおりです。
私自身、AppEngineを使った開発は初めてで練習がてらのまとめ情報ですので的を外してるところがあるかもしれません。それも踏まえてみていただければと思います。
あと、GoogleAppEngineの登録は済んでいる前提で進めます。そのあたりは詳しく解説しているサイトがたくさんあると思いますので。
開発準備PlayframeworkでAppengine用にアプリケーションを作成する場合、gaeモジュールを使うと便利です。
AppEngineへのデプロイ、開発時のログインエミュレート機能、APIのラッパーなどが揃っています。至れり尽くせり、というほどではありませんが、初心者には心強いです。
GAEモジュールのインストール他のモジュールと同じくコマンドラインよりインストールコマンドを入力します。(バージョンを指定しない場合は推奨バージョンが選択されます)
自動的にダウンロードが開始、playディレクトリに展開、インストールされます。
GoogleAppEngineSDKのインストール必須です。
ここより、Google App Engine SDK for Java をダウンロードしてきます。任意のディレクトリに展開します。
ディレクトリに展開したら環境変数「
gae_path 」にディレクトリのパスを設定しておきます。Windowsであれば、システムのプロパティ>詳細設定>環境変数>ユーザの環境変数、で登録しておけばよいと思います。
Play SienaモジュールのインストールAppEngineで使用可能なデータベースは、Datastoreと呼ばれるKV型のデータベースです。
DatastoreはPlayframework標準添付のJPAではサポートされていません。
代わりに近い感覚でデータベースを扱えるSienaをインストールします。JPAほどではありませんがダイレクトにDatastoreを扱うのと比べて格段に楽です。たぶん。SienaはMySQLやPostgreSQLなどのデータベースに対応しているので、Sienaで作っておけば将来、別の環境で動かすとなったときの敷居が低いかもしれません。
他のモジュールと同じようにコマンドラインよりインストールします。
各モジュールのページへのリンクhttp://www.playframework.com/modules/gaehttp://www.playframework.com/modules/siena http://www.playframework.com/modules/crudsiena アプリケーション側の準備アプリケーション側では、gaeモジュールやSienaモジュールを使用することを宣言する必要があります。
dependencies.ymlファイルへ依存関係を記述します。
- play -> play [1.2.1,) - play -> siena [2.0.7,) - play -> gae [1.6.0,) 書式の詳細はこちらで。
次にdependenciesコマンドでアプリケーションの開発ディレクトリの各種設定に反映させます。
アプリケーションディレクトリは、カレントディレクトリがそのアプリケーションのディレクトリであれば省略可能です。
準備ができたら取りあえずアプリケーションを起動します。コマンドラインからでもEclipseからでも構いません。
初回の起動だけは特別で、
war/WEB-INF/appengine-web.xml というファイルの生成が行われます。ファイルが生成されないようならインストールのどこかで失敗した可能性があります。で、生成されたこのxmlファイルの
<application> タグにアプリケーションのID(Create Applicationとして「Application Identifier:」に入力したアプリケーションID文字列です)を記入しましょう。
<application><!-- Replace this with your application id from http://appengine.google.com --></application> <version>1</version> <threadsafe>true</threadsafe> </appengine-web-app> これが間違っているとデプロイがうまくいきません。
あと、Sienaモジュールを使う場合は、JPA関連の設定は無効化しておきます。
デフォルトのapplication.conf には
db=mem とだけ書かれていると思いますので、これをコメントアウトしておきます。
Sienaモジュールにはデータの更新や削除の前後イベントをフックする機能があります。
その機能を利用したい場合は、application.conf に次の設定を追加しておきます。
デプロイまずはデプロイできることを確認しましょう。
コマンドラインからデプロイコマンドを入力します。
途中でアカウントとパスワードを聞かれるので、入力する準備をしておきましょう。
フレームワークIDの指定(
--%prod というやつ)は、やっておいた方がいいというレベルです。やっておけば起動時に「強制的にprodモードにしたよ」というメッセージが出なくなります。
application.conf に次のように指定しておけばよいです。
少なくとも
OnApplication() を実行する段階では、この「強制的にprodモードにする」前の状態です。その段階ではdevモード用の設定が有効になっているため、OnApplication() 内での処理で設定を扱う場合は注意しなくてはなりません。そのような煩わしさから解放されるためにも手動でフレームワークIDを指定しておくことをお勧めします。
その他のコマンドgaeモジュールで指定できるデプロイ以外のコマンドについての説明はコレといったのが無く、長いことデプロイだけしかできないのかと思っていました。
しかし、
${play_path}/modules/gae-1.6.0/commands.py の中を見ると色々できそうです。簡単に書き出してみます。実際に試してみてないので動くかどうかまでは不明です。
ソースをよく見るとヘルプが書いてありました。抜粋しておきます。
'gae:deploy': "Deploy to Google App Engine", 'gae:update_indexes': "Updating Indexes", 'gae:vacuum_indexes': "Deleting Unused Indexes", 'gae:update_queues': "Managing Task Queues", 'gae:update_dos': "Managing DoS Protection", 'gae:update_cron': "Managing Scheduled Tasks : upload cron job specifications", 'gae:cron_info': "Managing Scheduled Tasks : verify your cron configuration", 'gae:request_logs': "Download logs from Google App Engine", } デプロイ中断からの回復デプロイ中に何らかの理由で中断してしまった場合は、AppEngine側はデプロイ中という状態にはまってしまい、そのままでは再デプロイを受け付けてくれなくなります。
このような状態になってしまったら、ロールバックコマンドを使ってデプロイ処理を巻き戻します。
appcfg.cmd で直接コマンドをたたくのに慣れないのであれば、gaeモジュールを以下のように改造する方法もあります。
${play_path}/modules/gae-1.6.0/commands.py の変更ポイント。
L18に受け付けるコマンドのリストがあるようなので、これにロールバックコマンドを追加します。
ソースの最後にロールバックコマンドに対する処理を追加します。pythonは詳しくないのですが、インデントルールが重要なようなのでそのままコピペしても怒られます。1行目は空白4個。2行目以降はタブでのインデントで動作しております。
if command == "gae:rollback":
print '~' print '~ Performing Rollback' print '~ ---------' if os.name == 'nt':
os.system('%s/bin/appcfg.cmd rollback %s' % (gae_path, war_path)) else: os.system('%s/bin/appcfg.sh rollback %s' % (gae_path, war_path)) print "~ "
print "~ Done!" print "~ " sys.exit(-1) スレッド数の設定Playframeworkでのスレッド数は特に指定しない限り、PRODモードでは利用可能なCPU数+1に設定されます。(DEVモードでは1)
AppEngineでは多くのリクエストを裁くために自動でスケールするため、利用可能なCPU数も半端ではありません。
下記のようなコードでプロセッサ数を表示させてみたところ、1337という数字が表示されました!
これを真に受けて1338スレッドをMAXとされても現実的ではありません。設定ファイルで任意の数を設定する方が良いと思います。
%prod.play.pool=3 タイムゾーンAppEngineは日本だけに提供されているわけではありませんのでタイムゾーンはUTCとなっています。
私はアプリケーションを日本時間で動かしたかったので下記のようなコードを発行してみたのですが、一時的にはJSTになるもののいつの間にか戻ってしまいました。原因は良くわかりません。
TimeZone.setDefault(TimeZone.getTimeZone("JST")); ここの「LocaleとTimeZone」という項目で、次のように書かれていました。
Filterで設定した方が、と書かれています。PlayframeworkでFilterに相当するのは・・・
@Before でいいのでしょうかね。そこで物は試しと
@Before でTimeZone.setDefault() するようにしたところ、今のところ問題なく動いておりますです。エンコードplayframework自体、utf-8を基本としているため、単純なhtmlページを表示するだけなら意識しなくてもいいです。でも、csvファイルなどのテキストファイルをダウンロードさせるような時、文字列をそのままバイトストリームに変換して返すと文字化けしました。 StringBuffer src = new StringBuffer();
: renderBinary(new ByteArrayInputStream(src.toString().getBytes(), "example.csv"); try { renderBinary(new ByteArrayInputStream(src.toString().getBytes("UTF-8"), "example.csv"); } catch (UnsupportedEncodingException e) { throw new UnexpectedException(e); } ファイルのアップロードplayframeworkのサイトではFileオブジェクトを使った受け取り方の説明がありますが、一時ファイルを作成する仕組みのため、ファイルシステムへのアクセス制限のあるAppEngineでは使用できません。しかし、諦めるのはまだ早いです。アップロードされたファイルは、Uploadオブジェクトとして扱われます。下記のあたりに実装があります。 play.data.parsing.ApacheMultipartParser.parse(InputStream) ここでは先ずFileUploadオブジェクトへの変換を試み、失敗した場合はMemoryUploadオブジェクトへ変換されます。名前のとおりメモリ上でアップロードされたファイルを受け止める仕組みです。AppEngineではファイルシステムへのアクセス制限を受けてFileUploadへの変換が失敗しますので、MemoryUploadオブジェクトが生成されます。 これを利用すれば、例えば下記のように、InputStreamとして扱ったり、バイト列として扱うことができます。 Upload upload = params.get("file", Upload.class); InputStream is = upload.asStream(); 一時ファイルを置けないので大きなサイズのファイルには対応できませんが、ちょっとしたファイルであれば使えるのではないでしょうか。 utf-8のテキストファイルをアップロードしたとき、プログラム側ではそのまま受け取ると文字化けしてしまった。 受け取ったバイト列をutf-8として解釈するように書く必要がありそう。 String data = new String(upload.asBytes(), "UTF-8") ; 認証機能などGoogleアカウントでの認証機能を利用することができます。
これを利用すればアプリケーションでは認証の可否やパスワードを管理する必要がありません。
開発時のログイン処理エミュレーションAppEngineで提供されるログイン機能をローカルの開発時にエミュレートするため、GAEActionクラスが用意されています。
Routesに以下のパスが追加されて開発時はこれらのアクションが実行されるようになっています。
POST /_ah/login GAEActions.doLogin GET /_ah/logout GAEActions.logout 本番ではログイン画面が出るタイミングで、gaeモジュールが提供するログインエミュレート画面が開きます。
デプロイしてAppEngine上で稼働するとPlayでパスの評価が行われる前にAppEngineが横取りするためにエミュレーション処理は行われなくなります。
play.modules.gae.GAE.classユーティリティクラス。
public static DatastoreService getDatastore()
public static UserService getUserService()
public static URLFetchService getURLFetchService()
ログイン補助
public static void login()
public static void login(String returnAction) ログイン画面へ遷移する。ログイン後は指定のアクションへリダイレクトする。 public static void login(String returnAction, Map<String, Object> returnParams)
その他、パラメータ有無やOpenID用のパラメータなどを渡す種々のパターンが用意されている。
ログアウト補助
public static void logout()
public static void logout(String returnAction)
ユーザ情報
public static User getUser()
public static boolean isLoggedIn()
public static boolean isAdmin()
拡張タグ#[gae.ifAdmin}~#{/gae.ifAdmin}
#{gae.ifLoggedIn}~#{/gae.ifLoggedIn}
#{gae.loginURL}
#{gae.logoutURL}
#{gae.user}
cron機能playframeworkにはスケジューリングされたジョブ実行機能がありますが、この機能はgaeにデプロイすると無効になってしまいます。
そこでAppEngineが提供しているcron機能を利用します。
cron機能では、指定した時刻に設定したurlへのhttpリクエストが発生する仕組みで実現されます。
そのurlへのリクエストを手動で発生させれば、ジョブをキックすることも可能です。開発時はcron機能は動かないので、これを利用するとよいと思います。
cron機能を利用するには、
war/WEB-INF 配下に cron.xml を作成して各種定義を記述します。<?xml version="1.0" encoding="UTF-8"?>
<cronentries> <cron> <url>/cron/job1/</url> <description>1時間ごとに実行するテスト</description> <schedule>every 1 hours synchronized</schedule> <timezone>America/New_York</timezone> </cron> <cron> <url>/cron/job2/</url> <description>30分ごとに実行するテスト</description> <schedule>every 30 minutes synchronized</schedule> </cron> </cronentries> 記述方法などについては本家を参照。
cron機能はhttpリクエストという形で実行されるので、Frontend Instance のリソースを消費します。
Frontend Instanceはリクエストが全くない状態が続くと勝手に終了してしまうという問題がありますが、cron機能を利用してリクエストが途絶えないようにインスタンスが終了してしまうのを回避することができます。
Googleの解説によると、cronのURLへ一般ユーザがアクセスできないようにするためにweb.xmlで制限する方法が書かれています。
他の方法として、AppEngineからのcronリクエストはIP:0.1.0.1 という特殊なアドレスから行われていることを利用して、@Before処理の中で本番モードの時はこのIPからのアクセスのみ受け付けるように制限するようにしても良いかもしれません。
request.remoteAddress でリクエスト元のIPアドレスが参照できます。本番モード(Play.mode.isProd()) == true )でリクエスト元が IP:0.1.0.1 なら forbidden(); で権限なしエラーページへ飛ばすようにしたらよいかと思います。権限なしエラーページはそのままだとフレームワーク標準のページが開きますが、
views/errors/403.html に自前のテンプレートを置けばそれが優先的に使用されます。taskqueue機能タスクキュー機能はその名の示す通り、仕事をキューに貯めて逐次実行していく機能です。
実行タイミングはAppEngine側の判断に任されますが、基本的にすぐに実行されます。
タスクキュー機能で実行されたタスクは、httpリクエストのように30秒制限ではなく10分以内と緩くなっています。
直ぐにレスポンスを返すことができないような処理をタスクキューに積んでおいて、後で処理結果を得るような使い方が考えられます。
cron機能を利用するには、
war/WEB-INF 配下に queue .xml を作成して各種定義を記述します。キューの名前、実行頻度、リトライ間隔など定義します。
cron機能と同じでURLリクエストとして起動される仕組みです。
URLを直接たたけばタスクが実行されることになるので、cron機能と同じく防御策が必要です。
タスクキューからのリクエストは、IP:0.1.0.2からなので、これを利用すると良いかもしれません。
Queue queue = QueueFactory.getQueue("my-queue"); queue.add(TaskOptions.Builder.withUrl('/path/')); "my-queue"は定義したキューの名前、'/path/'は実行するタスクのURLパターンです。
Playframeworkであれば、
.withUrl(Router.reverse("Application.myQueue").url) みたいに記述した方が汎用性がアップしますね。キューに積む際に、
.param("id", id).method(Method.GET) などと続ければパラメータを同時に渡すことも可能です。参考:
非同期JobAppEngineにデプロイするとスケジュール実行などが動かないことはcron機能の項で書いた通りです。
@OnApplicationのようなアノテーションも無効になってしまいます。
それでもどうしても起動時に行いたい処理がある、ということで解決された方が海外におりましたので紹介します。
プラグインを記述する要領で実現しています。
プラグインを作成してプロジェクト内に置く方法が書かれています。
これなら
onApplicationStart 以外にもいろいろなトリガで処理が実装できそうですね。試してみたところ、
onApplicationStart() メソッドをオーバーライドしたものは、確かにインスタンス起動時に実行されました。onApplicationStop() メソッドも試しにオーバーライドしてみたのですが、こちらはインスタンス停止時に実行されることはありませんでした。プラグイン方式だから万能というわけではないようです。
また、AppEngine上ではインスタンス起動時に1回実行されるのですが、ローカル環境は連続して2回実行されてしまう現象が発生しています。
原因はよくわかりません。プロジェクト内にプラグインを置いているからでしょうか。
支障があるなら開発モードでは2回目の実行を抑制するようなコードを書いた方が良いですね。
Sienaの利用JPAと似ているので違いを押さえればすぐに使えるようになります。たぶん。
Modelクラスの定義定義する際の注意事項。
定義サンプル
public class Genre extends Model { @Id(Generator.AUTO_INCREMENT) public Long id; public String name; public String address;
} 開発時のもろもろデータの保持場所ローカルでSienaを使ってデータベース操作を行うと、いつの間にか
/tmp/datastore というファイルが作られます。ローカル環境で保存したデータはここに保持されているようなのでデータをクリアしたいときはこのファイルを削除すれば良さそうです。
インデックス定義また、いつの間にか
/WEB-INF/ appengine-generated/datastore-indexes-auto.xml というファイルも作成されます。 Sienaがデータベースの使用状況からインデックス定義を考えてくれているみたいです。
このファイルを
datastore-inidexes.xml に名前を変えて /WEB-INF/ 配下に置けば定義が反映されるようです。(まだ試してない)どの項目で検索をするかアプリケーションに合わせて設定することで検索効率を上げることができますが、一方でインデックス設定をするということはデータの書き込み時にインデックスを更新する手間が発生することを意味します。やみくもに設定してしまうと更新時の効率が落ちることになります。
などと一般論を書いておきます。
Fixtureローカル環境でのデータ保持場所は
/tmp 配下ということもあり、ちょっとしたことで消えてしまいます。きっちり初期データを投入できるようにしておくと開発も捗ります。
SienaFixtures.delete(CmGroup.class); // CmGroup.class で示されるテーブルのデータを削除
SienaFixtures.loadModels("cm_init.yml"); // cm_init.yml に書かれたデータを投入 Yamlファイルの書き方は、JPAの時と大して変わりません。
データ操作更新insert メソッド、update メソッド、save メソッドで追加や更新を行います。IDを自動採番にしておくと
insert するときにIDに値が入っていても重複エラーにはなりません。無視されて新たなIDが採番されてレコード追加されます。save がどこまでやってくれるのかまだ把握してないのですが、親子関係を定義しておくと親のsave で子の追加を行ってくれたりはするようです。削除delete メソッドでレコードの削除を行います。バッチ処理複数レコードをまとめて更新したり削除したりするためのメソッドが用意されています。
Model.batch(Person.class).insert(persons); 検索
永続化しないフィールドJPAのときは @Transient アノテーションを付けることで表現していましたが、Sienaの場合は下記のようにアノテーションではなくjavaの予約語で指定します。
public transient Long age = 0L; リレーションMany<T>JPAほどではないですが、OneToManyのようなこともできるようです。
参考:
AppEngineの場合とそれ以外の場合とで別の書き方をするようなので、AppEngine以外も視野に入れてる方は要注意です。
Many<T> を使ったリレーションはAppEngineで先行して使えるようになっています。下記のサンプルコードを見ると使い方や挙動が理解できるかと思います。
参考:https://github.com/mandubian/siena/blob/master/source/samples/siena/samples/relations/owned/one2many
ざっくり私なりの解釈を書いておきます。
@OwnedアノテーションMany<T> プロパティに@Owned アノテーションを付けると、子の方に書く親オブジェクトへの参照を One<T> なしでシンプルに書けます。@Owned の引数mappedBy には、子オブジェクトのどのプロパティが親への参照かを与えます。必須ではありません。指定しない場合はSienaが勝手に推測します。
旧表記の
Query<T> を使って書くとこんな感じになります。@Filter("owner") public Query<ChildModel> child; @AggretedアノテーションMany<T> プロパティに@Aggreted アノテーションを付けると、参照される側には参照する側への参照を書かないでよくなります。ということですが詳しくは良くわかりません。
Lifecycleデータ操作の各タイミングをトリガにして任意のメソッドを実行する機能があります。
データの削除をトリガにログを出力したり、更新をトリガにしてMemCache上のデータも更新したり、といった使い方ができます。
参考:https://github.com/mandubian/siena/blob/master/source/documentation/manuals/lifecycle.textile現在(siena-2.0.7)は、デフォルトで使用しない設定になっているので(将来的にはデフォONになると思われる)、手動でONにする必要があります。
application.conf に以下の設定を記述してください。使用する場合は true または yes を指定します。on ではダメです。厳しいです。
siena.lifecycle=true ![]() |