Seeder は Laravel の提供する DB(データベース)の初期値生成方法です。Seeder を使うことによって何度もデータを生成することが容易になります。
データベース:シーディング 6.x Laravel
便利なのですが大量のクエリを実行しがちで実行完了までにかかる時間が長くなりがちです。この記事では Seeder が高速になるコードの作り方を紹介します。
まず大筋の方針は実行するクエリの回数を減らすことです。よくある遅いコードの例が次です。記述はシンプルで便利ですが create メソッドによって 1000 回 INSERT 文が実行されます。
factory(User::class, 1000)->create();// 内部で factory の結果を回している
高速化するには次です。
$users = factory(User::class, 1000)->make(); User::insert($users->toArray());// 渡した配列を元に一つの INSERT 文を構築する
このコードは一度の INSERT 文でまとめてデータを挿入するためクエリの実行は一回です。クエリを実行する時の諸々(トランザクションなど)がクエリ実行回数分走っていたところ、一回で済む様になります(大きな一回ですがクエリサイズに依らない処理もあるため早くなります)。
Laravel 接続先の RDB(リレーショナルデータベース)によって実行される INSERT の種別(バルクとかマルチプルとか)が変わりますが、ほとんどの RDB (全部試してない、理論を知らないため断言できませんがおそらく全部)で都度 INSERT を実行させるメソッドを呼び出すよりもまとめて INSERT するメソッドを使った方が早いです。
このまとめて INSERT 方式は高速ですがリレーションとサイズで問題が起きやすくあります。その対策が次です。
この方式でリレーションを壊さずにデータを作るためには適切な主キーらをあらかじめ取得する必要があります。次の様なコードを書くことで実行クエリ数を減らしつつ、リレーション構造を壊さないデータを生成できます。
// あらかじめリレーションテーブルの親のテーブルのデータを初期化する UserSeeder, CategorySeeder を実行しておく public function run(): void { // あらかじめリレーション親の主キー群を配列にして格納 $userKeys = User::get(((new User())->getKeyName()))->flatten()->toArray(); $categoryKeys = Category::get(((new Category())->getKeyName()))->flatten()->toArray(); $models = factory(UserCategoryRelations::class, 100)->make()->map( function (Eloquent $eloquent) use ($userKeys, $categoryKeys) { // あらかじめ用意しておいた親主キーの値をランダムに選ぶ $eloquent->user_id = Arr::random($userKeys); $eloquent->category_id = Arr::random($categoryKeys); // insert メソッドでは自動付与されないのでここで付与 $eloquent->created_at = $eloquent->created_at ?? now(); $eloquent->updated_at = $eloquent->updated_at ?? now(); $eloquent->setHidden([]);// toArray した時に落ちるデータをなしにする return $eloquent; } ); // まとめて INSERT UserCategoryRelations::insert($models->toArray()); }
巨大なクエリを作るとエラーが出力されることがあります。大体、too many placeholders やリソース不足的なエラーメッセージが出力されます。これを回避するためには次の様に少しずつクエリを実行する必要があります。この時 chunk メソッド(Laravel のいろんな場所に生えています)を使うと小分けしやすくて便利です。
public function run(): void { $n = 10000; // INSERT するデータを構築 $users = factory(User::class, $n)->make()->map( function (Member $member) { // insert メソッドでは自動付与されないのでここで付与 $member->created_at = $eloquent->created_at ?? now(); $member->updated_at = $eloquent->updated_at ?? now(); $member->setHidden([]);// toArray した時に落ちるデータをなしにする return $member; } ); // 1000 件ずつまとめて INSERT $users->chunk(1000)->each( static function (Collection $chunkCollection) { Member::insert($chunkCollection->toArray()); } ); }
上述の二つはよくある問題ですが比較的珍しい問題としてファクトリー(Laravel のテストデータ生成アルゴリズム定義機能) 等の INSERT 元データ生成処理が遅い場合もあります。これが起きやすいのはパスワードです。次のコードは管理者のファクトリー定義で Seeder が遅くなりやすいコードです。
<?php /* @var $factory \Illuminate\Database\Eloquent\Factory */ use Faker\Generator as Faker; // 管理者モデルのファクトリー $factory->define(App\Models\Eloquents\Admin::class, function (Faker $faker) { return [ 'name' => $faker->name, 'email' => $faker->safeEmail, 'password' => \Hash::make('admin'),// 新たに Admin インスタンスをファクトリー経由で作るたびに実行される 'remember_token' => Str::random(10), ]; });
コード中のコメントの様にテストデータ用のインスタンス生成の度に暗号化処理が走ります。本来の処理そのもの挙動ですが、必要ない時がほとんどです。そういった時は次の様にファクトリー中に処理をはさまないコードにすることでテストデータ生成処理を軽くして Seeder を軽くできます。
<?php /* @var $factory \Illuminate\Database\Eloquent\Factory */ use Faker\Generator as Faker; // ここであらかじめパスワードハッシュを生成して使いまわす $adminPassword = \Hash::make('admin'); $factory->define(App\Models\Eloquents\Admin::class, function (Faker $faker) use ($adminPassword) { return [ 'name' => $faker->name, 'email' => $faker->safeEmail, 'password' => $adminPassword,// ファクトリー経由で Admin インスタンスを作る時に暗号化をせず、あらかじめ決めた値を使う 'remember_token' => Str::random(10), ]; });
こんな感じでループ内の処理の重さに気を付ける、まとめて INSERT できる様に処理を構築する、とすれば Seeder は高速化します。高速化によってデータベース定義の作り直し、テストデータの生成し直しが快適になり開発もはかどります。