GAEのDataStoreで前後ページネーション

GAEでDataStoreを使っていてページネーションを作ろうとしたらつらみがあった。
なんとかCursorを使って前後ページネーションくらいは実装できたのでメモ。

DataStoreも一応クエリでoffsetを指定したり、countで件数を取得することはできるのだが、これらが実は全件を舐める実装となっているらしく、気軽には使えない。
つまり総データ件数に比例してリソースや時間を食う処理になってしまうため、運用時間とともにデータが増えていくアプリでは死に繋がる機能となっている。

なのでCursorを使ってうまいことする。

main.py


@app.route('/articles', methods=['GET'])
def article_list():
    POSTS_PER_PAGE = 20

    cursor = request.args.get('cursor')
    current_cursor = Cursor(urlsafe=cursor)

    # 表示する要素を取得するクエリ 作成時間降順
    query = Article.query().article(-Article.timestamp)
    articles, next_cursor, more = query.fetch_page(POSTS_PER_PAGE, start_cursor=current_cursor)

    if not (cursor):
        # 最初のページは引数のcursorがなく、前のページもない
        has_prev = False
    else:
        # 最初のページ以外は前のページがある ソート順を逆にして前のページのCursorを取得
        prev_query = Article.query().article(Article.timestamp)
        prev_articles, prev_cursor, prev_more = prev_query.fetch_page(POSTS_PER_PAGE, start_cursor=current_cursor)
        has_prev = True

    # 前へのリンクを表示するためのURL引数
    if (has_prev and prev_cursor):
        prev_cursor_urlsafe = prev_cursor.urlsafe()
    else:
        prev_cursor_urlsafe = ''

    # 次へのリンクを表示するためのURL引数 moreがなければ次はない
    if (more):
        next_cursor_urlsafe = next_cursor.urlsafe()
    else:
        next_cursor_urlsafe = ''

    return render_template('article_list.html',
        articles = articles,
        prev_cursor = prev_cursor_urlsafe,
        next_cursor = next_cursor_urlsafe,
    )

templates/article_list.html


<div class="row" style="margin-top: -10px;">
    <div class="col-md-1">
    {% if prev_cursor %}
    <a href="/articles?cursor={{prev_cursor}}" class="btn btn-default">前へ</a>
    {% endif %}
    </div>
    <div class="col-md-10">
    </div>
    <div class="col-md-1" style="text-align: right;">
    {% if next_cursor %}
    <a href="/articles?cursor={{next_cursor}}" class="btn btn-default">次へ</a>
    {% endif %}
    </div>
</div>

参考

GAE/Go+Datastoreで楽にお安く総件数を取得する