はじめに
お久しぶりです。だいぶ間が開いてしまいました。
前回から色々ありまして、結果として8月から新しい就業先で働いてます。(この辺はまた別の機会に書きます)
で、新しい環境でもAWSに携わっていますが、 コードレビューなどを通して、(知ってたつもりでも)色々知らなかった事や勉強になる事が多いなあ...と感しています。
そこで今回は基本に立ち返るという意味で、ビギナー向けネタとして「DynamoDBデータのページネーション処理」について書こうと思います。
前提:ページネーション処理について
元々は「Webページに長い文章を掲載する際に、同じデザインの複数のページに分割し、各ページへのリンクを並べたもの」という意味です。
が、今回はDynamoDBのデータについて「該当データを一定個数ごとに区切る処理」という意味合いで使います。
例えばGoogle等が「検索結果を一定データ数ごとにページ区切りする」感じです。
DynamoDBのデータ取得の制限
DynamoDBでデータ取得する場合、主にQueryやScanを使うと思いますが(※)、QueryやScanで取得できるデータには「最大1MBまで」という制限があります。
なので「該当データを(データ量や件数に関係なく)全件取得する」という事はできません。
※Getはページネーション自体がいらない(=1件しか取得できない)ので省略。
なので、データ量が多い場合、全データを取得する場合は「何回かに分けて取得する」処理(≒ページネーション処理)が必要になってきます。
基本的なページネーション方法(LastEvaluatedKey, ExclusiveStartKey)
先程のAWS公式サイトの説明にもある通り、DynamoDBのQueryやScanには、戻り値の中に「LastEvaluatedKey」というキーが定義されています。
QueryやScanの実行時に全件取得できなかった場合、この「LastEvaluatedKey」に最後のデータのパーティションキー情報が格納されます。(全件取得できた場合は、そもそも「LastEvaluatedKey」キーが存在しない(※))
そして「LastEvaluatedKey」から続きのデータを取得する方法ですが、QueryやScanには実行時のパラメータに「ExclusiveStartKey」というキーがあり、ここに先程の「LastEvaluatedKey」キーの値を指定することで、前回取得したデータの続きから取得することができます。
※ただし後述の「Limit」設定時に「ちょうど全データ取得できた場合」はLastEvaluatedKeyに値が入ってきます。
例えば、末尾に記載した(僕の記事ではお約束の)「ドルアーガの塔 宝物取得リスト」に対して下記ソースを実行すると...
const AWS = require('aws-sdk'); async function scanSample() { const dc = new AWS.DynamoDB.DocumentClient(); const data = await dc.scan({ TableName: 'tower-of-druaga', Limit: 6 }).promise(); console.log(`[data] ${JSON.stringify(data)}`); const data2 = await dc.scan({ TableName: 'tower-of-druaga', Limit: 6, ExclusiveStartKey: data.LastEvaluatedKey }).promise(); console.log(`[data2] ${JSON.stringify(data2)}`); return; }
結果として、下記ログが書き込まれます。
[data] { "Items": [ // Floor1~6までの情報 ], "Count":6, "LastEvaluatedKey":{ "Floor":6, "Type":"treasure" } } [data2] { "Items": [ // Floor7~10までの情報 ], "Count":4, } /*ちなみに、Limitを5にするとこうなる */ [data] { "Items": [ // Floor1~5までの情報 ], "Count":5, "LastEvaluatedKey":{ "Floor":5, "Type":"treasure" } } [data2] { "Items": [ // Floor6~10までの情報 ], "Count":4, "LastEvaluatedKey":{ "Floor":10, "Type":"treasure" } }
問題点
ただし、先述の通りDynamoDBには「最大1MBまで」という制限があるので、各データの内容によっては件数にばらつきが生じる(=最大1MBを超えない最大件数になる)ことがあります。
例えば取得対象データが下表のような場合、QueryやScanを実行すると取得件数は1回目は5件、2回目は2件となります。(さすがにこれは極端ですが)
No | サイズ(KB) |
---|---|
1 | 200 |
2 | 200 |
3 | 200 |
4 | 200 |
5 | 200 |
6 | 500 |
7 | 500 |
8 | 700 |
もちろんそれが問題にならない場合は良いんですが、例えば「画面表示の関係で、毎回決まった件数を取得したい」というような場合が問題です。
対策
上記問題への対策ですが、QueryやScanの実行時パラメータには他にも「Limit」というものがあり、「取得するデータ件数」を指定することができます。
なのでこれを指定することで、取得件数を固定することができます。(例えば上表の場合、Limit=1にすれば毎回1件のみの取得に固定することができます)
ただしLimitを付けたとしても、「最大1MBまで」の制限はあるので注意です。(例えば上表の場合に「Limit=3」にしても、3ページ目はNo.7の1件のみになる)
これに対しては、下記のような対策があります(設計段階での対策)。
- 「Limit(=1ページに表示する件数) × 1データのデータサイズ」が、1MBよりも余裕をもって下回るようにLimitの値を設定する
- 各データのデータサイズにあまり大きな差が出ないようにしておく
あえて全件取得するには?
ここまでは「何回かに分けて全データを取得する方法」について書きました。
ただし実際にDynamoDBからデータを取得するケースとして「クライアントからAPI Gateway等にリクエストして、そこからLambdaで取得処理を実行して...」というケースも多いと思います。
そしてその場合、何回もリクエストが発生するのを防ぐために「可能であれば1回のリクエスト(=Lambda実行)で全データを取得したい」というケースもあると思います。
その場合は、1回のLambdaの処理で何度もQueryやScanを実行して全データを取得するような処理を(ループ処理や再起処理で)書けばOKです。(サンプルコードを下記に記します)
なおAPI Gateway(AppSyncも)には30秒という時間制限があるので、あまりにも大量のデータを1度に取得しようとすると、タイムアウトが発生してしまうので注意です。(これはRDSでも同じです)
/* * ループで書く方法 */ async function scanSampleLoop() { const dc = new AWS.DynamoDB.DocumentClient(); let items = []; let exclusiveStartKey = null; while(true) { const param = { TableName: 'tower-of-druaga', Limit: 5 }; if (exclusiveStartKey) param.ExclusiveStartKey = exclusiveStartKey; const data = await dc.scan(param).promise(); items = items.concat(data.Items); if (!data.LastEvaluatedKey) break; exclusiveStartKey = data.LastEvaluatedKey; } return; } /* * 再起処理で書く方法 */ async function scanSampleRecursive(exclusiveStartKey = null) { const dc = new AWS.DynamoDB.DocumentClient(); const param = { TableName: 'tower-of-druaga', Limit: 5 }; if (exclusiveStartKey) param.ExclusiveStartKey = exclusiveStartKey; const data = await dc.scan(param).promise(); let items = []; if (data.LastEvaluatedKey) { items = data.Items.concat(await scanSampleRecursive(data.LastEvaluatedKey)); } else { items = items.concat(data.Items); }; return items; }
まとめ
というわけで、今回は基本に立ち返って、ビギナーの方向けに記事を書きました。
冒頭にも書いた通り「今まで知ってたつもりでも、意外と理解しきれていなかった」と感じるところが多かったので、自分の復習も兼ねて、今後もこういったビギナー向けの記事を書くのも良いなあと感じています。
もちろん、ビギナー向けではないような記事も書いていくつもりです。(バタバタも落ち着いて、新しい就業先で働いたのもありますし)
それでは、今回はこの辺で。
参考:「ドルアーガの塔 宝物取得リスト」について
「ドルアーガの塔 宝物取得リスト」は「ドルアーガの塔」というゲームの宝物情報(全60階分)をKey-Value形式でまとめたものです。(ただし、今回は10階まで)
※「ドルアーガの塔」が分からない人はググってみてください。
構造は下記の通りとなっています。
フィールド名 | キーの種類 | 説明 |
---|---|---|
Type | パーティションキー | 格納情報の種類を表す。今回は「treasure」固定 |
Floor | ソートキー | 階数(1~60)が格納される。実質RDBでいう主キー。 |
Detail | なし | 実際の宝物情報(名前、効果、出現条件など)が格納されている。(今回の記事では未使用) |