iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 3

Day 03:Model 嚴格模式抓出的一個真實 N+1 與一個藏了很久的 typo

  • 分享至 

  • xImage
  •  

前言:「這段程式碼能動」跟「這段程式碼沒問題」是兩回事

「這個功能上線好一段時間了,也沒人回報問題,代表程式碼沒問題吧?」

這句話有一個隱藏的前提:沒人回報,不代表沒問題,只代表問題還沒被踩到,或者踩到的人沒意識到那是問題。今天要看一個真實案例——這個系統啟用 Model::shouldBeStrict() 之後,一次性抓出了好幾個「能動,但其實有問題」的地方,包括一個藏了不知道多久的 typo。

今日目標

  • 認識 Model::shouldBeStrict() 這個開發輔助機制在防什麼
  • 看一個真實案例:一支 Controller 因為缺少 eager loading,觸發了 N+1 查詢
  • 理解為什麼一個看起來完全正常運作的功能,背後可能藏著效能問題
  • 知道 shouldBeStrict() 除了 N+1,還能抓出哪些容易被忽略的錯誤

本文主體

Model::shouldBeStrict() 在做什麼

Laravel 的 Eloquent Model 預設是「寬鬆」的——存取一個沒有 eager load 的關聯,它會默默幫你發一次額外的查詢;賦值一個不在 $fillable 裡的欄位,它會默默忽略。這種寬鬆對開發很方便,但也代表很多本來該被抓出來的問題,會被悄悄吞掉。

Model::shouldBeStrict() 就是把這層寬鬆關掉,通常只在非正式環境開啟:

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::shouldBeStrict(! $this->app->isProduction());
    // ...
}

開啟之後,存取未 eager load 的關聯會直接丟例外,而不是默默多發一次查詢。這代表:只要你在本機或測試環境正常操作過一次功能,N+1 查詢就會立刻現形,不用等到正式環境流量大了才被效能監控抓到。

一個真實的 N+1 案例

這個系統有一支 Controller,負責顯示「快速連結」分類頁:

// app/Http/Controllers/QuickLinkController.php(修正前)
public function __invoke(int $id)
{
    $categories = QuickLinkCategory::query()->tap(new ByType(QuickLinkCategory::TYPE_LINK))->get();
    // ...
    return view('quick_link', [
        'children' => $categories,
        // ...
    ]);
}

View 裡要顯示每個分類底下的連結清單($category->links),但查詢 $categories 的時候沒有 eager load links 這個關聯。在寬鬆模式下,這支程式碼「能動」——每次 View 存取 $category->links 時,Eloquent 會自動補發一次查詢,畫面顯示完全正常,只是分類數量越多,額外查詢的次數就跟著線性增加。這正是典型的 N+1:1 次查詢拿到分類清單,再對每個分類各發 1 次查詢拿連結,N 個分類就是 N+1 次查詢。

開啟嚴格模式之後,這支 Controller 在本機測試時直接丟出例外,修法很直接:

// 修正後
$categories = QuickLinkCategory::query()->with('links')->tap(new ByType(QuickLinkCategory::TYPE_LINK))->get();

加上 with('links'),一次查詢就把所有分類跟對應的連結都拿到,View 存取 $category->links 時不需要再補發查詢。

這不是唯一一處

同一次修正裡,還有好幾個地方犯了一樣的問題——首頁的跑馬燈元件、影音元件、快速連結元件,各自查詢 QuickLinkCategory 時都沒有 eager load links

// 修正前
public function links(): Collection
{
    return QuickLinkCategory::query()
        ->tap(new ByType(QuickLinkCategory::TYPE_MARQUEE))
        ->first()
        ?->links ?? new Collection;
}

// 修正後
public function links(): Collection
{
    return QuickLinkCategory::query()
        ->with('links')
        ->tap(new ByType(QuickLinkCategory::TYPE_MARQUEE))
        ->first()
        ?->links ?? new Collection;
}

這幾處問題有一個共通點:都是同一種「查詢分類、再存取分類底下的關聯」模式,只是出現在不同的元件裡各自被複製了一次。 沒有嚴格模式的話,這些問題會一直安靜地存在,直到某天流量夠大、資料夠多,才會在效能監控上看到異常。

順手抓到的一個 typo

同一次修正裡,還修掉了一個藏在後台管理頁面裡的真實 typo——一段判斷條件寫成 $record->fist,應該是 $record->first(少打一個字母)。這個 typo 沒有讓程式報錯,因為 Eloquent 的寬鬆模式下,存取一個不存在的屬性只會回傳 nullnull 在條件判斷裡會被當成 falsy,程式碼照樣「能執行」,只是這個判斷式永遠不會成立。這正是嚴格模式最有價值的地方:它抓的不只是效能問題,是任何「因為 Eloquent 太寬容,所以錯誤被吞掉沒有顯現」的情況。

❌ 依賴寬鬆模式「反正能動」

$user->pages; // 沒 eager load,靜靜地多發一次查詢
$record->fist; // typo,靜靜地回傳 null,判斷式永遠是 false

✅ 開發環境開啟嚴格模式,讓問題提早現形

Model::shouldBeStrict(! $this->app->isProduction());

今日思考題

你的專案有沒有開啟過 Model::shouldBeStrict()?如果現在打開它,你猜會冒出幾個「能動但其實有問題」的地方?

今日重點回顧

  • Model::shouldBeStrict() 把 Eloquent 的寬鬆行為關掉,讓 N+1 查詢跟屬性 typo 在開發階段就直接現形
  • 真實案例:QuickLinkController 跟三個首頁元件都因為缺少 with('links') 觸發 N+1,問題模式相同,只是複製到不同檔案
  • 同一次修正還順手抓到一個藏很久的 typo(fist 應為 first),因為 Eloquent 的寬鬆行為把這個錯誤吞掉了
  • 「功能能動」不等於「程式碼沒問題」,嚴格模式的價值就在把兩者的落差找出來

明日預告

明天要看這個系統裡一個身兼多職的 Model——用階層樹狀結構管理整站頁面,還用一個欄位動態決定自己的行為模式。


上一篇
Day 02:Eloquent 模型設計——一段字串處理邏輯,為什麼後來搬進了 Model
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言