~/blog/laravel-eloquent-orm-pitfalls-best-practices-guide.md
Laravel 與後端開發 · 2025 / 09 / 22

Eloquent 是蜜糖還是毒藥? Laravel ORM 實戰心法,避開效能地雷區

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
Eloquent 是蜜糖還是毒藥? Laravel ORM 實戰心法,避開效能地雷區
目錄 table-of-contents.md

用物件導向優雅地操作資料庫、不必手寫一堆原生 SQL——Laravel Eloquent 的蜜月期確實美好,但蜜月期過後,列表頁越載越慢、API 回應時間長到讓人想砸電腦的場景就會找上門。這時你才驚覺,當初愛不釋手的 ORM 可能正是拖垮效能的元兇。這篇整理我在 Laravel 專案裡踩過的 Eloquent 地雷與實戰心法。

今天這篇文章,不是要再寫一篇 Laravel Eloquent ORM 的完整指南,教你怎麼 CRUD。那種文章網路上太多了。我想聊的,是那些官方文件不會特別強調,但卻會在真實專案中讓你踩坑踩到懷疑人生的「實戰心法」。我們會深入探討 Eloquent 的雙面刃特性,從人人聞之色變的「N+1 問題」開始,到如何精準地為你的記憶體減壓,最後聊聊如何讓 Eloquent 在大型專案架構中安分守己。準備好了嗎?讓我們一起來馴服 Eloquent 這頭美麗又危險的猛獸吧!

Eloquent 的雙面刃:為何你的「方便」正在扼殺效能?

Eloquent 最大的魅力來自於它所實現的 Active Record 模式。這個模式讓每個資料表都對應到一個 Model,而資料表中的每一筆紀錄,都是這個 Model 的一個實例。這讓我們可以像操作普通物件一樣,直觀地存取和修改資料庫數據。但,工程師的小囉嗦時間來了:天下沒有白吃的午餐,方便的背後,往往隱藏著效能的代價。

Active Record 模式的魅力與詛咒

用 Eloquent 寫程式碼真的很愉快,你看:

<?php
// 找到 ID 為 1 的文章並更新標題
$post = Post::find(1);
$post->title = '新的標題';
$post->save();
?>

是不是很直觀?但這種便利性也帶來了詛咒。Active Record 模式將資料存取邏輯(怎麼從資料庫拿資料、存資料)和業務邏輯(資料本身代表的意義)緊密地耦合在 Model 裡。當專案變大,Model 可能會變得異常臃腫,也就是我們常說的「Fat Model」,這完全違背了軟體設計的單一職責原則(Single Responsibility Principle)。一個 Model 不只管自己跟哪個資料表對應,還管關聯、管資料格式轉換、管一堆有的沒的,最後變成一個難以維護的怪物。

隱藏的 SQL:你以為的一行 Code,其實是 N+1 條 Query

這大概是所有 Eloquent 新手都會踩到的最大地雷,也是最經典的效能殺手:N+1 查詢問題。

想像一個情境:你想顯示一個文章列表,並在每篇文章下方顯示作者的姓名。用 Eloquent 寫起來,直覺上會是這樣:

<?php
// 找出最新的 10 篇文章
$posts = Post::latest()->take(10)->get();

foreach ($posts as $post) {
    // 在迴圈中,每次都去查詢一次作者資料
    echo $post->author->name;
}
?>

看起來很無害,對吧?但骨子裡,這段程式碼執行了什麼?

  • 第 1 次查詢:SELECT * FROM posts ORDER BY created_at DESC LIMIT 10
  • 第 2 次查詢:SELECT * FROM authors WHERE id = ? (第一篇文章的作者)
  • 第 3 次查詢:SELECT * FROM authors WHERE id = ? (第二篇文章的作者)
  • ...
  • 第 11 次查詢:SELECT * FROM authors WHERE id = ? (第十篇文章的作者)

看到了嗎?你總共執行了 1 (N) + 10 (N) = 11 次資料庫查詢!如果你的列表有 100 篇文章,那就是 101 次查詢!這就是所謂的「技術債」,而且利息高得嚇人。當你的使用者越來越多,資料量越來越大,網站就會慢到讓人無法忍受。

解法:Eager Loading (預先載入)

Laravel 早就想到了這個問題,解法就是 Eager Loading。透過 with() 方法,你可以告訴 Eloquent:「嘿,在我查詢文章的時候,順便把作者資料也一次撈回來!」

