Laravelのバリデーションはexistsもinも遅かった

# Laravelのバリデーションexistsルールもinも遅かった

## データを用意する

- 投入データを作成
- 今回は数だけ欲しいので郵便番号を使います

```
$ curl -O http://www.post.japanpost.jp/zipcode/dl/roman/ken_all_rome.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 1774k  100 1774k    0     0  4373k      0 --:--:-- --:--:-- --:--:-- 4370k
$ unzip ken_all_rome.zip
Archive:  ken_all_rome.zip
  inflating: KEN_ALL_ROME.CSV
$ cat KEN_ALL.CSV | cut -d, -f3 | sed 's/"//g' > zip_code.csv
```

- テーブル定義も郵便番号程度のテーブルを用意
- ユニーク制約をかけても11万件超あるので検証内容としては十分

```sql
mysql> show create table zip_codes\G
*************************** 1. row ***************************
       Table: zip_codes
Create Table: CREATE TABLE `zip_codes` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `code` int(11) DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> load data local infile '/path/to/zip_code.csv' into table zip_codes fields terminated by ',' (code);
mysql> select count(*) from zip_codes;
+----------+
| count(*) |
+----------+
|   119900 |
+----------+
1 row in set (0.08 sec)
```

## existsルールで1000件ほどのパラメータをバリデーション

- まずは1000件ほどをワイルドカードで指定

```php
        $params = \App\ZipCode::take(1000)->get()->toArray();

        $bench = new \Ubench;
        $bench->start();
        $validator = \Validator::make($params, [
            '*.code' => 'required|integer|exists:zip_codes,code',
        ]);
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
```

- 結果、1000件ほどのバリデーションに2secもかかっていしまうことがわかります
    - existsルールは要素ごとにSQLを発行しているため低速です
    - また、ワイルドカード指定していることも低速な理由の一つです

```
[2018-03-18 03:16:32] local.INFO: 2.292s
```

- ワイルドカード指定でなく、項目ごとのバリデーションを作成します

```php
        $params = \App\ZipCode::take(1000)->get()->toArray();

        $bench = new \Ubench;
        $bench->start();
        $rules = [];
        foreach ($params as $i => $param) {
            $rules[$i . '.code'] = 'required|integer|exists:zip_codes,code';
        }
        $validator = \Validator::make($params, $rules);
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
```

- 結果、1sec近くバリデーションにかかる時間を削減できました
    - ワイルドカード指定は都度、要素の特定を行っているため低速です

```
[2018-03-18 03:20:02] local.INFO: 1.468s
```

## in条件で1000件のバリデーション

```php
        $params = \App\ZipCode::take(1000)->get()->toArray();
        $all_codes = implode(',', array_column($params, 'code'));

        $bench = new \Ubench;
        $bench->start();
        $rules = [];
        foreach ($params as $i => $param) {
            $rules[$i . '.code'] = 'required|integer|in:' . $all_codes;
        }
        $validator = \Validator::make($params, $rules);
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
```

- 結果、existsよりもパフォーマンスが低下することがわかります
    - 内部でin_arrayを使っているため、要素が多いほど検索対象が配列後方にあるほど低速です
    - そもそも、すべての郵便番号を指定できてない状態でこれです

```
[2018-03-18 03:28:40] local.INFO: 6.898s
```

## カスタムバリデータを作ってissetでバリデーション

- まずはカスタムバリデータクラスを作成

```php
<?php
namespace App\Validator;

class ZipValidator extends \Illuminate\Validation\Validator {

    public $all_codes = [];

    public function validateIsset($attribute, $value, $rule, $messages)
    {
        $property = $this->{$rule[0]};
        return isset($property[$value]);
    }
}
```

- DBから取得した結果セットをarray_columnを使って郵便番号をキーにした連想配列に変換
- 作成した連想配列をValidatorのプロパティにセット
- セット後にバリデーションを実施

```php
        $params = \App\ZipCode::take(1000)->get()->toArray();
        $all_code = array_column(\App\ZipCode::select('code')->get()->toArray(), 'code', 'code');

        $bench = new \Ubench;
        $bench->start();
        $rules = [];
        foreach ($params as $i => $param) {
            $rules[$i . '.code'] = 'required|integer|isset:all_codes';
        }
        $validator = \Validator::make($params, $rules);
        $validator->all_code = $all_code;
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
```

- existsやinルールよりも高速になる
- ただし、issetに用いる連想配列を作成するコスト次第ではexistsよりも低速になりえます
    - 今回の例では連想配列を作成するために5secほどかかっています
    - 12万件のエンティティ生成が遅い要因と思われる
 
```
[2018-03-18 04:16:37] local.INFO: 351ms
```