SiNBLOG

140文字に入らないことを、極稀に書くBlog

Slim3 もっと読むを作ってみた!

ソーシャル系なんかで、よく見る"もっと読む"機能。
これを作りたかったので、やってみました。
基本的には公式のサンプルにあるページングと同じです。

http://code.google.com/intl/ja/appengine/articles/paging.html

最初はoffset,limitを使って作ろうと思っていたのですが・・・。
GAEのoffsetは速度が遅い上に、ReadOpsの消費が大きい・・・。
対象データが1000件ある状態で、offset(20),limit(20)とやっても、ReadOpsは1000かかってしまいます。
これでは、クラウド破産への道にまっしぐらですw
以下にそこら辺りのことを図に交えて解説してくださっています。

DataStore Read Operation の Quota (プログラマーですが、何か?(I am a software programmer.))


では、どうするか?
クエリカーソルを使います。
Slim3でもサポートされています。
クエリとインデックス - Slim3 日本語サイト(非公式)

このクエリカーソルというのは、公式で以下のように書かれています。


カーソルの位置は、返された最後の結果の後の結果リスト内での場所として定義されます。
カーソルはリスト内の相対的な位置ではありません(オフセットではありません)。
データストアが結果を取得するためのインデックスのスキャンを開始するときに、その場所へジャンプするためのマーカーです。

もっと読む機能を作るには、かなり都合の良い感じです。
一覧表示した時にカーソルを保持しておき、そこから続きを読めば良いだけです。
それでは、ソースです!
作っていたのはBBSなので、ソース中にそれっぽい記述が出ています。

IndexController


public class IndexController extends AbstractController {

private BBSService bbsService = new BBSService();

/**
* 掲示板トップページを表示
*/
@Override
public Navigation run() throws Exception {
S3QueryResultList bbsPostList = bbsService.getBBSPostList();
requestScope("bbsPostList", bbsPostList);
requestScope("cursor", bbsPostList.getEncodedCursor());
requestScope("hasNext", bbsPostList.hasNext());
return forward("index.jsp");
}
}

S3QueryResultListがSlim3でクエリカーソルを使う場合のクラスです。
Listと同じ機能を提供しつつ、クエリカーソルに必要な機能を提供してくれます。

以下がカーソルを取得している部分です。


requestScope("cursor", bbsPostList.getEncodedCursor());
これをhiddenに保持しておき、もっと読むボタンを押した時に受け取っています。
また、今回は検索条件とソート条件が固定なので、利用しませんが以下のメソッドも存在します。

getEncodedFilters() //Fileter条件
getEncodedSorts() //Sort条件
カーソルを使う場合、カーソルを取得した時とFilter条件とSort条件が同一でなければなりません。
これらも必要に応じて、hiddenに格納してやれば良いと思います。


requestScope("hasNext", bbsPostList.hasNext());
ここは続きがあるかどうかを取得しています。
続きがなければ、もっと読むボタンを表示する必要は無いですしね。

BBSService


public class BBSService {

public final Integer DEFAULT_LIMIT = 20;

private BBSPostMeta meta = new BBSPostMeta();

public final SortCriterion DEFAULT_SORT = meta.serial.desc;

/**
* 一覧取得
*
* @return
*/
public S3QueryResultList getBBSPostList() {
return getBBSPostList(DEFAULT_LIMIT);
}

/**
* 一覧取得
*
* @param cursor
* @return
*/
public S3QueryResultList getBBSPostList(String cursor) {
if (StringUtil.isEmpty(cursor)) {
return getBBSPostList(DEFAULT_LIMIT);
}
return getBBSPostList(cursor, DEFAULT_LIMIT);
}

/**
* 一覧取得
*
* @param limit
* @return
*/
public S3QueryResultList getBBSPostList(int limit) {
return Datastore
.query(meta)
.limit(limit)
.sort(DEFAULT_SORT)
.asQueryResultList();
}

/**
* 一覧取得
*
* @param cursor
* @param limit
* @return
*/
public S3QueryResultList getBBSPostList(String cursor, int limit) {
return Datastore
.query(meta)
.encodedStartCursor(cursor)
.limit(limit)
.sort(DEFAULT_SORT)
.asQueryResultList();
}
}

公式のサンプルでは、一番最初の一覧取得に条件は指定せず、全件取得していました。

2012/06/09更新
これだと、BBSへの投稿が増えていけば最初の取得でReadOpsが大きくかかってしまいます。
それを防ぐために、1ずつカウントアップする値を投稿に設定して、現在のカウントより100件小さい値を持っている投稿だけを取得しています。
この辺りの処理はユースケースによって変わってくると思います。
今回の実装だと、最新100件を超える投稿は読み込めません。
カーソルが使用できるのは、カーソル取得時点でのFileter,Sort条件が同一の場合のみだからです。
その辺りは、良い感じになるように変えていただければと思います。
limitを指定さえしておけば、ReadOpsはlimitで指定したサイズしか消費されませんでした。


Modelに関しては、特に何もしていません。
一意となるcount値を持つserialがあるぐらいです。


もっと読むボタンを押された後は、Ajaxで受け取っているのですが、hiddenに保持していたカーソルを取得し、BBSService#getBBSPostList(String cursor)を読んでいるだけなので、割愛します。

以上でもっと読む機能の実装は終りです。
実際に実装する場合は、公式ページも読んでおいた方が良いと思います。
http://code.google.com/intl/ja/appengine/docs/java/datastore/queries.html#Query_Cursors


ただ、よくあるページングを作りたい場合は、上記の実装だけでは不十分です。
先に進むだけで、戻れませんし、指定のページに行くこともできません。
何となく思いつくのは、ページごとにカーソルをhiddenやSession、Datastoreに保持しておくとかでしょうか。
風の噂で、カーソルとoffsetを組み合わせた方法を、@shin1ogawa 殿が作っていたという話を聞いたので、ちょっと調べてみようかなーとも思っています。


手探りで実装したので、ご指摘や、もっと良い方法があるよ!という方は、コメント欄やtwitterでぜひ教えてください!


実際のコードは以下!
BBS取得一覧Controller
Google Code Archive - Long-term storage for Google Code Project Hosting.
BBSService
Google Code Archive - Long-term storage for Google Code Project Hosting.
BBSPostModel
Google Code Archive - Long-term storage for Google Code Project Hosting.
BBS一覧表示のjsp
Google Code Archive - Long-term storage for Google Code Project Hosting.
もっと読むボタン押下時のAjaxのリクエストを受け取るController
Google Code Archive - Long-term storage for Google Code Project Hosting.