playframework+gae

はじめに

PlayframeworkとGoogleAppEngineの組み合わせについて、集めた情報などをまとめます。
Playframeworkはバージョン1系と2系があり、現在は2系がメインストリームとなってしまっていますが、ここでは1系の情報を扱います。
使用バージョンは下記のとおりです。
  • Playframework 1.2.5
  • Google App Engine [gae] module 1.6.0
  • Play Siena module 2.0.7
  • Google App Engine SDK for Java 1.8.0
私自身、AppEngineを使った開発は初めてで練習がてらのまとめ情報ですので的を外してるところがあるかもしれません。それも踏まえてみていただければと思います。
あと、GoogleAppEngineの登録は済んでいる前提で進めます。そのあたりは詳しく解説しているサイトがたくさんあると思いますので。

開発準備

PlayframeworkでAppengine用にアプリケーションを作成する場合、gaeモジュールを使うと便利です。
AppEngineへのデプロイ、開発時のログインエミュレート機能、APIのラッパーなどが揃っています。至れり尽くせり、というほどではありませんが、初心者には心強いです。
 

GAEモジュールのインストール

他のモジュールと同じくコマンドラインよりインストールコマンドを入力します。(バージョンを指定しない場合は推奨バージョンが選択されます)

C:\> play install gae-1.6.0

自動的にダウンロードが開始、playディレクトリに展開、インストールされます。
 

GoogleAppEngineSDKのインストール

必須です。
ここより、Google App Engine SDK for Java をダウンロードしてきます。任意のディレクトリに展開します。
ディレクトリに展開したら環境変数「gae_path」にディレクトリのパスを設定しておきます。
Windowsであれば、システムのプロパティ>詳細設定>環境変数>ユーザの環境変数、で登録しておけばよいと思います。

C:\> SET gae_path=C:\gae_sdk

 

Play Sienaモジュールのインストール

AppEngineで使用可能なデータベースは、Datastoreと呼ばれるKV型のデータベースです。
DatastoreはPlayframework標準添付のJPAではサポートされていません。
代わりに近い感覚でデータベースを扱えるSienaをインストールします。JPAほどではありませんがダイレクトにDatastoreを扱うのと比べて格段に楽です。たぶん。SienaはMySQLやPostgreSQLなどのデータベースに対応しているので、Sienaで作っておけば将来、別の環境で動かすとなったときの敷居が低いかもしれません。
他のモジュールと同じようにコマンドラインよりインストールします。

C:\> play install siena-2.0.7

アプリケーション側の準備

アプリケーション側では、gaeモジュールやSienaモジュールを使用することを宣言する必要があります。
dependencies.ymlファイルへ依存関係を記述します。

require:

    - play -> play [1.2.1,)
    - play -> siena [2.0.7,)
    - play -> gae [1.6.0,)
書式の詳細はこちらで。
次にdependenciesコマンドでアプリケーションの開発ディレクトリの各種設定に反映させます。

C:\> play dependencies [アプリケーションディレクトリ]

アプリケーションディレクトリは、カレントディレクトリがそのアプリケーションのディレクトリであれば省略可能です。
 
準備ができたら取りあえずアプリケーションを起動します。コマンドラインからでもEclipseからでも構いません。

C:\> play run [アプリケーションディレクトリ]

初回の起動だけは特別で、war/WEB-INF/appengine-web.xml というファイルの生成が行われます。ファイルが生成されないようならインストールのどこかで失敗した可能性があります。
で、生成されたこのxmlファイルの<application>タグにアプリケーションのID(Create Applicationとして「Application Identifier:」に入力したアプリケーションID文字列です)を記入しましょう。

<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">

    <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 とだけ書かれていると思いますので、これをコメントアウトしておきます。

# db=mem

 
Sienaモジュールにはデータの更新や削除の前後イベントをフックする機能があります。
その機能を利用したい場合は、application.conf に次の設定を追加しておきます。

siena.lifecycle=true

 

デプロイ

まずはデプロイできることを確認しましょう。
コマンドラインからデプロイコマンドを入力します。

C:\> play gae:deploy [アプリケーションディレクトリ] --%prod

途中でアカウントとパスワードを聞かれるので、入力する準備をしておきましょう。
 
フレームワークIDの指定(--%prodというやつ)は、やっておいた方がいいというレベルです。
やっておけば起動時に「強制的にprodモードにしたよ」というメッセージが出なくなります。
application.conf に次のように指定しておけばよいです。

%prod.application.mode=prod

 
 
