Laravel で複数削除 where (whereIn)+ delete vs destroy はどちらが早いのかを検証してみた。



# はじめに
<!-- 発端や概要を記載 -->
Laravel を使っていて、複数レコードを削除する方法が2つありましたので、
どちらが処理速度早いのかを検証してみました。

携わっていた案件で、ユーザーが選択したものを一括削除する機能があったことがきっかけです。

まあ、該当するidがフロントから送られてきたら、そのidをもとに削除すればいいだけだから、
複雑じゃないし気楽にやるか〜と思っていたところ、
モデルを複数削除する方法って複数あるんやな〜と見つけました。

それでどの処理が良いんだろうかと調査することにしました。


<!-- 各チャプター -->
<a id="#Chapter1"></a>
# Laravel のEloquent 削除処理について

まずは、基本的な削除について改めて確認をします。
なお、検証で使用するlaravel のバージョンは8となっております。

以下例などは、公式docs : laravel Eloquent の準備  から抜粋
https://readouble.com/laravel/8.x/ja/eloquent.html

基本的な削除処理

```php
use App\Models\Flight;

$flight = Flight::find(1);

$flight->delete();
```

該当のモデルを取得後、 delete メソッドで削除ができますね。

流れとしては、
該当のモデルを find で取得し、取得後にデータを削除する 処理となっています。

では、複数削除の場合はどうでしょう。

<a id="#Chapter2"></a>
# Laravel のモデル 複数削除について

パターン1： destroy メソッド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572374/a7ca7f39-96de-47b9-f018-9db74a3bc8f8.png)

1つ目が、destroyメソッドです。

入れる引数は、

- 削除したいid
- 削除したい複数のid
- id の配列
- id (modelでも可) のcollection 
上記となっています。

docs の注意書きを見ると、
```php
モデルの主キーがわかっている場合は、destroyメソッドを呼び出して、モデルを明示的に取得せずにモデルを削除できます。
destroyメソッドは、単一の主キーを受け入れることに加えて、複数の主キー、主キーの配列、または主キーのコレクションを引数に取ります。
```

と書かれており、処理のフローとしては
モデルを明示的に取得せず削除できる と書かれていますね。

ただ、下のNote部分を見ると、
```php
Note: destroyメソッドは各モデルを個別にロードし、deleteメソッドを呼び出して、
deletingイベントとdeletedイベントが各モデルに適切にディスパッチされるようにします。
```
と記載がありますね。
Eloquent のイベント処理を発火させるために、
内部的に、削除する該当モデルは取得して1つずつdeleteメソッドを適用している処理みたいですね。


パターン2： where or whereIn + delete メソッド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572374/1f249f63-8a3f-7d9b-b3f6-30e70abd3720.png)

2つ目が、whereやwhereIn + deleteメソッド です。

Eloquent クエリ 時に where句やwhereIn句で該当のモデルを絞り込んだあと、deleteメソッドで削除する流れですね。

docs の説明とNote を見てみます。

```php
一括更新と同様に、一括削除では、削除されたモデルのモデルイベントはディスパッチされません。

Note: Eloquentを介して一括削除ステートメントを実行すると、削除されたモデルに対してdeletingおよびdeletedモデルイベントがディスパッチされません。
これは、deleteステートメントの実行時にモデルが実際には取得されないためです。
```

destroy メソッドとは違い、Eloquent イベントの発火はしないみたいですね。
となると、この情報からモデル1つ1つをロードしていないことがわかりますね。

2つの処理を比較すると、

:point_up:Eloquent イベント が発火するかどうか:point_up: の違いだとわかります。



<a id="#Chapter3"></a>
# 処理速度の比較

それでは、先ほどのメソッドを使用して処理速度をそれぞれ計測していきます。

計測条件については下記の通りです。

1. laravel のUnit test を使って計測
2. 任意のユーザーデータを1000件を作成し、削除
3. 3回計測し、その平均値をもとに比較


destroy メソッド