<?php
// 使用 with() 預先載入 author 關聯
$posts = Post::with('author')->latest()->take(10)->get();

foreach ($posts as $post) {
    // 這裡不會再觸發新的查詢
    echo $post->author->name;
}
?>

這樣修改後,Eloquent 只會執行兩次查詢:

  • 第 1 次查詢:SELECT * FROM posts ORDER BY created_at DESC LIMIT 10
  • 第 2 次查詢:SELECT * FROM authors WHERE id IN (1, 2, 5, 7, ...)

從 N+1 次變成固定的 2 次,效能天差地遠。請把「隨時檢查 N+1」刻在你的 DNA 裡,這是使用 Eloquent 的第一條鐵則。

打造高效能 Eloquent 查詢的實戰心法

避開了 N+1 這個大地雷,我們來看看更多能讓查詢效能坐上火箭的技巧。

Eager Loading 不是萬靈丹:`with()`、`load()` 與 `loadMissing()` 的精準使用時機

with() 是在建立查詢時就決定要預先載入的關聯,但有時候,你可能是在拿到一個 Model 或 Collection 之後,才決定需不需要載入它的關聯。這時候 load() 就派上用場了。

  • with(): 用在查詢的開頭,一次性決定。
  • load(): 用在已經存在的 Model 或 Collection 物件上,動態載入。
  • loadMissing(): 和 load() 類似,但它會先檢查關聯是否已經被載入,如果沒有,才會去執行查詢。這在複雜的邏輯中可以避免重複載入,非常實用。

別再 `all()` 了!用 `select()` 和 `chunk()` 為你的記憶體減壓

很多新手喜歡用 User::all() 來撈全部使用者,在開發初期資料少的時候沒問題。但想像一下,你的使用者資料表有 10 萬筆紀錄,每筆紀錄有 30 個欄位,all() 會試圖一次把這 10 萬筆完整資料全部載入到記憶體中,結果就是記憶體耗盡,程式直接崩潰。

心法一:用 `select()` 指定你需要的欄位。
你真的需要全部 30 個欄位嗎?如果只是要顯示使用者名稱和 Email,就明確指定它們:

<?php
$users = User::select('id', 'name', 'email')->get();
?>

相信我,你的 DBA 和你的伺服器記憶體都會感謝你。

心法二:用 `chunk()` 或 `cursor()` 處理大量數據。
如果你需要對大量的紀錄做處理(例如:發送通知信),千萬不要一次 `get()` 下來。改用 chunk(),它會把結果分成一小塊一小塊處理,大幅降低記憶體壓力。

<?php
// 一次處理 200 個使用者,直到全部處理完畢
User::chunk(200, function ($users) {
    foreach ($users as $user) {
        // 執行你的邏輯...
    }
});
?>

cursor() 則是更進階的用法,它一次只會在記憶體中保留一筆紀錄的 Eloquent Model,對於超大數據集的處理更加節省記憶體。

當 Eloquent 不夠用:Query Builder 與 Raw Expressions 的時機

我愛 Eloquent,但它不是萬能的。當你需要執行非常複雜的 `JOIN`、子查詢、或是用到特定資料庫才有的函式時,硬要用 Eloquent 的語法去兜,程式碼可能會變得比原生 SQL 還要難懂。這時候,就該是 Query Builder 或甚至原生 SQL (Raw Expressions) 上場的時候了。

有時候,我們就是要「返璞歸真」。與其寫一個四不像的 Eloquent 查詢,不如直接用 Query Builder 或 DB::raw(),程式碼更清晰,效能也可能更好。記住,工具是為了解決問題,而不是讓你被工具綁架。

Eloquent 與架構設計:如何讓你的 Model 保持清爽?

前面提到,Eloquent 很容易造成「Fat Model」。在一個成熟的專案裡,我們需要透過好的架構設計來約束它,讓程式碼可以長期維護。

Repository 模式:隔離你的業務邏輯與資料庫

Repository 模式的核心思想是建立一個「倉儲層」,專門負責資料的存取。你的 Controller 或 Service 不會直接去呼叫 Post::create()User::find(),而是透過 `PostRepository` 或 `UserRepository` 來做事。

這樣做的好處是:

  • 關注點分離: Controller 專心處理 HTTP 請求和回應,Repository 專心處理資料庫操作,Model 則回歸到單純的資料結構定義。
  • 易於測試: 你可以輕易地用一個假的 (Mock) Repository 來測試你的 Controller,而不需要真的去碰資料庫。
  • 易於替換底層實作: 哪天你想從 MySQL 換到 PostgreSQL,甚至換成某個 NoSQL 資料庫,理論上你只需要更換 Repository 的實作,而不用動到上層的業務邏輯。