少なくともOnApplication()を実行する段階では、この「強制的にprodモードにする」前の状態です。その段階ではdevモード用の設定が有効になっているため、OnApplication()内での処理で設定を扱う場合は注意しなくてはなりません。
そのような煩わしさから解放されるためにも手動でフレームワークIDを指定しておくことをお勧めします。
 

その他のコマンド

gaeモジュールで指定できるデプロイ以外のコマンドについての説明はコレといったのが無く、長いことデプロイだけしかできないのかと思っていました。
しかし、${play_path}/modules/gae-1.6.0/commands.py の中を見ると色々できそうです。簡単に書き出してみます。
実際に試してみてないので動くかどうかまでは不明です。
コマンド 説明
gae:package warファイルを作成する。デプロイまでは行わない。
gae:update_indexes appcfg.cmd update_indexes コマンドを実行する。インデックスの更新?
gae:vacuum_indexes appcfg.cmd vacuum_indexes コマンドを実行する。インデックスの削除?
gae:update_queues appcfg.cmd update_queues コマンドを実行する。
gae:update_dos appcfg.cmd update_dos コマンドを実行する。
gae:update_cron appcfg.cmd update_cron コマンドを実行する。
gae:request_logs appcfg.cmd request_logs コマンドを実行する。
ソースをよく見るとヘルプが書いてありました。抜粋しておきます。

