既存アプリに DynamoDB を導入する話のリアリティが高まってきましたので、 以前にも取り上げた以下のパッケージについて深掘りしてみます。
baopham/laravel-dynamodb: Eloquent syntax for DynamoDB
https://github.com/baopham/laravel-dynamodb
README から読み取れる、「RDB のつもりで使うとハマりそうなポイント」を DynamoDB ならではのクセという観点でまとめます。
0. 大前提:Eloquent 完全互換ではない
-
モデルは DynamoDbModel を継承し、「サポートされている Eloquent メソッドだけ」 が使える、という書き方になっています。
→ 「Eloquent と同じインターフェイスだけど中身は DynamoDB」であり、RDB 用 Eloquent と同じつもりで全部の機能が動くと思わない方が安全です。
1. クエリまわりのクセ
1-1. where → Query か Scan かが自動で変わる
- where() したとき、指定したキーが プライマリキー or インデックスで、かつ Query でサポートされる条件なら DynamoDB の Query、それ以外は Scan を使う、と書かれています。
- RDB だと「とりあえず where 書けば DB がよしなにインデックスを選ぶ」ですが、DynamoDB では インデックス設計と where の書き方で、突然全表 Scan になりコスト・性能が激変します。
- 「Eloquent 風に書けるけど、実際に Query になるか Scan になるか」は自分で意識する必要あり。
1-2. whereNull / whereNotNull の意味が違う
- README に注意書きがあり、whereNull() / whereNotNull() は 「値が NULL かどうか」ではなく「属性が存在するかどうか」 を見ているだけ、と明記されています。
- SQL の IS NULL と同じ感覚で使うと挙動がズレます。
- 「属性自体が無い」= NULL 判定、という DynamoDB 仕様に合わせる必要があります。
1-3. all() / first() は Scan(全表スキャン)前提+1MB 制限
- all() は Scan を使い、「DynamoDb will only give 1MB total of data」とコメントされています。
- RDB の Model::all() の感覚で「全部取れるはず」と思うと、1MB 超えた分は返ってこないので危険。
- first() も「limit 1 の Scan」扱いなので、必ずしも「最も古い/新しい」といった意味順になっているわけではないです。
1-4. offset / ページングの概念が違う
- README に「offset of how many records to skip does not make sense for DynamoDb」とあり、ページングは after($last) / afterKey() を使って「前ページの最後の項目から再開」する形で実装するよう書かれています。
- RDB の skip() / offset / forPage() 的な発想は使えません。
- 「前回取得した最後のレコードのキーを持ち回る」という カーソル型ページング前提です。
1-5. limit() / take() も 1MB 制限に注意
- limit() / take() についても「DynamoDB has a limit of 1MB so if your limit is very big, the results will not be expected」と注意書き。
- RDB のように「limit(10000) なら 10000 行返ってくる」とは限らない。
- 1MB で途中までしか返らない可能性がある前提で設計する必要があります。
2. キー設計・インデックス周りのクセ
2-1. 自動インクリメント ID は使えない
- README で「DynamoDb doesn’t support incremented Id, so you need to use UUID for the primary key」と繰り返し書かれています。
- つまり id の auto increment はないので、自分で UUID(や他の一意 ID)を振ってから save する必要があります。
- Laravel/Eloquent の $incrementing $keyType の感覚そのままでは動きません。
2-2. インデックスはモデル側に定義+順序にも意味がある
- protected $dynamoDbIndexKeys プロパティで、テーブルのインデックス(GSI/LSI)のハッシュキー/レンジキーを宣言します。
- README には 「インデックスの順番が重要」 と明示されていて、同じキーが複数インデックスに含まれる場合、定義順によってどのインデックスが使われるかが変わる例が載っています。
- RDB だと「どのインデックスを使うかは DB のオプティマイザ任せ」が一般的ですが、このパッケージでは モデル側の定義順が実際に挙動を変えるので要注意。
- 明示的にインデックスを指定したい場合は withIndex(‘index_name’) を使う必要があります。
2-3. 複合キー(Composite Keys)の扱い
- 複合キーを使う場合は protected $compositeKey = [‘customer_id’, ‘agent_id’]; のように配列で定義し、find() も配列で渡す必要があります。
- Eloquent(RDB)標準では複合主キーは公式サポートされていませんが、このパッケージは独自のやり方でサポート。
- その代わり、find() 周りのインターフェースが通常の Eloquent と違うので、共通化しすぎるとハマりやすいです。
3. NULL・属性削除のクセ
- さきほどの whereNull() の話に加えて、属性削除は removeAttribute() を使い、DynamoDB の UpdateExpression の REMOVE を内部で組み立てる形です。
- RDB の UPDATE … SET column = NULL とは概念が違い、「属性そのものを消す」という世界観になるので、「欠損」と「NULL値」が混ざったデータモデルにしないよう注意が必要です。
4. 非同期処理(Async)のクセ
- このパッケージは AWS SDK v3 の Guzzle Promise を使った非同期処理に対応しており、updateAsync() / saveAsync() / deleteAsync() などが用意されています。
- ただし README のサンプルの通り、->wait() しないと実際の完了を待ちません。
- 「非同期メソッドを呼べばもう保存済み」と思い込むと、後続処理が先に走って不整合の原因になります。
5. マイグレーション・ファクトリ周りのクセ
README の FAQ より:
- マイグレーション
- 「How to create migration? → issue を参照」となっており、通常の Schema::create() でテーブル作成…とは別世界です。
- 実際のテーブル定義は CloudFormation / CDK / SDK など DynamoDB 側で行う前提なので、RDB と同じ感覚で「全部 Laravel の migration に集約」はできません。
- Factory
- 「How to use with factory? → issue 参照」となっており、Laravel 標準の ModelFactory との組み合わせにも独特の工夫が必要になります。
6. ざっくりまとめ(RDB Eloquent との違いイメージ)
RDB Eloquent と比べて、特に意識した方がいいポイントを一言ずつにすると:
- all()/first()/limit() が平気で全表 Scan になる(かつ 1MB 制限)
- offset ベースのページングは存在せず、カーソル型(after())で進める
- NULL 判定は「属性の有無」で、SQL 的な NULL とは別物
- 自動採番 ID は無いので主キーは自前(UUID 等)で用意する
- どのインデックスを使うかはモデル定義の順番や withIndex() 次第
- Eloquent のつもりで全部の機能を期待せず、「README に載っているものがサポート対象」と考える