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でやるのが一番素直なんだけど,どうなんだろう?