DynamoDBを使ったデータ設計の反省点2020
2020年に取り組んだ案件の一つですが、サーバレスで検索アプリを開発していました。インフラはAWSを採用していたため、ほぼ必然的な運びでデータストアはDynamoDBを採用しています。業務アプリケーションではデータ設計が重要で、データ設計の良し悪しでアプリケーションの実装がシンプルor複雑になります。本記事はDynamoDBのデータ設計の反省点とこうすればよかったな、という振り返り記事です。
アプリケーション概要
映画館のシートような縦×横の空間に対して、人を割り当てるためのアプリケーションです(設定はダミーですが本質的には似ている構造です)。
制約
- 席の空間は予め決まっている
- 席は前(最前列)から詰めて座っていく
- 各席には座れる人の区分がきまっている(ジュニア/成人/シニア)
- 各席は事前に予約することもできるし、映画館の現地で予約なしに直接席を決めることもできる
- 事前に予約した場合も、最終的には現地で席を確定させる
- 30分前までには映画館内で着席していないといけない。そうでない場合は確定した席は取り消しされて、予約可能な席になる。別の方が確保することが可能
席の状態遷移図
反省点
反省点1:状態を区別するためにアイテムを分離させたこと
事前「予約」と、「確定」/「着席」の状態を区別するために2つのアイテムに分けたこと。これが一番の反省点です。なぜこのアイテム設計にしたか、というと、実は業務アプリを開発する前に、同じチームの別のメンバがPoCを実施していました。私が参画したときにはPoCである程度良好なフィードバックが得られていました。そのときに採用していたデータモデル上、事前「予約」と「確定」/「着席」の状態を別に管理していたため、この分離するモデルを採用しました。
分離したときのデータモデルのキーは以下のようなものです。
- ハッシュキー(
hkey
)- 日付をもつ
- ソートキー(
skey
)- 席の座標や状態を持つ
例えば日付が 20210101
で座標が縦=A、横=1の席で「空」の場合は以下の2つのアイテムを持ちます。ソートキーは複数の条件で絞り込みできるように複合キーになっています。
hkey |
skey |
... (その他の属性) |
---|---|---|
20210101 | YOYAKU#UNUSE#A#1 | ... |
20210101 | KAKUTEI#UNUSE#A#1 | ... |
- 日付が
20210101
で座標が縦=B、横=2の席で「確定」の場合
hkey |
skey |
... (その他の属性) |
---|---|---|
20210101 | YOYAKU#USE#B#2 | ... |
20210101 | KAKUTEI#USE#B#2 | ... |
まずRDBやDynamoDBといったKey-Valueストアに関わらず、 基本的な原則として状態をもつアイテム(レコード)は1つにするべき です。状態が2つ以上のアイテムに分かれていると管理が大変になって、アプリケーションが複雑になります。PoCではfeatureの機能を開発、動作させてフィードバックをもらうことが主眼ですが、プロダクションに採用するアプリ開発となると別です。保守運用しやすい、追加の開発コストが低いといった、単に機能だけでなく、別の重要な観点があります。データモデルはフラットに検討して、PoCの結果に引きづられるべきではありませんでした。
弊害
この分離モデルを採用したことの一番の弊害は、状態の把握が複雑になることです。データストアで把握すべき状態が事実上2倍になっています。特にある席を予約していたが、確定時は別の席を選択した場合は、もとの「予約」の席を未使用にする。確定として選択した席を「確定」にするだけでなく、別の人からの予約の対象にならないように、「確定」した席に対して「予約」の割当を行う必要があります。本来は不要な複雑性を持ち込んでいて、アプリケーションが必要以上に複雑になりました。保守性が悪く、これは大きな反省点です。
こうすべき
状態は1つの属性(キー)にまとめるべきです。今回の場合は、状態を「空、予約、確定、着席」という状態とすれば、必要十分でした。
hkey |
skey |
... (その他の属性) |
---|---|---|
20210101 | CYAKUSEKI#A#1 | ... |
20210101 | KAKUTEI#A#2 | ... |
20210101 | KAKUTEI#B#1 | ... |
20210101 | YOYAKU#B#2 | ... |
20210101 | EMPTY#C#1 | ... |
ここがつらい
つらみ1:ソートキーの複合キー
DynamoDBはRDBとはデータモデルが異なります。柔軟な検索クエリを投げることができないため、RDB以上にデータモデルが重要になります。RDBであれば、検索条件の追加、別テーブルのJOINなどSQLを用いて柔軟に検索できますが、DynamoDBではクエリはソートキーに対してのみ実施できます。その他の属性はデータの絞り込みには使えません。フィルターはデータを取得したあとに、結果を落とします。
ただし、座標をキー(ハッシュキーorソートキー)に含めないとアイテムが一意にならないため、座標をキーに含める設計は必須です。かつ状態(空、予約、確定、着席)によるクエリも必須ですので、状態もキーの一部に含める必要があります。
結果として以下のようなソートキーとなりました。DynamoDBで複雑なクエリを処理しようとすると、どうしてもソートキーが複雑になってしまい、アプリケーションのコードが複雑になるのがネックですが仕方がありません。
つらみ2:トランザクション管理
DynamoDBを使う上での制約ですが、トランザクションが最大25アイテムまで、という制約がつらいです。アプリケーションとしては数百件のレコードをトランザクショナルに処理したいのですが、DynamoDBでは実現できません。結局現在のところ、アプリケーション側でバッチインサートを行って、バッチインサート中にエラーが発生した場合はロールバックする、という処理をアプリケーションのコードで実装しています。ただしロールバック中にエラーが発生する可能性があり、この場合は運用でカバーする必要があるなど運用が複雑になってしまいました。
つらみ3:ソートキーの更新
他のつらみに比べるとだいぶ細かいで小さなことですが、ソートキーは更新できないので、アイテムを一度削除して、更新後のアイテムを追加する、という操作がDynamoDBでは必要です。まぁこの程度であれば、アプリケーション側でラッパーでも挟めばいいので、そこまでつらみではないです。
つらみへの対処
結局のところ、複雑なクエリやトランザクションをDynamoDBだけで処理しようとすると、どうしても複雑性がアプリケーションに漏れ出してしまう、と感じています。冒頭ではサーバレス環境ではほぼ必然的にデータストアはDynamoDB一択と言いましたが、2021年1月現在では、RDS Proxy+Amazon Aurora( Serverless)などの選択肢も十分考えられると思います。複雑性をインフラ側にも押し込むかアプリで吸収するのか、インフラ側に一部を寄せたときの可用性、運用保守性はどうか?など考慮するポイントは多いですが、設計しがいがあるポイントです。