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で吐き出して共有できる)

参考になったページ

範囲検索について
PKについて
クエリパターン洗い出しの例
設計のベスプラ

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を使う
ログインするとコメントできます