App Engine 1.3.1
でた.プレリリースが先週出ていて,その1週間後に予定通り本リリース.データストア周りが結構変わっていて,これまで苦労していた部分で苦労しなくて済みそうな感じ.
Query Cursor
いわゆるカーソルが導入された.RDBだと普通にあるのだけど,データストアからの読み出しにrangeで制約をつけておき,結果からカーソルを取得しておくと,次に同じキュエリをだすときに,カーソルを指定して,そのつづきから,という形で検索ができる.
これまでだと,ソートのキーになっているプロパティの値を取っておいて,それを条件に加えて検索していたのだけど,そんなテクは不要に.
No more 1000 result limit
これまで,データストアには,1000個以上の値を返せない,という結構きつい縛りがあった.rangeで指定しても,たとえば 900-1100 みたいな検索ができなかった.この制約が無くなったらしい.ほんとかよ.iterationであれ,listであれ,cursorであれ,1000個以上でも大丈夫,とのこと.とはいえ,あんまりやり過ぎると,30秒ルールでタイムアウトするだけだろうけど.
JDOで,試してみたら,setRange(0, 10000)でサックリ動いた.すばらしい..が,setRange(1001, 10000)だとこんなエラーが.
java.lang.IllegalArgumentException: offset may not be above 1000
ちなみに,ローカル開発環境では問題なく動く.
おそらく,ライブラリ内でのチェックが残っちゃってるだけだろう.JDOじゃなければ大丈夫なんだろうし,そのうち治るだろうけど.
自動リトライ
これまで,データストアアクセスは結構な確率で失敗することがあり,ユーザが自前でリトライをかけていた.これが改善されたらしい.といっても,自動的にリトライをかけてくれるようになっただけで,別に内部的なエラーが起きにくくなったわけではないようだ.
URLFetchが非同期に対応
Python版では以前から使えていたのだけど,なぜかJavaでは使えなかった非同期URLFetchができるように.すばらしい.
具体的には,Low-level APIの URLFetchServiceにfetchメソッドに加えて,fetchAsyncメソッドが追加されている.fetch がHTTPResponseを返すのに対して,fetchAsyncはFuture
App Engineで使えるクラスのホワイトリストに,スレッドの使えないApp Engine環境では不要そうなjava.util.concurrentが登録されていたのは,非同期機構を追加するための布石だったのか!!!
多分今後,データストアへのリクエストも非同期にできるようになったりするんだろうなあ..
cron に month と synchronized が追加
month は 毎月,という意味.下は毎月1,15日の9時の意.これまでは,jan,feb,mar,...と書かなければいけなかったらしい.そっちのほうが凄い.
これまでは,そもそも第XY曜みたいな書き方しかできなかったのが,X月Y日みたいな書き方ができるようになった,ということ.
1,15 of month 9:00
で,第XY曜の場合は,of 月を省略すると毎月という意味なんだけど,日付指定の場合は of 月が省略できない,で,かわりにmonthというキーワードを導入したらしい.
たしかに
1st, 2nd wed 12:00
なら意味がわかるが,
1, 15 9:00
だと意味不明だ.
synchronizedは interval実行につける事のできるキーワード
every 5 minutes
だと,前回のジョブ終了の5分後に次のジョブが実行される.ジョブが30秒ぎりぎりまで走ったりすると,5分に1度ではなく,5分30秒に一回しか実行されない.
これに対して
every 5 minutes synchronized
だと,前回のジョブ*開始時間*の5分後にジョブが起動されるようになる.
カスタム管理コンソール
今見てみたけど,カスタマイズするような方法は見えなかった.まだ反映されていないのか,見落としているのか...
所感
データストアの改良は地味だけど非常に重要.すばらしい.
実はいまApp Engineの本を書かせてもらっていて,もうすぐしめきりなのだけど,結構書き直さなければいけなくなっちゃって,ちょっとショック.まあ,古い内容の本をだすよりはいいのだけど,校了までにもう1回ぐらいアップデートがあったりするとイヤだな−...
Cursorをつかったページング
1.3.1までは,データストアからの読み出し個数が1000個以下に限られていた.なので,1000個以上のデータを取り出すときに,rangeを使ってやろうとするとうまく動かなかった.というのは,range(990, 1010) とやると,0から1100までのキーをとりだして,その後,990から1010までの中身を取りに行く,という実装なので,0から1100までのキーを取りだすという時点で,落ちてしまうからだ.また,そもそも20個だけ読みたいのに,1100個キーを取りだしてしまうので非効率.
これまでの方法
そこでよく知られているテクが,これ.条件式を組み合わせることで,rangeのオフセットを常に0にしたままスキャンする方法.このサンプルはPythonだけど,JDOで書いてもそのまま動く.
1.3.1
ところが,1.3.1になって事情が変わった.1000個の制約が無くなったので,とりあえず性能に目をつぶれば,rangeだけで大規模データに対してもページングできる(2/11日現在,JDOでは,オフセットが1000以下という縛りがまだ取れていないようだけど).
さらに,Cursorという機能が導入されたことで条件式を組み合わせなくても,ページングできるようになった.Cursorは,検索した結果が指している場所をおぼえておき,次の検索の際にそこから続きを検索する仕掛け.Cursorは文字列に変換できるので,生成したページに埋め込んでおけば,次にクリックした時に続きを実行するようにできる.
サンプル
ソースコードはこんな感じ.JDOだと,カーソルを設定するのに,マップをわざわざ作って,extensionMapとしてセットしなければならないのが面倒くさいところ.
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html"); PrintWriter writer = resp.getWriter(); String cursorString = req.getParameter("cursor"); PersistenceManager pm = null; try { pm = PMF.get().getPersistenceManager(); Query query = pm.newQuery(TestEntity.class); query.setOrdering("num asc"); query.setRange(0, 100); if (cursorString != null) { Cursor cursor = Cursor.fromWebSafeString(cursorString); Map<String, Object> extensionMap = new HashMap<String, Object>(); extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor); query.setExtensions(extensionMap); } List<TestEntity> entities = (List<TestEntity>) query.execute(); for (TestEntity e: entities) writer.println(e.getNum() + "<br/>"); Cursor newCursor = JDOCursorHelper.getCursor(entities); writer.println("<a href=\"cursortest?cursor=" + newCursor.toWebSafeString() + "\">next</a>"); } finally { if (pm != null && !pm.isClosed()) pm.close(); } }
所感
Cursor便利,だけど,Cursorだと,nextリンクを表示するべきかどうか,つまりもっとデータがあるのかどうかを知るのがちょっと面倒.Cursorを使って検索をかけてやればいいのだけど,2度検索することになるのでなんかイマイチ.条件式を組み合わせるテクの場合は,あらかじめ1個余分にリクエストしておいて,いくつ返ってきたかで,判断することができたので,検索は一回ですんだのだけど.
そもそも,rangeの実装が変更されてキーを全部取ってくる,なんて構造じゃなくなっていれば,この辺のことは全部rangeでやるのが一番素直なんだけど,どうなんだろう?