Laravel のコレクションとは

## SQL クエリとコレクションの境界

次の２つは同じ結果を返すが、実行されるSQLクエリが異なる。


```php
$users = User::where('id', '<', 2)->get();  // パターン１
$users = User::get()->where('id', '<', 2);  // パターン２
```

tinker で確認してみる。クエリログを確認するため最初に `DB::enableQueryLog()` を実行する。
この記事にそったプロジェクトを作成するには [こちらの記事](https://qiita.com/mikomey/items/ea3f4126962839581a4f) を参照。

```php
$ php artisan tinker
Psy Shell v0.11.4 (PHP 7.4.11 — cli) by Justin Hileman
>>> DB::enableQueryLog()
=> null

>>> User::where('id', '<', 2)->get()
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
=> Illuminate\Database\Eloquent\Collection {#3587
     all: [
       App\Models\User {#3589
         id: 1,
         name: "渡辺 零",
         email: "hanako.kondo@example.org",
         company_id: 1,
         email_verified_at: "2022-05-25 10:10:22",
         #password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
         #remember_token: "oPnq5DLKtc",
         created_at: "2022-05-25 10:10:22",
         updated_at: "2022-05-25 10:10:22",
       },
     ],
   }

>>> User::get()->where('id', '<', 2)
=> Illuminate\Database\Eloquent\Collection {#3584
     all: [
       App\Models\User {#3662
         id: 1,
         name: "渡辺 零",
         email: "hanako.kondo@example.org",
         company_id: 1,
         email_verified_at: "2022-05-25 10:10:22",
         #password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
         #remember_token: "oPnq5DLKtc",
         created_at: "2022-05-25 10:10:22",
         updated_at: "2022-05-25 10:10:22",
       },
     ],
   }
```
どちらも結果は同じ。
しかし、クエリログを確認してみると２つは異なり、パターン２では where 句がクエリに含まれない。
where メソッドが get メソッドの前にあるか後にあるかで動作が異なっている。

```php
>>> DB::getQueryLog()
=> [
     [
       "query" => "select * from `users` where `id` < ?",
       "bindings" => [
         2,
       ],
       "time" => 1.85,
     ],
     [
       "query" => "select * from `users`",
       "bindings" => [],
       "time" => 0.75,
     ],
   ]

```

where が get の前であれば where は **DBビルダーのメソッド** であり、クエリに where 句が含まれるが、get の後ではクエリに where 句が含まれない。
１行で書くと同じように見えてしまうが、get でコレクションを取得し、where はそのコレクションを絞り込む **コレクションのメソッド** だ。

```php
$users = User::get()->where('id', '<', 2);
// 同じ処理をロジックを分けて書くと
$collection = User::get();                   // SELECT * FROM users の実行
$users = $collection->where('id', '<', 2);   // コレクションの絞り込みで、SQL は実行されない
```


一般的な SQL 構文の良し悪しとしてはクエリ絞り込みがされるパターン１のほうが推奨されている。今回はデータが少ないのでパターン２のほうがクエリ実行速度が速かったが、データが数万件あったとしたら１レコードの結果を得るために全件取得するのは馬鹿らしい。
だが、**「パターン２の書き方をしてはいけない」とは思考停止** してはならない。

## コレクションは使いまわせ

コレクションを理解した上で使用すると、パターン２の真意が見えてくる。


まず、変数 $users にモデルの全オブジェクトをコレクションとして格納する。ここで全件取得の SQL クエリが実行される。

```php3
# php artisan tiker
Psy Shell v0.11.4 (PHP 7.4.11 — cli) by Justin Hileman
>>> $users = User::get()
=> Illuminate\Database\Eloquent\Collection {#4209
     all: [
       App\Models\Eloquent\User {#4176
         id: 1,
         ～～（省略）～～
```
コレクションに対する条件の絞り込みを変えていろいろ取得してみる。
何度繰り返しても、もう SQL クエリは実行されないし（※）、$users そのものは変化しない。
```php
>>> $users->firstWhere('id', 1)->email
=> "hanako.kondo@example.org"

>>> $users->firstWhere('id', 2)->company_id
=> 1

>>> $users->firstWhere('id', 2)->company->name
=> "株式会社 渡辺"

>>> $users->pluck('company_id')->unique()
=> Illuminate\Support\Collection {#3590
     all: [
       0 => 1,
       5 => 2,
       13 => 3,
       21 => 4,
       31 => 5,
       41 => 6,
       46 => 7,
       51 => 8,
       55 => 9,
       60 => 10,
     ],
   }
```
※ 今回の例では、`company->name` を表示したところで、`select * from companies where id = ?` がクエリ実行されている。[Ｎ＋１問題については別の記事で](https://qiita.com/mikomey/items/9f06fb3c5558acf6827b)。


コレクションに対するメソッドの種類は [Laravelドキュメント](https://readouble.com/laravel/8.x/ja/collections.html) を参照。絞り込みや集計などさまざまだ。

#### 使用例

例えば、ユーザーの登録日によって異なるメールを送信するなど。
１年未満とそれ以降にわける場合に、 SQL クエリを２回実行する必要はない。


```php
// ユーザーのコレクションを取得する
$users = User::where('email', '<>', '')->get();
// コレクションを条件によって絞り込む
foreach ($users->where('created_at', '<', now()->subYear()) as $user) {
  // メール送信処理
}
foreach ($users->where('created_at', '>=', now()->subYear()) as $user) {
  // メール送信処理
}
```

間違ったコード。get の位置が微妙に異なる。

```php
// get()がないのでクエリオブジェクトの途中
$users = User::where('email', '<>', '');
// 条件をつけてクエリオブジェクトを完結する
foreach ($users->where('created_at', '<', now()->subYear())->get() as $user) {
  // メール送信処理
}
foreach ($users->where('created_at', '>=', now()->subYear())->get() as $user) {
  // メール送信処理
}
```
変数 $users はユーザーのコレクションではなく、この場合はクエリオブジェクトである。これが意図したコードなら変数名は `$query` とするべきだろう。
クエリオブジェクトなので get() を実行するごとに SQL クエリが走る。しかも、where() はその都度オブジェクトに加算され、２回目の判定は意図したものにはならない。二度判定したいなら、先にオブジェクトのクローンを作成する必要がある。

#### コレクションは統計集計が大得意

とあるテーブル（とそのサブテーブル）のレコードの各種項目に対し、今月の合計、先月の合計、３ヶ月平均、縦計、横計・・・などなどを、１つのレスポンスとして出力しなければならないとき、どのようなSQLクエリを考えるだろうか？　無理やりに１回のクエリを書く？　条件に分けてクエリをいくつも書く？
集計する値によって JOIN するサブテーブルが都度変わってくるようなら、１つのクエリを記述するのはもう無理だろう。

具体的なテーブル構造やコードサンプルは書かないが・・・

* テーブル同士の結びつきは、Eloquent のレレーションを定義する。
* ざっくりとコレクションを取得するシンプルなコードを最初に実行。
* あとは欲しいだけコレクションに対する絞り込みと集計するコードを記述する。

どんな複雑な集計を行っても、絞り込み条件を変えて何度ループを繰り返しても、SQL クエリは最初の１回しか実行されないのだった。