Laravel は PHP のフレームワークでよくデータベースを利用します。このデータベースの利用をより快適にするために初期データ(テストデータ、製品版の初期値データ)をデータベースに保存するためのシーダーという仕組みがあります。
データベース:シーディング 6.x Laravel
負荷テストも行うためのテストデータを作る時などどうしてもシーダーを動かすのに時間がかかる時があります。この記事では非同期でシーダーを動作させることによってシーダー全体の処理時間を短くする方法を紹介します。
非同期実行のために使う PHP ライブラリは Symfony の Process コンポーネントです。
The Process Component (Symfony Docs)
【PHP】安全にお手軽にexecするためのsymfony/processの紹介 – 株式会社シーポイントラボ | 浜松のシステム・RTK-GNSS開発
Process を使うことで PHP でも非同期実行が比較的容易に記述できます。これを使って次の様に適切なクラスの db:seed を順次呼び出します。
<?php
namespace App\Console\Commands\ForDevelop;
use Arr;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class AsyncSeeder extends Command
{
protected $name = 'db:async-seed';
protected $description = 'データベースシーダーを非同期実行';
public function handle(): void
{
// 会員を持つことになる親テーブルの初期化
$this->asyncSeeder(['MemberGroupSeeder', 'MemberPositionSeeder', 'MemberTypeSeeder']);
// 会員の初期化。この初期化処理は親テーブルが初期化済みでないと動かせない
$this->asyncSeeder('MemberSeeder');
}
/** クラス名から Process に渡すためのコマンドを生成
* @param string $className
* @return array
*/
private function makeSeedCommand(string $className): array
{
return ['php', 'artisan', 'db:seed', '--class='.$className];
}
/**
* 渡されたクラス名のシーダーを非同期で実行する
* @param string|string[] $classNames 非同期で動かすシーダークラス達の名前
* @param bool $await true ならば渡された全てのシーダーが終わるまで待つ
* @return array
*/
public function asyncSeeder($classNames, $await = true): array
{
// 第一引数に配列でない値が来るときもあるので配列になる様にラップ
$seedCommands = array_map(fn ($className) => $this->makeSeedCommand($className), Arr::wrap($classNames));
// シーダーコマンドをコマンド非同期実行メソッドに受け渡し
return $this->asyncRun($seedCommands, $await);
}
/**
* 渡されたコマンドを非同期で実行する
* @param string[]|string[][] $commands 非同期で動かすコマンドたち
* @param bool $await true ならば渡されたプロセスが全て終わるまで待つ
* @return array
*/
public function asyncRun(array $commands, $await = true): array
{
// 後にプロセスを wait したり、プロセスの制御を呼び出し側に渡すための配列
$processes = [];
foreach ($commands as $command) {
// コマンドを実行する準備
$process = (new Process($command));
$processes[] = $process;
// 一つのコマンドの実行時間が長くてもタイムアウトで中断しない様にする
$process->setTimeout(null);
// コマンドを非同期実行。実行先の出力を手元にも出力される様にコールバックを渡す
$process->start(static fn ($type, $buffer) => print $buffer);
}
foreach ($processes as $process) {
// 第二引数の await が true ならば全てのプロセス完了まで待つ
$await && $process->wait();
}
// プロセスを格納した配列をreturn。こうすると呼び出し側で非同期の細かい制御をする時に便利
return $processes;
}
}
上記例の様に同時に動かしても問題のないシーダーを非同期実行で同時に走らせ、動かす必要のあったシーダーが全て終わった後に次のシーダーを動かす、とすることで動けるのに動いていないシーダーの待ち時間を減らせます。例では二段以上の非同期実行の完了を待てない欠点がありますが、渡されたコマンドを非同期実行するコマンドを作ることで入れ込構造に非同期実行を呼び出せるようになり、多段制御もできる様になります。
ちなみに非同期制御が複雑ならばいっそ JavaScript のスクリプトで非同期 exec を使って書いた方が楽です。
const util = require('util');
const childProcess = require('child_process');
const exec = util.promisify(childProcess.exec);
/** シーダーコマンド生成. Symfony の Process と違って文字列で渡す必要あり */
const makeSeedCommand = (className) => {
return ['php', 'artisan', 'db:seed', `--class=${className}`].join(' ');
};
/** メインの処理。Promise, async, await を使うことで PHP よりも細やかな非同期処理が分かりやすく書ける */
const main = async () => {
await Promise.all([
exec(makeSeedCommand('MemberGroupSeeder')),
exec(makeSeedCommand('MemberPositionSeeder')),
exec(makeSeedCommand('MemberTypeSeeder')),
]);
await exec(makeSeedCommand('MemberSeeder'));
};
main();