🧔🏻‍♀️

LaravelのModelの処理が予想以上に時間がかかっていた

2024/03/08に公開

はじめに

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