🤷

laravel-dynamodb でデータのsortがしたい

2024/09/18に公開

AWS でのシステム開発をしているとDynamoDBを使用することが増えてきた。
キャパシティユニットのことを気にしつつ、テーブルから大量のデータを取得する時に並べかえを実行するタイミングによっては意図しない結果が返ってくることとその対処方が分かったので残しておく。

環境・前提

  • PHP Laravel
  • DynamoDB
  • baopham / laravel-dynamodb

データの並び順のを気にすることなくDBに問い合わせるだけでいい場合は今回の記事は考慮する必要はない。

忙しい人向けに先に結論

すべてのデータを取得して結合してからsortBy()をする。

$records = new DynamoDbCollection();
$last = null;
do {
    $users = User::after($last)->all();
    $last = $users->last();
    $records = concat($records, $users);
 } while ($last !== null)

$records = $records->sortBy('kana');

function concat($org, $add)
{
    foreach($add as $item) {
        $org->push($item);
    }
    return $org;
}

順を追って説明

Scanのデータ上限について

DynamoDBは一度の問い合わせのクエリに対する回答量の上限が決まっている。
AWS公式ドキュメント DynamoDB でのテーブルのスキャン

同じようにbaophamのREADMEにも書かれている

// Using scan operator, not too reliable since DynamoDb will only give 1MB total of data.
$model->all();

つまり、システムの利用者が登録してデータがどんどん増えていくような環境では表示されないデータが出てきてしまう。

一度だけ取得

// DynamoDbModelを継承したモデルUser
User::all();

そのため、この上限のことを考慮した書き方がそもそも必要。

$last = null;

do {
    $users = User::after($last)->all();
    $last = $users->last();
    // 必要であれば$users->toArray()して、array_merge()して1つにまとめたりする

 } while ($last !== null)

問題のsortBy() (あとで落とし穴に気付いた)

取得の上限のことを考えた書き方をした場合にNoSQLではないDBMSの方が慣れているのでこう書いた。
ユーザーの氏名カナを格納するカラムkanaが存在するとする。

$users = User::all()->sortBy('kana');
$last = $users->last();

do {
    $users = User::after($last)->all()->sortBy('kana');
    $last = $users->last();
    // 必要であれば$users->toArray()して、array_merge()して1つにまとめたりする

 } while ($last !== null)

イメージとしてはscanしたデータはすべて'kana'によってsortされていて、上限分だけ抜き出してくれる感じ。
なので、その後last()でとったレコードをafter()に渡してscanするといい感じにとれるんでしょ?と思っていた、この時は...。

結果を画面に表示させてみるとなんか重複しててレコード数が多い。なんじゃこりゃ。

色々デバッグして1回目の結果だけを表示させたりlast()のレコードをとってみたりしているうちにひらめく。
sortBy()した最後のレコードからscanをし直すから重複分が出るのでは...?

例えばDynamoDBでスキャンした結果は人の目にはくちゃくちゃに見える。
でもDynamoDB的には決まった並べ方なんですけど、とのこと(多分)。

山田太郎,ヤマダタロウ
佐藤花子,サトウハナコ

佐藤花子さんのレコードが1MBの上限だった時、sortBy()をしないとlast()の結果は佐藤花子さんになる。
after()->all()で花子さんの後ろを取得すると次にいる誰かのレコードを得ることになる。

ここでall()->sortBy('kana')を実行すると

佐藤花子,サトウハナコ
山田太郎,ヤマダタロウ

という結果が返ってきて、last()の結果は山田太郎さんとなる。
この結果を使ってafter()->all()をすると、佐藤花子から後ろが取得されるためにデータが重複しているように見えた。

よくよく考えるとall()で返ってくるのはCollection型のものであるため、実際はSQLとしてsortしているのではなくCollectionオブジェクトを並べ替えているので当然ともいえる。

Collections - Laravel 11.x

結論 last()の後にsortByすればいい...と思ったけど違うくね?

つまりこう。

$last = null;

do {
    $users = User::after($last)->all()->sortBy('kana');
    $last = $users->last();
    $users = $users->sortBy('kana');
    // 必要であれば$users->toArray()して、array_merge()して1つにまとめたりする

 } while ($last !== null)

ここで一つ気が付いたのがこの方法では正確なソートではないということ。

すべてのデータを取得してからsortByしないとダメだ

DynamoDB的には決まった順でデータが並んでいる、というこれが落とし穴再び。

先ほどの例で説明すると下記3件のデータがあるとする。
この時、一度に取得できる1MB制限の関係で佐藤さんまでが取得できたとする。

山田太郎,ヤマダタロウ
佐藤花子,サトウハナコ
// ↑ここまで取得できた
鈴木次郎,スズキジロウ

ピンとくる方もいるだろうがこの時の理想の順と実際の順がこれ。

// 理想の順
佐藤花子,サトウハナコ
鈴木次郎,スズキジロウ
山田太郎,ヤマダタロウ

// 実際の順
佐藤花子,サトウハナコ
山田太郎,ヤマダタロウ
鈴木次郎,スズキジロウ

1回目の取得で山田さんと佐藤さんのデータをソートし、その後に鈴木さんのデータを取得して結合しているのでそりゃ理想の順にはならない。

Collectionを結合しよう

LaravelにはCollectionがあり、baopham / laravel-dynamodb もこれを継承しているのだが、どうも相性が悪いらしく用意されたconcat()メソッドが上手く使えなかった。
※型の関係っぽいが詳しい原因究明はしていない

仕方ないので自分で結合する。Laravelのconcat()メソッドの中身を見てみるとループで回してただ順番にpush()しているだけだったので自分で実装した。

やっと結論

そんなわけで実装結果。
Collectionのまま結合して、最後にsortBy()する。

$records = new DynamoDbCollection();
$last = null;
do {
    $users = User::after($last)->all();
    $last = $users->last();
    $records = concat($records, $users);
 } while ($last !== null)

$records = $records->sortBy('kana');

function concat($org, $add)
{
    foreach($add as $item) {
        $org->push($item);
    }
    return $org;
}

できた!

Discussion