[Laravel] N+1 Problem

Kunyu
4 min readJul 28, 2024

--

一個系統會包含許多不同類型的資料,資料之間會有某種程度的關聯性,例如一個求職網站當中,有人 PO 了一個職缺文章,我們想從那篇職缺文章找到是誰 PO 的,在關聯性資料庫中會使用 JOIN 語法來獲取。

Laravel 提供另一種方式來處理,可以透過定義好的 Model 的 Dynamic Property 來取得關聯,可以在 Laravel 官方網站文件的 Eloquent ORM 當中的 Relationships 看到類似以下的寫法。

class Job extends Model
{
use HasFactory;

protected $table = 'job_listings';
protected $fillable = ['title', 'salary'];

public function employer()
{
return $this->belongsTo(Employer::class);
}
}

現在我們要列出職缺文章清單,有一個很常見的做法,我們在 Controllers 把 Job 所有資料傳到 Views,再由 Views 跑迴圈把清單做出來,然而實際上每當迴圈跑到 PO 文者姓名的時候,背後都必須執行 SQL 一次,如果清單有 100 筆,那麼就必須跑 100 次,這就是所謂的 N+1 Problem,而這種調用方式稱為 Lazy Loading

@foreach ($jobs as $job)
<a href="/jobs/{{ $job['id'] }}">
<div>{{ $job->employer->name }}</div>
<div>{{ $job['title'] }}: Pays {{ $job['salary'] }} per year.</div>
</a>
@endforeach

為了更有感覺一點,我們可以安裝 Laravel Debugbar 方便看出系統在跑的時候都執行了哪些 SQL。

composer require barryvdh/laravel-debugbar --dev

由於 Debugber 會揭漏很多資訊在網頁上,因此 Laravel 定義只有 .env 檔案中的 APP_ENV=localAPP_DEBUG=true才會啟動。

APP_ENV=local
APP_DEBUG=true

實際執行如下,Laravel Debugber 讓整個背後幹了什麼事,一目了然。

為了避免 N+1 Problem,可以透過 with 語法來做 Eager Loading,我們在 Controllers 讀取 Job 資料之時就要一併讀取所有的 PO 文者資料,然後才丟到 Views 去跑迴圈。

$jobs = Job::with('employer')->get();

接著一樣透過 Laravel Debugbar 來看執行結果,可以發現 SQL 的執行方式改變了,執行起來更有效率。

如果專案想禁止 Lazy Loading,app/Providers/AppServiceProvider.php 寫入以下內容。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
}

public function boot(): void
{
// 禁止 Lazy Loading
Model::preventLazyLoading();
}
}

--

--

Kunyu
Kunyu

Written by Kunyu

軟體工程師,Web系統開發為主。

No responses yet