Scopes 與 Accessors/Mutators 的藝術

即便不用 Repository 模式,Eloquent 內部也提供了很多讓程式碼更乾淨的工具:

  • Scopes (查詢作用域): 如果你常常需要查詢「已發佈的文章」,你可以定義一個 `published` scope,之後只要用 Post::published()->get() 即可,而不是每次都重寫 ->where('status', 'published')
  • Accessors (取值器) & Mutators (修改器): 可以在你存取或設定 Model 屬性時自動做一些處理。例如,從資料庫拿出的 `first_name` 和 `last_name`,可以透過 Accessor 合併成一個 `full_name` 屬性;存入資料庫前的密碼,可以透過 Mutator 自動加密。

善用這些工具,可以讓你的查詢邏輯和資料處理邏輯更有組織,避免散落在程式碼的各個角落。

總結:馴服 Eloquent 這頭猛獸

Eloquent 是一個非常強大的工具,它極大地提升了開發效率和程式碼的可讀性。但強大的力量需要被正確地駕馭。今天我們聊到的幾個重點:

  • 警惕 N+1 問題: 永遠把 Eager Loading 放在心上。
  • 精準索取: 只用 `select()` 拿你需要的資料,用 `chunk()` 處理大量數據。
  • 適時放手: 當 Eloquent 變得礙手礙腳時,勇敢地使用 Query Builder 或原生 SQL。
  • 良好架構: 透過 Repository、Scopes 等模式,讓你的 Model 保持簡潔與專注。

掌握了這些心法,你才能真正發揮 Eloquent 的全部潛力,而不是讓它成為你專案效能的瓶頸。它究竟是蜜糖還是毒藥,完全取決於使用它的人。希望這篇文章能幫助你成為一個更好的 Eloquent 駕馭者。

延伸閱讀

在浪花科技,我們每天都在處理各種複雜的 WordPress 和 Laravel 專案,從效能調校到架構設計,我們有豐富的實戰經驗。如果你正被棘手的技術問題困擾,或是希望為你的專案打造一個穩固、高效能的後端架構,歡迎與我們聯繫,讓我們的專業團隊為你提供解決方案!

// FAQ

常見問題

Laravel Eloquent 的 N+1 查詢問題是什麼?
N+1 是 Eloquent 最經典的效能陷阱。當你查出 N 筆紀錄後,在迴圈中逐一存取其關聯資料(例如每篇文章的作者),就會額外觸發 N 次查詢,總共執行 1+N 次。列表有 100 筆時就是 101 次查詢,資料量一大網站便會明顯變慢。
如何解決 Laravel Eloquent 的 N+1 問題?
使用 Eager Loading,透過 with() 方法在查詢時一併載入關聯,例如 Post::with('author')。如此 Eloquent 只會執行兩次查詢:一次撈文章,一次用 WHERE id IN (...) 一次撈回所有作者,將查詢數從 1+N 降為固定的 2 次。
Eloquent 的 with()、load() 和 loadMissing() 差在哪?
with() 用在建立查詢的開頭,一次性決定要預先載入哪些關聯;load() 用在已經取得的 Model 或 Collection 物件上,動態載入關聯;loadMissing() 與 load() 類似,但會先檢查關聯是否已載入,未載入才執行查詢,可在複雜邏輯中避免重複載入。
用 Eloquent 處理大量資料時要怎麼避免記憶體耗盡?
避免用 all() 或 get() 一次撈全部資料。第一,用 select() 明確指定需要的欄位,不要載入全部欄位。第二,處理大量紀錄時改用 chunk(),將結果分批處理以降低記憶體壓力;cursor() 則更進階,一次只在記憶體中保留一筆 Model,適合超大資料集。
什麼時候該放棄 Eloquent,改用 Query Builder 或原生 SQL?
當需要執行非常複雜的 JOIN、子查詢,或用到特定資料庫才有的函式時,硬用 Eloquent 語法可能讓程式碼比原生 SQL 更難懂。此時改用 Query Builder 或 DB::raw() 通常程式碼更清晰、效能也可能更好。工具是為了解決問題,不該被工具綁架。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。

$
// final.exec()

準備好讓你的網站開始為你工作了嗎?