🧔🏻♀️
LaravelのModelの処理が予想以上に時間がかかっていた
はじめに
Laravelを使っているとEloquentで取得した複数のModelに対して処理を実行したときがありますよね。
// Collection & Model
$items = collect();
foreach(range(1,500000)as $i){
$items->push(new App\Models\SampleModel());
}
$start = microtime(true);
foreach($items as $item){
$item->id;
}
$end = microtime(true);
echo "Collection & Model:" . (int)(($end-$start) * 1000) . "ms\n";
// Collection & Model:1977ms
この処理は500000個のモデルをforeachでプロパティを取り出すだけですが約2000ミリ秒かかってしまいました。
しかし処理の前にtoArray()をしておくと…
// array & array
$items = collect();
foreach(range(1,500000)as $i){
$items->push(new App\Models\SampleModel());
}
$items = $items->toArray();
$start = microtime(true);
foreach($items as $item){
$item["name"];
}
$end = microtime(true);
echo "array & array:" . (int)(($end-$start) * 1000) . "ms\n";
// array & array:12ms
12ミリ秒にも減りました。
なそ
にん
他の形式でも試してみましょう。
ほかの形式とも比較してみる
// array & Model
$items = [];
foreach(range(1,500000)as $i){
$items[] = new App\Models\SampleModel();
}
$start = microtime(true);
foreach($items as $item){
$item->id;
}
$end = microtime(true);
echo "array & Model:" . (int)(($end-$start) * 1000) . "ms\n";
// array & Model:1277ms
// array & stdClass
$items = [];
foreach(range(1,500000)as $i){
$items[] = (object) array('name' => "");
}
$start = microtime(true);
foreach($items as $item){
$item->name;
}
$end = microtime(true);
echo "array & stdClass:" . (int)(($end-$start) * 1000) . "ms\n";
// array & stdClass:88ms
// Collection & array
$items = [];
foreach(range(1,500000)as $i){
$items[] = ['name' => ""];
}
$start = microtime(true);
foreach($items as $item){
$item["name"];
}
$end = microtime(true);
echo "Collection & array:" . (int)(($end-$start) * 1000) . "ms\n";
// Collection & array:17ms
時間 | |
---|---|
Collection & Model | 1977ms |
array & array | 12ms |
array & Model | 1277ms |
array & stdClass | 88ms |
Collection & array | 17ms |
うん、やっぱり重いのはModelからのプロパティの取り出しっぽい。
Modelを大量に処理するならtoArray()するべきと言いたいわけではない
そもそもModelを大量ののループで逐一取り出して処理をするとき、本当にそれはその量をforeachで回す必要があるのかを考えたほうが良いのかなと思います。
例えば、このような処理はモデルにリレーションを設定し、withを使うことで改善できます。
// before
$itemsA = App\Models\SampleModelA::where('type',1)->get();
$itemsB = App\Models\SampleModelB::where('relation_for','model_a')->select(['target_id', 'name'])->get();
foreach ($itemsA as $a) {
foreach ($itemsB as $b) {
if ($a->id == $b['target_id']) {
$a->b_name = $b['name'];
}
}
}
// after
$itemsA = App\Models\SampleModelA::where('type',1)->with('sample_model_b')->get();
foreach ($itemsA as $a) {
$a->b_name = $b['name'];
}
プロパティの整形がなければafterに残っているforも消せますね。
(抽象化がうまくいかなくて不自然すぎるコード書いてしまった…コメント欄でもっといい例を募集します)
実際僕がこのようなコードに直面したときもEloquent側で受け取るデータを工夫することにより速度改善できました。
modelに対してめちゃでかforeachを実行しそうになったら思い出してね。
Discussion