DynamoDBでWebのページングをやりたい

DynamoDBでWebのページングをやりたい:


概要

  • やってみたこと

    • DynamoDBから、ソートキー順にデータを取り出して、ページに分割(ページング)1する。
    • 分割した各ページにWebの画面から飛べるようにインデックスを作りたい。
    • boto3のPaginatorを使って、インデックスに必要なデータを生成する。
  • その結果

    • 単純なクエリに対応できるコードは書けた。
    • クエリにフィルタをかける場合、Paginatorでは対応できなさそう。
以下本題です。


Webのページング

なにをやりたいかというと、この画像を見てもらった方が早いかと思います。
page-navigation.png

こんな風に、検索結果の各ページへ飛ぶリンクを、DynamoDBでうまく作れるか、そして簡単にできるか。試してみました。

なお、この記事の範囲はインデックスに使えるjsonを返すところまでで、フロントエンドについては省略します。


テスト用のデータ

{"itemKey":{"S":"test-record"},"itemSortKey":{"N":"0"},"itemType":{"N":"1"}} 
{"itemKey":{"S":"test-record"},"itemSortKey":{"N":"1"},"itemType":{"N":"2"}} 
{"itemKey":{"S":"test-record"},"itemSortKey":{"N":"2"},"itemType":{"N":"3"}} 
(以下itemSortKey149まで続く) 
こんな感じのデータを使います。パーティションキー"itemKey"は"test-recored"で全件固定に、ソートキー"itemSortKey"は0-149まで、計150件格納しています。項目"itemType"は順に1-2-3を繰り返しています(各itemTypeが50ずつ)。itemTypeはこの記事の最後で使います。


boto3のPaginatorsを使う

pythonでコードを書いていますが、boto3を見てみたら、Paginatorなるものがありました。いかにも役に立ちそうな名前です。

しかし、このPaginatorは、「検索結果をページ分割して全件返す」ので、各ページのインデックスのみが欲しい場合はレスポンスが大き過ぎます。

そこで、Paginatorが生成した各ページの一番後ろのレコードだけを返すように、以下を作ってみます。

indexer.py
class PaginatorIndexer: 
    def __init__(self, *, client): 
        self.client = client 
 
    def buildIndex(self, **queryargs): 
        pageSize = queryargs['PaginationConfig']['PageSize'] 
        pages = self.client.get_paginator('query').paginate(**queryargs) 
 
        # 先頭ページのインデックスを予め含む、先頭ページはkeyを持たせない 
        resultKeys = [{'pageNo':0, 'pageSize':pageSize}] 
        pageNo = 1 # ページ番号 
 
        for page in pages: 
            pageContent = page['Items'] 
            size = len(pageContent) 
 
            # 1ページのサイズに満たない場合はインデックスを作らない 
            if size >= pageSize: 
                keyContent = pageContent[-1] 
                index={'pageNo':pageNo, 'pageSize':pageSize,'key':keyContent} 
                resultKeys.append(index) 
                pageNo += 1 
            # 末尾に空のページが来た場合は最後のインデックスを削除する 
            elif size == 0: 
                resultKeys.pop() 
 
        return resultKeys 
説明すると…

  • クエリパラメータを受け取り、Paginatorからページを取得する。2
  • ページにはページサイズ分の全てのレコードがリストで入っている。
  • 各ページのリスト末尾のレコードを取得し、インデックスとしてリストに追加する。
  • リストのサイズがページサイズ未満の場合、インデックスを作らない。

    • このページが、検索結果の最後のページになるはずです。
  • Paginatorは以下の場合、末尾に空のページを返すので、その場合は最後に追加したインデックスを削除する。

    • レコード件数がページサイズで割り切れる場合

      • ひとつ前に作ったインデックスが最終ページになるので、最後のインデックスは不要です。
    • 検索結果が0件の場合

      • インデックスのリストが空になります。
Paginatorが空のページを返すケースがある。というのがポイントです。

次は、このクラスを使う側を書きます。これはLambdaで動かすつもりなのでLambdaのハンドラにしています。

