DynamoDB の特性を理解しないで開発していたら pagination の実装でつまづいた
前置き
私の勘違いが書かれている可能性がありますが、ご指摘頂けると大変喜びます。特に「いや、それはこうやったらできるよ」という情報をお待ちしております。
あらすじ
Serverless Framework 使って個人サイトを作ろうとしたら DynamoDB で若干手間取ったので後世のエンジニアの為に備忘録を残します。
DynamoDB について
DynamoDB は優れた分散データベースです。バックアップやスケールアップなどに時間を割く必要がないように工夫されたサービスです。なんですが、これまでの RDS や KVS とは注力されている箇所が違うで少し使いづらい部分があります。
パーティションキーの概念
DynamoDB では Pagination がしづらいです。できなくは無いのですが RDS と少し違うところがあります。検索条件にインデックスが含まれていないと正しい件数を取得できません
そしてインデックスを作成するには必ずパーティショニングする必要があります
というわけで実際に動作させて説明したいと思います。
準備
Serveless Framework の migration 機能が便利(再起動するたびにデータがセットされる)なのでこれを利用します。
環境は以下の通り
% node -v v8.11.1 % sls -v 1.26.1
雛形を作成
sls create --template aws-nodejs --path practice-dynamodb && cd $_ touch example1.js example2.js mkdir migrations touch migrations/locations.json
migrations/locations.json を作成
[ { "country": "JP", "cityEn": "Shibuya-ku", "stateEn": "Tokyo", "cityJp": "渋谷区", "createdAt": "2018-04-19 20:10:43", "stateJp": "東京都", "id": "1028337572", "name": "Xiang ni cafe(シャンニーカフェ)" }, { "country": "JP", "cityEn": "Shibuya-ku", "stateEn": "Tokyo", "cityJp": "渋谷区", "createdAt": "2018-04-18 20:08:45", "stateJp": "東京都", "id": "620336124833357", "name": "岡本肉店" }, { "country": "JP", "cityEn": "Shibuya-ku", "stateEn": "Tokyo", "cityJp": "渋谷区", "createdAt": "2018-04-17 15:31:09", "stateJp": "東京都", "id": "429313840761469", "name": "香港ロジ 原宿店" }, { "country": "JP", "cityEn": "Sapporo-shi", "stateEn": "Hokkaido", "cityJp": "札幌市", "createdAt": "2018-04-16 14:07:07", "stateJp": "北海道", "id": "5841572", "name": "5坪 gotsubo" }, { "country": "JP", "cityEn": "Sapporo-shi", "stateEn": "Hokkaido", "cityJp": "札幌市", "createdAt": "2018-04-15 14:07:07", "stateJp": "北海道", "id": "3518106", "name": "SAKANOVA" }, { "country": "JP", "cityEn": "Toshima-ku", "stateEn": "Tokyo", "cityJp": "豊島区", "createdAt": "2018-04-15 14:07:07", "stateJp": "東京都", "id": "447856545563610", "name": "大衆酒場 かぶら屋池袋7号店" }, { "country": "JP", "cityEn": "Sapporo-shi", "stateEn": "Hokkaido", "cityJp": "札幌市", "createdAt": "2018-04-15 14:07:07", "stateJp": "北海道", "id": "52232", "name": "菜もっきりや" }, { "country": "JP", "cityEn": "Sumida-ku", "stateEn": "Tokyo", "cityJp": "墨田区", "createdAt": "2018-04-14 14:07:07", "stateJp": "東京都", "id": "8588811", "name": "立ち呑み 粋" }, { "country": "JP", "cityEn": "Kawasaki-shi", "stateEn": "Kanagawa", "cityJp": "川崎市", "createdAt": "2018-04-14 14:07:07", "stateJp": "神奈川県", "id": "1032997013", "name": "和食と立喰い寿司 ナチュラ" }, { "country": "JP", "cityEn": "Shibuya-ku", "stateEn": "Tokyo", "cityJp": "渋谷区", "createdAt": "2018-04-14 14:07:07", "stateJp": "東京都", "id": "896121", "name": "Bistro ひつじや" }, { "country": "JP", "cityEn": "Takamatsu-shi", "stateEn": "Kagawa", "cityJp": "高松市", "createdAt": "2018-04-13 14:07:07", "stateJp": "香川県", "id": "1028420342", "name": "たべごとや 艸 - そう" }, { "country": "JP", "cityEn": "Osaka-shi", "stateEn": "Osaka", "cityJp": "大阪市", "createdAt": "2018-04-12 14:06:28", "stateJp": "大阪府", "id": "242874420", "name": "魚屋ひでぞう 立ち呑み" }, { "country": "JP", "cityEn": "Sapporo-shi", "stateEn": "Hokkaido", "cityJp": "札幌市", "createdAt": "2018-04-11 14:06:28", "stateJp": "北海道", "id": "1021458277", "name": "鮨角打ち 裏・小樽酒商たかの札幌駅前店" } ]
serverless.yml を修正
service: practice-dynamodb plugins: - serverless-dynamodb-local custom: dynamodb: start: port: 8000 inMemory: true migrate: true seed: true seed: development: sources: - table: locations sources: [./migrations/locations.json] provider: name: aws runtime: nodejs6.10 stage: dev region: ap-northeast-1 resources: Resources: ArticlesTable: Type: AWS::DynamoDB::Table Properties: TableName: locations AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: country AttributeType: S - AttributeName: stateEn AttributeType: S - AttributeName: cityEn AttributeType: S - AttributeName: createdAt AttributeType: S KeySchema: - AttributeName: id KeyType: HASH GlobalSecondaryIndexes: - IndexName: country-index KeySchema: - AttributeName: country KeyType: HASH - AttributeName: createdAt KeyType: RANGE Projection: NonKeyAttributes: - stateEn - cityEn ProjectionType: INCLUDE ProvisionedThroughput: ReadCapacityUnits: '1' WriteCapacityUnits: '1' - IndexName: stateEn-index KeySchema: - AttributeName: stateEn KeyType: HASH Projection: NonKeyAttributes: - id ProjectionType: INCLUDE ProvisionedThroughput: ReadCapacityUnits: '1' WriteCapacityUnits: '1' - IndexName: cityEn-index KeySchema: - AttributeName: cityEn KeyType: HASH - AttributeName: createdAt KeyType: RANGE Projection: NonKeyAttributes: - id ProjectionType: INCLUDE ProvisionedThroughput: ReadCapacityUnits: '1' WriteCapacityUnits: '1' ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1
dynamodb を local に install して動作確認します。
sls dynamodb install
動作確認します。13件のアイテムを取得できていればOKです。
aws dynamodb scan --table-name locations --endpoint-url http://localhost:8000 | jq .Count 13
というわけで locations テーブルにはいくつかのインデックスが存在します。それぞれのインデックスで出来る事と出来ない事を確認します。
Just Do it
dynamoDB を参照するコマンドは大きく分けて scan
, get
, query
の3種類です。
まずはプライマリーキーに対してコマンドを実行してみます。
example1: scan, get, query
create example1.js
const aws = require('aws-sdk'); function getDocClient() { return new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1', endpoint: "http://localhost:8000" }); } function main() { // Good getDocClient().scan({ TableName: 'locations', }).promise().then(console.log).catch(console.error); // Good getDocClient().get({ TableName: 'locations', Key: { id: "1032997013" } }).promise().then(console.log).catch(console.error); // Good getDocClient().query({ TableName: 'locations', KeyConditionExpression: 'id = :id', ExpressionAttributeValues: { ':id': "1032997013" }, Limit: 1 }).promise().then(console.log).catch(console.error); // No Good getDocClient().query({ TableName: 'locations', KeyConditionExpression: 'begins_with(id, :id)', ExpressionAttributeValues: { ':id': "103" } }).promise().then(console.log).catch(console.error); } main();
scan
は全レコードをシーケンシャルに取得します。get
は一意のユニークキーからレコードを一つだけ取得します。
DynamoDB のテーブル設計で考えないといけないのは query
です。上記の結果からHash Key に対して部分一致の query が発行できないことが分かります。
DynamoDB では HASH Key を country:state:city
のような構造にして先頭部分から一致したものを取得することはできません。その場合は RANGE KEY にする必要があります。
example2: セカンダリインデックス
今度は都道府県別に locations を取得したい場合は以下のようにします。
index を使用するので IndexName に 'stateEn-index' と cityEn-index
を指定します。
create example2.js
const aws = require('aws-sdk'); function getDocClient() { return new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1', endpoint: "http://localhost:8000" }); } function main() { // Good getDocClient().query({ TableName: 'locations', IndexName: 'stateEn-index', KeyConditionExpression: 'stateEn = :stateEn', ExpressionAttributeValues: { ':stateEn': "Hokkaido" } }).promise().then(console.log).catch(console.error); // Good getDocClient().query({ TableName: 'locations', IndexName: 'cityEn-index', KeyConditionExpression: 'cityEn = :cityEn AND createdAt >= :createdAt', ExpressionAttributeValues: { ':cityEn': "Sapporo-shi", ':createdAt': "2018-04-16" } }).promise().then(console.log).catch(console.error); // Good getDocClient().query({ TableName: 'locations', IndexName: 'cityEn-index', KeyConditionExpression: 'cityEn = :cityEn AND begins_with(createdAt, :createdAt)', ExpressionAttributeValues: { ':cityEn': "Sapporo-shi", ':createdAt': "2018-04" } }).promise().then(console.log).catch(console.error); // Good getDocClient().query({ TableName: 'locations', IndexName: 'country-index', KeyConditionExpression: 'country = :country', ExpressionAttributeValues: { ':country': "JP", }, ScanIndexForward: false }).promise().then(console.log).catch(console.error); } main();
stateEn-index
は HASH
, cityEn-index
は HASH + RANGE
で作成されています。
stateEn-index は createdAt を含まないのでソート出来ないのですが、 cityEn-index はcreatedAt でソートされています。
このように Pagination が必要となる場合は HASH + RANGE
でインデックスを作成する必要があります。
ただし KeyConditionExpression: 'cityEn = :cityEn AND begins_with(createdAt, :createdAt)'
のように必ず HASH Key を指定しなければいけません。
このように DynamoDB ではデータがパーティションで区切られる前提で設計されています。もし全件をソートするなら country-index
のように全データを同一のパーティションに含める必要があります。たぶんその辺りの事を理解した上で DynamoDB を利用する、しないを考えた方が良いでしょう。この例だとすでに Index を 3つ追加しているので RDS を検討した方が良いのかもしれません。
まとめ
DynamoDB は我々エンジニアにとっては強力な補助ツールとなるでしょう。ただし利用する場合はその特性を良く理解する必要があります。
近年では Serverless Framework という選択肢が出てきていますが、案件によってはそのような選択をできる様にする為に DynamoDB の Table 設計に一度慣れておいた方が良いと思います。
あとそれから
その DynamoDB で個人サイトつくりました。 飲み歩くのが好きなので飲み歩いたお店を記録するサイトです。
こちらからは以上です。