```php
/**
 * @group test_delete_speed
 */
public function test_delete_speed()
{
    $userIds = User::factory()->count(1000)
        ->create()->pluck('id');

    $startTime = microtime(true);

    User::destroy($userIds);
    $runningTime =  microtime(true) - $startTime;
    Log::info('destroy: running time: ' . $runningTime . ' [s]');
}
```

計測結果

平均速度：0.54864454269409 [s]

```php
testing.INFO: destroy: running time: 0.55044484138489 [s]  
testing.INFO: destroy: running time: 0.54447293281555 [s]  
testing.INFO: destroy: running time: 0.55101585388184 [s] 
```



where or whereIn + delete メソッド

```php
/**
 * @group test_delete_speed
 */
public function test_delete_speed()
{
    $userIds = User::factory()
        ->count(1000)
        ->create()->pluck('id');

    $startTime = microtime(true);

    User::whereIn('id', $userIds)->delete();
    $runningTime =  microtime(true) - $startTime;
    Log::info('whereIn+delete: running time: ' . $runningTime . ' [s]');
}
```

計測結果

平均速度：0.011776367823283 [s]

```php
testing.INFO: whereIn+delete: running time: 0.011615991592407 [s]  
testing.INFO: whereIn+delete: running time: 0.011584043502808 [s]  
testing.INFO: whereIn+delete: running time: 0.012129068374634 [s]  
```

比較してみると

1. destroy  => 0.54864454269409 [s]

2. where or whereIn + delete  => 0.011776367823283 [s]

以上のことから **where (whereIn) + delete の方が早い** とわかります。

ではなぜ処理速度に差が出るのか詳しく確認してみます。

<a id="#Chapter4"></a>
# メソッド内部処理を見てみる

**destroy**
vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
```php
/**
 * Destroy the models for the given IDs.
 *
 * @param  \Illuminate\Support\Collection|array|int|string  $ids
 * @return int
 */
public static function destroy($ids)
{
    if ($ids instanceof EloquentCollection) {
        $ids = $ids->modelKeys();
    }

    if ($ids instanceof BaseCollection) {
        $ids = $ids->all();
    }

    $ids = is_array($ids) ? $ids : func_get_args();

    if (count($ids) === 0) {
        return 0;
    }

    // We will actually pull the models from the database table and call delete on
    // each of them individually so that their events get fired properly with a
    // correct set of attributes in case the developers wants to check these.
    $key = ($instance = new static)->getKeyName();

    $count = 0;

    foreach ($instance->whereIn($key, $ids)->get() as $model) {
        if ($model->delete()) {
            $count++;
        }
    }

    return $count;
}
```
データをwhereInで取得したのちに、1つずつforeach で 削除を行なっているみたいですね。
確かに、foreachでまわす処理を内部的に行なっているのであれば、処理が遅くなりますね。


**delete**
vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php
```php
/**
 * Delete records from the database.
 *
 * @return mixed
 */
public function delete()
{
    if (isset($this->onDelete)) {
        return call_user_func($this->onDelete, $this);
    }

    return $this->toBase()->delete();
}
```
delete メソッドは内部処理を追いかけるとかなりのファイル量になるので一旦は上記までを表示します。

最終的にはSQLのdelete を行なっており、該当データに対して取得などはせず削除する処理が行われます。

その違いで速度処理が違うみたいです。


<a id="#Chapter5"></a>
# まとめ

一括でデータを削除する場合は、

処理速度の観点から where or whereIn などのクエリ文でデータを絞り込んだあと、
deleteメソッドで削除した方が良いですね！

ただ、Eloquent イベントを発火させたい場合はdestroyを指定する必要がありますね。

そのため、用途によって使い分ける形になりますが、
基本的にはdeleteをメインで使っていければと思います。

ここまでお読みいただきありがとうござました。

<a id="#reference"></a>
# 参考文献
- [公式Laravel Docs](https://readouble.com/laravel/8.x/ja/eloquent.html)