Open9
DynamoDBのめも
DynamoDBのリソース管理
Cfnで管理するとよさそう。(serverless.yamlとかにのせないほうが楽)
Parameters:
Suffix:
Description: Suffix
Type: String
Default: dev
みたいにして、テーブル名を
TableName: !Sub Hoge_${Suffix}
とすると、devやstageでわけられる。
実際の運用
実はDynamoDBは1リージョンに1つになる。さらにテーブルは256個までしか持てない。
Suffixのつけかたとして、devなどは検証用にしてfeature的なブランチをいじる間は
- ブランチごとに切る
- 個人名で切る
- 開発用で1つだけにする
みたいな方法があるが、前者の2つを採用する際は近くのリージョンで開発は行うみたいなハックもある。
インデックスとテーブル設計とか
AWSによると1アプリケーションに1テーブルを持ってしまうのがいい設計っぽいけど、実際には正規化して持った方が解釈しやすくてよい。
インデックス設計
- 基本LSIは使わず、GSIを使えばよさそう
- GSIは1テーブルに1つくらいまで
- もっとほしいときはInvertedIndexといって、インデックス用のテーブルを用意するといい
- あとはSKでなんとかならないときはフロント側でソートするのもあり
- インデックス使わないでPKを文字列連結の方式で持つのもあり(user_id+created_atをPKにするとか)
注意点
- インデックス(PKモ)は頻繁にアクセスされる、かつ、値が分散するようなattributeにつけること
- PKは範囲検索できない(完全一致)
- SKとくみあわせてもいいが、SKは種類の少ない値(is_hogehogeとか)だと非効率
- あとSKは必ず順序ある値にするべき
- 一度にとれる項目サイズに上限があるので、あまりにも長い値を入れまくるのはまずい
- batchGetとかscanは100件くらいしか一度にとってくることができないのでページングを考える必要がある
- DDBのスキーマを変えるときはテーブル作り直しになるので最初の設計が重要。データ入っちゃってるときはデータをバッチとかで移行してから作り直すみたいな必要あり
いっぱいひくときのtips(インデックスとかでなんとかしない方法)
- メモ化してひきなおさない
- キャッシュする
- join して S3 に入れてしまう
- lambda のメモリ上にもつ
設計手法
- ER図をかく
- クエリパターンを洗い出す(物理的にどうアクセスするか)←ここがDDB使うときは超重要で、最初にクエリパターンからSKやGSIの設計、正規化どこまで崩すかの設計をすべき
- あとはテーブル定義書に起こすとか、クエリ条件をちゃんと書き出すとかするといい
- NoSQLWorkBenchを使うと設計が楽(jsonで吐き出して共有できる)
参考になったページ
Dynamoの使い所
- 容量無制限
- オートスケーリング簡単
- カラム追加楽
e2eテストtips
- DynamoDBはDynamoDB localを使ってもいいし、本番を使っても良い(本番の方がいい文脈もあった気がする)
- テストのbefore系でレコードinsert、テストのafter系でレコードall deleteすると何度テストを走らせても良い感じ
batchGetとQuery
- だいたい挙動は同じ
- batchGetの方が一度にとってこられるアイテムの容量が大きい。batchGetは一貫性のある読み取りはサポートしていない
- batchGetはgetをbatchするので、PKもSKもいる
- QueryはSKなくても叩ける
DynamoはgetをするときにSKも指定するとSKなしでひっぱってくるができないのがちょっとめんどくさいわね
SK
- PKだけで一意にならない時に使う
- 日付などのように順序があって、かつ値がばらけるようにするとよい
- PKで一意なのにつけるとgetするときめんどくさい
あるAttributeの値を重複なしで、日付最新のものだけとりたい
- PK: user_id
- SK: created_at
- is_happy
みたいなテーブルがあったとする。残念ながらSQLみたいにDynamoDBのクエリでタイトルを実現することはできないので、まずQueryでPKで絞ってとってきたあとに手動で調べていく。
方針はMapオブジェクトをuser_idをキーにして作って、user_idがすでにMapにあったらcreated_atを比較してis_happyを上書きする、みたいな感じ。一発でfilterとかかけられる気もするけど…。
const is_happy_map = new Map<string, {user_id: string, created_at: Date, is_happy: boolean}>();
みたいなマップを定義する。型の付け方が<key, value>みたいにつけられるし、valueの中はキー名:キーの型みたいに定義できることに注意。
あとここで型をつけないと補完がきかなくて悲しい気持ちになる。
for (const record of 取得したレコードセット){
if(is_happy_map.has(user_id) && dayjs(is_happy_map.get(user_id).created_at).unix() > dayjs(record.created_at){
continue;
}
is_happy_map.set(user_id, user_id: record.user_id, created_at: record.created_at, is_happy: record.is_happy)
}
みたいな感じで更新していく。
追加でfilterしたりsortしたりしたら良い感じに使いやすい形になりそう。
TypeScriptでutil的なのを作る
- dyanamo-easyとかを使う場合の疑似コード
read/writeもいくつかにわけてbatchRead / batchWriteをする
read
async function getAllDatas<T>(){
chunks = []
for iから全部の長さまで、{
25とか50ずつallをsliceしてchunksに入れる
}
const result = Promise.all(chunks.map(v => batchGetでvをとる))
result.flat()
}
数の制限
- getは1つ
- transactGetItemsは25アイテムがmax
- BatchGetItemは100アイテムがmax
- queryはPKでしぼる感じ
- getするときはPKもSKも(あれば)必要
- Scanは最大1MBなのでそれを超えたらLastEvaluatedKeyを使う