DynamoDB Pagination:利用 LastEvaluatedKey 與 ExclusiveStartKey 實作分頁
11 Nov 2022如何為 Dynamodb 做分頁呢?本文主要分三個部份:(1) 為什麼要做分頁?;(2) 利用 LastEvaluatedKey 與 ExclusiveStartKey 實作分頁;(3) 範例。
為什麼要做分頁?
為什麼要做分頁 (pagination)?為什麼不一次把整個資料庫都讀出來就好?
如果資料量很少,一次全部都翻過一次拿出來給大家展示看看,是可行的;但若資料量很大,翻一次要花很多時間,在讀取過程中除了耗時也花錢,而 AWS 設定了每次存取上限是 4KB,讓我們無法無限制的讀取資料。
為了降低成本,將資料庫切成多個 segmenet 來分次讀取,是比較好的解法。注意,在這裡提到的分頁不是使用者介面的分頁,而是指 DynamoDB paginates the results from Scan operations。
利用 LastEvaluatedKey 與 ExclusiveStartKey 實作分頁
如果本次查詢 (1) 得到的結果超過預設的上限 4KB,或是 (2) 超過自己給定 Limit 的上限,DynamoDB 就會回傳 LastEvaluatedKey,利用 LastEvaluatedKey 設定 ExclusiveStartKey 來指定下一次存取的起始點,就能實作分頁功能。
白話來說,我們可以把 LastEvaluatedKey 和 ExclusiveStartKey 想像成 cursor,告訴我們現在掃到資料庫的哪一筆資料。LastEvaluatedKey 告訴我們最後看過的是哪一筆資料,而 ExclusiveStartKey 告訴我們要從「哪裡的下一筆開始」。
實際實作的方式是這樣的…
- 每次查詢帶入 ExclusiveStartKey,如果沒有就不要帶,或是帶 0、null。
- 怎麼判斷要不要繼續查找資料表呢?根據回傳的結果有無 LastEvaluatedKey 來決定。
- 若有 record 仍需要被檢視,會回應 LastEvaluatedKey,下一次查詢將 ExclusiveStartKey 的值帶入上一次 LastEvaluatedKey 回傳的結果,然後再次做 scan operation。
- 回傳的結果沒有 LastEvaluatedKey 時,表示沒有更多資料需要看了。
- 結論:若有 LastEvaluatedKey 表示 table 仍有尚未檢視的 record,若無則表示都看完了。
在這裡要注意的是,資料庫是隨時變動的,有可能在切換分頁時得到更多或更少的結果,導致分頁與預期不同,因此分頁到底在哪一頁,建議不要用回傳的資料量來決定,而是要用 LastEvaluatedKey。
範例
架構
如下圖,在此範例會透過 API Gateway 觸發 Lambda function 來讀取 Dynamodb table。
利用 DynamoDB 建立資料表
首先,利用 DynamoDB 建立甜點資料表 snack_list。這個資料表有 4 個欄位,分別是 id (primary key,這裡是用建立此筆紀錄時的 timestamp)、name (甜點店名)、phtotURL (圖片網址)、tags (標籤,幫店家做分類)。
資料表 snack_list 如下圖所示。
如果不知道怎麼用 DynamoDB 建立一個甜點資料表,可以參考這個手把手教學。
撰寫 Lambda 讀取資料表
再來,寫一支 Lambda function 來讀取資料表,選取資料表中標籤為「蛋糕」的 record。
const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });
const ddb = new AWS.DynamoDB({ apiVersion: '2012-08-10' });
exports.handler = async (event) => {
const {
queryStringParameters: { startKey },
} = event;
const ExclusiveStartInfo = !!startKey ? { ExclusiveStartKey: { id: { S: startKey } } } : {};
const params = {
TableName: 'snack_list',
FilterExpression: 'tags = :tags',
ExpressionAttributeValues: { ':tags': { S: '蛋糕' } },
Limit: 5,
...ExclusiveStartInfo,
};
const body = await getDataFromDB(params);
return {
statusCode: 200,
body: JSON.stringify(body),
};
};
const getDataFromDB = async (params) => {
return new Promise((resolve, reject) => {
ddb.scan(params, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
說明
- 由於之後會從 API Gateway 帶入 query string 來傳入 ExclusiveStartKey,因此會從 event 的 queryStringParameters 取得 startKey 來帶入 ExclusiveStartKey,其下一筆就作為之後讀取資料表的起點。
- Limit 為每次要去 table 抓的資料數,不是回傳的資料數。例如:Limit 為 5 時,第一次會去抓第 1 ~ 5 筆資料,並回傳 LastEvaluatedKey 在最後檢視的資料上,也就是第 5 筆 (id 為 1667273809990)。並且,根據查詢條件,只會回傳符合條件的 1 筆資料。
如果不知道怎麼撰寫 Lambda function,可以參考這個手把手教學。
第 1 次抓資料
由於是第一次抓資料,不帶入 ExclusiveStartKey,得到結果如下,其中 LastEvaluatedKey 是 1667273809990,掃過我們在 Limit 設定的 5 筆資料,回傳符合條件的 1 筆資料。由於有回傳 LastEvaluatedKey,表示資料表有東西還沒看過,稍後繼續掃剩下的部份。
{
"statusCode": 200,
"body": {
"Items": [
{
"id": {
"S": "1667273756197"
},
"name": {
"S": "艾媽咪"
},
"tags": {
"S": "蛋糕"
}
}
],
"Count": 1,
"ScannedCount": 5,
"LastEvaluatedKey": {
"id": {
"S": "1667273809990"
}
}
}
}
第 2 次抓資料
繼續第二次抓資料,ExclusiveStartKey 帶入 1667273809990
,檢視第 6 ~ 10 筆的資料,得到結果如下,其中 LastEvaluatedKey 是 1666341492264,掃過我們在 Limit 設定的 5 筆資料,回傳符合條件的 4 筆資料。由於有回傳 LastEvaluatedKey,表示資料表有東西還沒看過,稍後繼續掃剩下的部份。
{
"statusCode": 200,
"body": {
"Items": [
{
"id": {
"S": "1667273786503"
},
"name": {
"S": "藍門咖啡"
},
"tags": {
"S": "蛋糕"
}
},
{
"id": {
"S": "1667273636196"
},
"name": {
"S": "深夜裡的法國手工甜點"
},
"tags": {
"S": "蛋糕"
}
},
{
"id": {
"S": "1667273921641"
},
"name": {
"S": "生活在他方"
},
"tags": {
"S": "蛋糕"
}
},
{
"id": {
"S": "1666341492264"
},
"name": {
"S": "老法的酒窩"
},
"photoURL": {
"S": ""
},
"tags": {
"S": "蛋糕"
}
}
],
"Count": 4,
"ScannedCount": 5,
"LastEvaluatedKey": {
"id": {
"S": "1666341492264"
}
}
}
}
第 3 次抓資料
繼續第三次抓資料,將 ExclusiveStartKey 帶入 1666341492264 後再次 scan,得到的結果如下,並沒有回傳 LastEvaluatedKey,表示整張 table 都看完了,不需要再發 scan 的 request 了。
{
"statusCode": 200,
"body": {
"Items": [
{
"id": {
"S": "1667273860281"
},
"name": {
"S": "一百種味道"
},
"tags": {
"S": "蛋糕"
}
}
],
"Count": 1,
"ScannedCount": 1
}
}
結合 API Gateway
最後,要實作從 API Gateway 帶入 query string 來傳入 ExclusiveStartKey 這部份。
在 API Gateway 這裡,先來測試一下,在「Query Strings」欄位依次帶入 startKey 後按下「Test」按鈕…
表示成功透過 API Gateway 觸發 Lambda function 讀取資料表了。
deploy 這個 API 後,拿到 path https://xxx.us-east-1.amazonaws.com/dev/snacks?startKey=<id>
,如下圖 Postman 打 API 的方式,接下來就能在 UI 用這樣的解法來取得甜點資料表裡面的資料了。
如果不知道怎麼透過 API Gateway 觸發 Lambda function,可以參考這個手把手教學。