〜エンジニア初学者の開発日記〜 五の巻

こんにちは、つかさです
今回もLaravelのクエリビルダについてまとめていこかなと思います。

## 前提条件

### Usersテーブル
|id|name|email|status|created_at|updated_at|
|:-:|:-:|:-:|:-:|:-:|:-:|
| 1 | John Doe | john@example.com | active | 2024-01-01 10:00:00 | 2024-01-02 12:00:00 |
| 2 | Jane Smith | jane@example.com  | active | 2024-01-03 14:00:00 | 2024-01-04 16:00:00 |
| 3 | Mike Brown | mike@example.com  |inactive| 2024-02-01 08:30:00 | 2024-02-02 09:45:00 |


## 分割処理
対象のレコードが数千・数万件合った場合、一度に取得することが困難や効率が悪いかもしれません。そのような状況下では分散処理メソッドを用いて処理を行う事ができます。

### chunkメソッド
- 1回で取得するレコード数を指定して、クロージャの中でそれらを処理します
- クロージャ内でfalseを返すと処理が中断されます

:::note info
クロージャ(Closure)とは
関数内で特定の変数を参照し続けることで、関数が状態を持てる仕組みのこと。
:::

```php:例
 DB::table('users')->orderBy('id')->chunk(10, function (Collection $chunkedUsers): void {
    
    $chunkedUsers->each(function (object $user) {
        
    });
});
```

この例では、10 件ずつレコードを取得し、クロージャの中でそれらを処理しています。1回ごとに渡されるのはgetメソッド同様Collectionのstdオブジェクトになります。クロージャなので外から参照することはできません。

:::note warn
※注意点
chunkメソッドを使用する際は、必ずorderByメソッドを使用する必要があります。
・なぜ必要なのか？
1.  データの取得順序が保証されない
2.  データの挿入・削除が発生すると、途中のデータが飛ばされる可能性がある

例えば、1回目のクエリで取得した10件のうち、5件削除された場合、次の OFFSET 10 で本来取得するべきデータがスキップされるなど

`Laravel 9`以降ではorderByがないとエラーが起こってしまいます。
:::

・データを格納する場合

```php:例
$allUsers = collect(); // 空の Collection を作成

DB::table('users')->orderBy('id')->chunk(10, function (Collection $chunkedUsers) use (&$allUsers): void {
    $allUsers = $allUsers->merge($chunkedUsers); // 取得データをマージ
});

dd($allUsers);
```

クロージャ内の値は取り出せないので、空のCollectionを用意し、その変数に`mergeメソッド`で格納します。

実行されるSQL
```sql:
SELECT * FROM `users` ORDER BY `id` LIMIT 10 OFFSET 0;
SELECT * FROM `users` ORDER BY `id` LIMIT 10 OFFSET 10;
SELECT * FROM `users` ORDER BY `id` LIMIT 10 OFFSET 20;
...
```

### chunkByIdメソッド
- `->orderBy('id')->chunk()`をシンプルなメソッドにしたもの
並び順がシンプルでPrimary Keyの昇順でいい場合、`->chunkById()`で十分になります

```php:例
 DB::table('users')->chunkById(10, function (Collection $chunkedUsers): void {
    
    $chunkedUsers->each(function (object $user) {
        
    });
});
```

#### 主キー名が同一のテーブルを結合している場合

以下の記述の処理はエラーが起こりますがなぜでしょうか？

```php:例
 $userBuilder->join('posts', 'users.id', '=', 'posts.user_id')
            ->chunkById(10, function (Collection $chunkedUsers): void {
});
```

<details><summary>答え</summary>
エラーの原因としてusersテーブルかpostテーブルのPrimary Keyのどちらのことを示しているのかわからないからです。
</details>

実行されるSQL
```sql:
select * from `users` 
inner join `posts` on `users`.`id` = `posts`.`user_id` 
order by `id` asc 
limit 10
```
このorder byのidがわからないということなので、明示的に指定してあげる必要があります。

chunkById() の第三引数に「ソート対象とする主キー（がどちらの主キーであるか）」を、第四引数にエイリアスを指定してあげることで解決できます。

```php:例
$userBuilder->join('posts', 'users.id', '=', 'posts.user_id')
            ->chunkById(10, function (Collection $chunkedUsers): void {
    }, 'users.id', 'id'); 
```

実行されるSQL
```sql:
SELECT * FROM `users`
INNER JOIN `posts` ON `users`.`id` = `posts`.`user_id`
WHERE `users`.`id` > 0
ORDER BY `users`.`id` ASC
LIMIT 10;
```

### chunkMapメソッド
- 結果データを map（個別処理して返す）する際に処理を分割します
- クロージャではなく、Collection を返すメソッド
- 返り値はCollectionの連想配列になる
- orderByメソッドは必要になる

```php:例
$users = $userBuilder->orderBy('id')->chunkMap(fn($user): array => [
    'id' => $user->id,
    'name' => $user->name,
], 100);

dd($users);
```
実行されるSQL
```sql:
SELECT * FROM `users` ORDER BY `id` LIMIT 100 OFFSET 0;
SELECT * FROM `users` ORDER BY `id` LIMIT 100 OFFSET 100;
SELECT * FROM `users` ORDER BY `id` LIMIT 100 OFFSET 200;
・・・
```

```html:
Illuminate\Support\Collection Object
(
    [items:protected] => Array
        (
            [0] => Array
                (
                    [id] => 1
                    [name] => John Doe
                )

            [1] => Array
                (
                    [id] => 2
                    [name] => Jane Smith
                )

            [2] => Array
                (
                    [id] => 3
                    [name] => Mike Brown
                )
            .
            .
            .
)
```

取得したい結果データの個々は小さいが、全体を1つにまとめたい時など、元々の個々データが大きく処理自体は分割したい場面で有効かも

### lazyメソッド
- LazyCollectionによって内部的にchunkを行なってくれるメソッドになります
- チャンクサイズのデフォルトは1000件です

チャンクサイズは5件に指定
```php:例
$userBuilder->orderBy('id')->lazy(5)
```
```html:
LazyCollection {
    all: [
        { "id": 1, "name": "Alice" },
        { "id": 2, "name": "Bob" },
        { "id": 3, "name": "Charlie" },
        { "id": 4, "name": "David" },
        { "id": 5, "name": "Eve" },
        { "id": 6, "name": "Frank" },
        ...
    ]
}
```


実行されるSQL
```sql:
SELECT * FROM `users` ORDER BY `id` LIMIT 100 OFFSET 0;
SELECT * FROM `users` ORDER BY `id` LIMIT 100 OFFSET 100;
SELECT * FROM `users` ORDER BY `id` LIMIT 100 OFFSET 200;
・・・
```

・chunkByIdメソッドのようにlazyByIdメソッドがあり、シンプルに書くこともできます。


#### chunkとlazyの違い

- コールバックを書く必要があるかないか
- 返り値

## まとめ
今回はクエリビルダの分散処理するためのメソッドについて整理しました。大量のデータを扱う時やメモリも節約しなくてはならない時などに有効なのかなと思います。
読んでいただきありがとうございました！