dynamoIndexer.py
def lambda_handler(event, context): 
 
    dynamodb = boto3.client('dynamodb') 
    indexer = PaginatorIndexer(client = dynamodb) 
 
    indexes = indexer.buildIndex( 
        TableName='page.test', 
        Select='SPECIFIC_ATTRIBUTES', 
        ProjectionExpression='itemKey,itemSortKey', 
        KeyConditionExpression ="itemKey = :keyVal", 
        ExpressionAttributeValues = { 
            ':keyVal':{'S': 'test-record'} 
        }, 
        PaginationConfig={ 
            'PageSize': 10 
        } 
    ) 
 
    return json.dumps(indexes, indent=True) 
返されるjsonはこんな感じ(実際は配列)です。

{"pageNo":0,"pageSize":10} 
{"pageNo":1,"pageSize":10,"key":{"itemKey":{"S":"test-record"},"itemSortKey":{"N":"9"}} 
{"pageNo":2,"pageSize":10,"key":{"itemKey":{"S":"test-record"},"itemSortKey":{"N":"19"}} 
{"pageNo":3,"pageSize":10,"key":{"itemKey":{"S":"test-record"},"itemSortKey":{"N":"29"}} 
(以下pageNo:14まで続く) 
これを元にして、フロントエンドでナビゲーションのリンクを作成します(省略)。

以下はリンクがクリックされた時の処理、目的のページのレコードを取得する場合のクエリです。

# 変数indexにインデックスが入っています 
    queryargs = { 
        'TableName':'page.test', 
        'Select' : 'SPECIFIC_ATTRIBUTES', 
        'ProjectionExpression': 'itemKey,itemSortKey', 
        'KeyConditionExpression' :'itemKey = :keyVal', 
        'Limit':index['pageSize'], 
        'ExpressionAttributeValues' : { 
            ':keyVal':{"S":"test-record"}, 
        }, 
    } 
 
    if 'key' in index: 
        queryargs['ExclusiveStartKey'] = { 
            'itemKey':index['key']['itemKey'], 
            'itemSortKey':index['key']['itemSortKey'] 
        } 
先頭ページ分のインデックスにはkeyがないので、keyが設定されている場合のみExclusiveStartKeyを設定します。pageSizeはLimitに使用します(このクエリのレスポンスは省略)。


うまくいかなかったケース

上の例ではパーティションキーのみの指定でデータを取って来る結果をページングしましたが、もう一歩、今度はフィルターを使ってみたいと思います。

先ほどのlambdaのハンドラのクエリ部分を以下のように書き換えます。

lambda_function.py(抜粋)
indexes = indexer.buildIndex( 
        TableName='page.test', 
        Select='SPECIFIC_ATTRIBUTES', 
        ProjectionExpression='itemKey,itemSortKey,itemType', 
        KeyConditionExpression ="itemKey = :keyVal", 
        FilterExpression='itemType = :filterVal', 
        ExpressionAttributeValues = { 
            ':keyVal':{'S': 'test-record'}, 
            ':filterVal':{'N' : '3'} 
        }, 
        PaginationConfig={ 
            'PageSize': 10 
        } 
    ) 
これをやったら、空の配列が返ってきました。実際には空にしているのは私が作ったPaginatorIndexerクラスですが、Paginatorのレスポンスも、全てのページがページサイズ以下の件数になっています34


続きは次回

どうやら、Paginatorではフィルタまでは扱えないようですね。

では、次回以降で、フィルタを使った検索結果にインデックスを作る方法を模索してみます。



  1. ページングとかページネーションとか呼ばれますが、英語的正しさはとりあえず考えません。 



  2. 今回はクエリがLastEvaluatedKeyを返した場合の動作を考慮していません。また、テーブル内の各レコードが大きい場合は、LSIやGSIを作成して、そちらにクエリを発行すると、キャパシティの消費が抑えられます。 



  3. Paginatorが返すページ数はフィルタなしの場合と同じ数なんですが、各ページに含まれるレコードはフィルタ適用後のものだけ格納されています。結果が空になったのは、全てのページがページサイズ以下になったせいで、データに起因しています。 



  4. どういう動きをしているのか、boto3のソースも探したんですが、まだこの動きの原因に辿り着けていません。 


コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)