HELP = {

    '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に受け付けるコマンドのリストがあるようなので、これにロールバックコマンドを追加します。

COMMANDS = ["gae:rollback", "gae:deploy", "gae:package", "gae:update_indexes", "gae:vacuum_indexes", "gae:update_queues", "gae:update_dos", "gae:update_cron", "gae:cron_info", "gae:request_logs"]

ソースの最後にロールバックコマンドに対する処理を追加します。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という数字が表示されました!

Logger.info("Available processors : %d", Runtime.getRuntime().availableProcessors());

これを真に受けて1338スレッドをMAXとされても現実的ではありません。設定ファイルで任意の数を設定する方が良いと思います。
%prod.play.pool=3
2015/2/16 久しぶりに確認してみたところ、プロセッサ数2になっていました。

タイムゾーン

AppEngineは日本だけに提供されているわけではありませんのでタイムゾーンはUTCとなっています。
私はアプリケーションを日本時間で動かしたかったので下記のようなコードを発行してみたのですが、一時的にはJSTになるもののいつの間にか戻ってしまいました。原因は良くわかりません。
TimeZone.setDefault(TimeZone.getTimeZone("JST"));
 
ここの「LocaleとTimeZone」という項目で、次のように書かれていました。
  • GAEのデフォルトでは、Localeはen_US、TimeZoneはUTCです。
    • 但し、ローカルの開発環境では、LocaleはOSの設定のようです。日本ならja_JP。でも、TimeZoneはUTCです。
  • Locale.setDafault()は、アクセス制御により使用が禁止されていますが、TimeZone.setDefault()は利用可能なようです。
  • ということで、TimeZoneはFilterでsetDefault()でJSTにしておいたほうが、何かとシアワセかもしれません。
Filterで設定した方が、と書かれています。PlayframeworkでFilterに相当するのは・・・@Beforeでいいのでしょうかね。
そこで物は試しと@BeforeTimeZone.setDefault()するようにしたところ、今のところ問題なく動いておりますです。
 

エンコード

playframework自体、utf-8を基本としているため、単純なhtmlページを表示するだけなら意識しなくてもいいです。
でも、csvファイルなどのテキストファイルをダウンロードさせるような時、文字列をそのままバイトストリームに変換して返すと文字化けしました。
StringBuffer src = new StringBuffer();
src.append("aaa,bbb,ccc" + crlf);

renderBinary(new ByteArrayInputStream(src.toString().getBytes(), "example.csv");
→文字化けする

そこで、Stringからバイト列を得るときに"utf-8"でエンコードすると意図通りに動きました。もちろん要件にもよるのでしょうが。
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に以下のパスが追加されて開発時はこれらのアクションが実行されるようになっています。

GET    /_ah/login   GAEActions.login

POST   /_ah/login   GAEActions.doLogin
GET    /_ah/logout  GAEActions.logout
本番ではログイン画面が出るタイミングで、gaeモジュールが提供するログインエミュレート画面が開きます。 
デプロイしてAppEngine上で稼働するとPlayでパスの評価が行われる前にAppEngineが横取りするためにエミュレーション処理は行われなくなります。

play.modules.gae.GAE.class

ユーティリティクラス。
public static DatastoreService getDatastore()
GAEのデータストアサービスのインスタンスを返す。
public static UserService getUserService()
GAEのユーザサービスのインスタンスを返す。
public static URLFetchService getURLFetchService()
GAEのURLフェッチサービスのインスタンスを返す。
 
ログイン補助
public static void login()
ログイン画面へ遷移する。ログイン後は現在のアクション(URL)に入りなおす。
public static void login(String returnAction)
ログイン画面へ遷移する。ログイン後は指定のアクションへリダイレクトする。
public static void login(String returnAction, Map<String, Object> returnParams)
ログイン処理して指定のアクションへパラメータを渡しつつリダイレクトする。
その他、パラメータ有無やOpenID用のパラメータなどを渡す種々のパターンが用意されている。
 
ログアウト補助
public static void logout()
ログアウト処理して現在のアクション(URL)に入りなおす。
public static void logout(String returnAction)
ログアウト処理して指定のアクションへリダイレクトする。
 
ユーザ情報
public static User getUser()
ログイン中のユーザ情報を返す。
Userオブジェクトのプロパティは、Email、AuthDomain、FederatedIdentity、Nickname、UserId。
Googleアカウントでログインしている場合、EmailとNicknameにはメールアドレス。AuthDomainにはgoogle.com。UserIdには固有の数値が設定されていた。UserIdは少なくともログインログアウトを繰り返したくらいでは変わらなかった。
FederatedIdentityは不明。
public static boolean isLoggedIn()
ユーザがログイン中か否かを返す。
public static boolean isAdmin()
ユーザが管理者権限を持っているかを返す。
AppEngineのアプリケーションオーナーだと管理者と判定されるようだ。DevelopperやViewerがどうなるかまでは試せていない。

拡張タグ

#[gae.ifAdmin}~#{/gae.ifAdmin}
ログイン中のユーザが管理者の場合、タグで囲まれた範囲が出力される。
 
#{gae.ifLoggedIn}~#{/gae.ifLoggedIn}
ログイン中の場合、タグで囲まれた範囲が出力される。
 
#{gae.loginURL}
${play.modules.gae.GAE.userService.createLoginURL(_arg.toString())}
 
#{gae.logoutURL}
${play.modules.gae.GAE.userService.createLogoutURL(_arg.toString())}
 
#{gae.user}
${play.modules.gae.GAE.userService.currentUser}

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機能を利用してリクエストが途絶えないようにインスタンスが終了してしまうのを回避することができます。
※インスタンスの起動は通常よりCPUリソースを消費するため、何度も起動を繰り返していると無料枠に収まらないケースが生じてしまうことがある。
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) などと続ければパラメータを同時に渡すことも可能です。
 
参考:
 

非同期Job

AppEngineにデプロイするとスケジュール実行などが動かないことはcron機能の項で書いた通りです。
@OnApplicationのようなアノテーションも無効になってしまいます。
それでもどうしても起動時に行いたい処理がある、ということで解決された方が海外におりましたので紹介します。
プラグインを記述する要領で実現しています。
 
プラグインを作成してプロジェクト内に置く方法が書かれています。
  1. pluginパッケージ内にplay.PlayPluginを継承したクラスを作成します。
  2. 継承したクラスでonApplicationStart()メソッドをオーバーライドします。
  3. /app/play.plugins というファイルを作成し、作成したプラグインの優先度を設定します。
    書き方は「優先度を示す数値:クラスの修飾名(パッケージを含むフル名称) 」。優先度4000、plugin.Bootstrapクラスであれば、
    4000:plugin.Bootstrap
  4. 以上です。
これならonApplicationStart以外にもいろいろなトリガで処理が実装できそうですね。
試してみたところ、onApplicationStart()メソッドをオーバーライドしたものは、確かにインスタンス起動時に実行されました。
onApplicationStop()メソッドも試しにオーバーライドしてみたのですが、こちらはインスタンス停止時に実行されることはありませんでした。
プラグイン方式だから万能というわけではないようです。
また、AppEngine上ではインスタンス起動時に1回実行されるのですが、ローカル環境は連続して2回実行されてしまう現象が発生しています。
原因はよくわかりません。プロジェクト内にプラグインを置いているからでしょうか。
支障があるなら開発モードでは2回目の実行を抑制するようなコードを書いた方が良いですね。 

Sienaの利用

JPAと似ているので違いを押さえればすぐに使えるようになります。たぶん。

Modelクラスの定義

定義する際の注意事項。
  • siena.Modelを継承する。
  • Modelクラスは、@siena.Tableで修飾する。
  • ID列を必ず作成し、@siena.Idで修飾する。
     /** ID */
     @Id(Generator.AUTO_INCREMENT)
     public Long id;
  • 自分でIDを設定する場合は、アノテーション引数でGenerator.NONEを指定する
定義サンプル

@Table("address_tbl")

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メソッドでレコードの削除を行います。

バッチ処理

複数レコードをまとめて更新したり削除したりするためのメソッドが用意されています。

List<Person> persons = ...

Model.batch(Person.class).insert(persons);
 

検索

  1. IDによるレコード取得(1)

    Person john = new Person();

    john.id = 2;
    john.get();
    IDカラムに探したいIDを設定した後、getメソッドを呼びます。
     
  2. IDによるレコード取得(2)
    Person john = Model.getByKey(Person.class, 2);
    getByKeyメソッドを使ってレコード取得します。
    モデルクラスにこんな風にメソッドを定義しておけば、第一引数で毎回モデルクラスを指定する必要がなくなりスッキリします。
    Public static Person getByKey(final Long id) {
    return Model.getByKey(Person.class, id);
    }
      
  3. filterメソッドによる絞込み
    ID列以外での絞り込み、複数レコードの取得をしたい場合はfilterメソッドを用います。
    allメソッドで全レコード取得を宣言した後、filterメソッドで絞り込むイメージです。件数上限なども同時に行います。
    Person john = Model.all(Person.class).filter("name", "john").get();
    "name"カラムが"john"であるレコードを絞り込みます。getメソッドは1件だけ取得する機能があります。
     
    複数レコードを取得したい場合はfetchメソッドでリストを取得します。
    List<Person> persons = Model.all(Person.class).filter("age>=", 15).fetch(); // 15歳以上
    filterの条件はデフォルトで完全一致ですが、上記例のようにカラム名に「>=」「<」など不等号を付与することで範囲での指定ができます。
    また、filterの結果に対してfilterを行うこともできるので、複数条件のAND条件に対応できます。
    .filter("age>=", 60).filter("age<", 70)   // 60歳代
  4.  
  5. 複数レコード取得時の件数指定
    List<Person> persons = Model.all(Person.class).fetch(10);      // 10件
    List<Person> persons = Model.all(Person.class).fetch(10, 25);  // 25件目から10件

  6. ページング処理向けの複数レコード取得
    List<Person> persons = Model.all(Person.class).pagenate(10).fetch(); // 初めの10件
    persons = Model.all(Person.class).nextPage().fetch();                // 次の10件
    persons = Model.all(Person.class).nextPage().fetch();                // その次の10件
    persons = Model.all(Person.class).previousPage().fetch();            // 前の10件
    persons = Model.all(Person.class).previousPage().fetch();            // その前の10件
    先頭より前に戻ろうとするとサイズゼロのリストを返す。
 

永続化しないフィールド

JPAのときは @Transient アノテーションを付けることで表現していましたが、Sienaの場合は下記のようにアノテーションではなくjavaの予約語で指定します。
public transient Long age = 0L;
 

リレーション

Many<T>

JPAほどではないですが、OneToManyのようなこともできるようです。
参考:
AppEngineの場合とそれ以外の場合とで別の書き方をするようなので、AppEngine以外も視野に入れてる方は要注意です。
 
Many<T>を使ったリレーションはAppEngineで先行して使えるようになっています。
下記のサンプルコードを見ると使い方や挙動が理解できるかと思います。
ざっくり私なりの解釈を書いておきます。
  • test()
    新しいオブジェクトを作成するとき、親子関係を構成(asList().add()する)しておくと、親のsave()でまとめて登録できる。
  • test2()
    新しいオブジェクトを作成するとき、親をinsert()、子の親参照を設定してinsert()すると、save()相当の動作となる。
  • test3()
    親子の関係を切る(asList().remove()する)たり、追加(asList().add()する)して親をupdate()すると更新できる。
    切り離された子オブジェクトの親参照にはnull が設定される。
  • test4()
    子オブジェクトを変更して、親オブジェクトをupdate()しても子オブジェクトは永続化されない。子オブジェクトを個別にupdate()する必要がある
  • test5()
    初回のasList()でフェッチする。その後にasList().sync()で同期をとってもすでにフェッチしているとフェッチを省略してしまう。
    最新の状態をフェッチしにいかない。そんなときはasList().forceSync()で強制的にフェッチする。
  • test6()
    asQuery()を使うと親子関係に任意のフィルタを施して取得することができる。 order()で並べ替えもできる。
    その際、fetch()するとデータリスト。iter()するとイテレータを取得できる。

@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
 
 
 
Comments