手寫 SQL 還是 Eloquent?Laravel ORM 模型、關聯與效能優化實戰手冊
☰ 目錄 table-of-contents.md
一分鐘先看結論
Laravel Eloquent ORM 是 Laravel 內建的物件關聯對映工具,讓你用 PHP 物件導向語法操作資料庫,而不必手寫 SQL。真正決定一個 Laravel 專案優雅程度與維護性的,不是 CRUD,而是三件事:模型的正確設定(特別是 Mass Assignment 安全防護)、模型關聯(一對一、一對多、多對多)的善用,以及用渴求式載入(Eager Loading)解決 N+1 查詢這個最常見的效能殺手。
這篇文章會帶你從模型基礎一路走到效能優化:什麼情況該用 `$fillable`、關聯怎麼定義、N+1 為何拖垮資料庫、`with()` 如何把查詢從 N+1 次壓到 2 次,以及 Collection 如何讓你少寫一堆 `foreach`。看完你就能寫出可讀、安全又跑得動的 Eloquent 程式碼。
Eloquent 的核心:模型(Model)是什麼?
在寫任何查詢之前,得先搞懂 Eloquent 的靈魂——模型(Model)。簡單來說,一個 Model 就是你 PHP 程式碼中,對應資料庫裡一張資料表(Table)的代理人。例如你有一個 posts 資料表,就會建立一個 Post 模型來操作它。這種把資料表映射成物件的設計模式,叫做 Active Record Pattern。
它的價值在於:把冷冰冰的 SQL 指令,變成更容易理解和操作的物件導向語法。這也是 Eloquent 最大的魅力——程式碼可讀性極高,維護起來輕鬆許多。
如何建立第一個 Model?
Laravel 的 Artisan command-line tool 是我們的好朋友。要建立一個 Model,只需要一行指令:
php artisan make:model Post -m
這裡的 -m 參數是個小技巧,它會順便建立對應的 migration 檔案,用來定義 posts 資料表的結構。工程師嘛,能自動化就絕不手動。
Laravel 信奉「約定優於配置」(Convention over Configuration)。預設情況下,Post 這個單數、首字大寫的 Model,會自動對應到 posts 這個複數、全小寫的資料表。當然,你也可以打破這個約定。
資料表名稱或主鍵不符合慣例怎麼辦?
如果你的資料表名稱不符合預設的複數命名規則,或者主鍵不是 id,別擔心,Eloquent 提供幾個屬性讓你輕鬆覆寫預設行為:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
/**
* 與模型關聯的資料表。
*
* @var string
*/
protected $table = 'my_posts'; // 如果你的資料表不叫 posts
/**
* 資料表的主鍵。
*
* @var string
*/
protected $primaryKey = 'post_id'; // 如果你的主鍵不叫 id
/**
* 指示主鍵是否自動遞增。
*
* @var bool
*/
public $incrementing = false; // 如果主鍵不是自動遞增的整數
/**
* 指示模型是否自動維護時間戳記。
*
* @var bool
*/
public $timestamps = false; // 如果你不需要 created_at 和 updated_at
}
原則是:只在偏離慣例時才覆寫。如果你的資料表與主鍵都遵循慣例,這些屬性一個都不用寫,讓 Eloquent 自己推斷反而更清爽。
$fillable 與 $guarded:Mass Assignment 的安全防線
Mass Assignment(批量賦值)很方便,它允許我們用一個陣列一次性建立或更新 Model,例如 Post::create($request->all())。但這也可能是一個安全漏洞:如果惡意使用者在表單中偷偷加入一個 is_admin 欄位,而你又沒設防,後果不堪設想。
為了防止這種情況,Eloquent 提供 $fillable(白名單)和 $guarded(黑名單)兩個屬性,你必須二選一來使用:
- $fillable:只有在這個陣列裡的欄位,才能被批量賦值。這是比較推薦、比較安全的作法。
- $guarded:在這個陣列裡的欄位,不能被批量賦值。如果設為
protected $guarded = [];,就等於把所有欄位都開放,這在開發初期可能很方便,但請務必了解其風險。
class Post extends Model
{
// 推薦作法:明確列出可以批量賦值的欄位
protected $fillable = ['title', 'content', 'author_id'];
// 另一種作法:只有 id 和 is_published 不能被批量賦值
// protected $guarded = ['id', 'is_published'];
}
實務建議:請養成使用
$fillable的習慣,明確列出哪些欄位可以被使用者填入。白名單的好處是「預設拒絕」——新增欄位時若忘了加進名單,頂多功能不通,而不會默默開出一個安全破口。你的資安同事會感謝你。
Eloquent 的基本功:CRUD 操作怎麼寫?
搞定 Model,接下來就是實際操作資料。Eloquent 的語法非常直觀,幾乎就像在說英文。
新增資料(Create)
// 方法一:實例化後儲存
$post = new Post;
$post->title = 'Hello, Eloquent!';
$post->content = 'This is a complete guide.';
$post->save();
// 方法二:使用 create 方法 (需要設定 $fillable)
$post = Post::create([
'title' => 'Another Post',
'content' => 'Content here.'
]);
如果你想避免重複建立相同資料,Eloquent 還有 firstOrCreate 與 updateOrCreate 兩個常用方法:前者「找不到才建立」,後者「找到就更新、找不到就建立」,在處理匯入或同步資料時特別實用。
讀取資料(Read)
// 取得所有文章
$posts = Post::all();
// 根據主鍵查找文章
$post = Post::find(1);
// 查找,但如果找不到就拋出 404 錯誤
$post = Post::findOrFail(1);
// 加上條件查詢
$publishedPosts = Post::where('is_published', true)
->orderBy('created_at', 'desc')
->take(10)
->get();
// 只想取得第一筆符合條件的資料
$firstPost = Post::where('is_published', true)->first();
一個容易被忽略卻很關鍵的差別:all() 與 get() 會把符合條件的資料全部載入記憶體,資料量大時很危險。處理大量資料時,請改用 chunk() 分批處理,或在分頁時用 paginate(),避免一次把整張表灌進記憶體。
更新資料(Update)
// 方法一:先找到模型,再更新屬性後儲存
$post = Post::find(1);
$post->title = 'Updated Title';
$post->save();
// 方法二:批量更新 (Query Builder 模式)
// 注意:這種方式不會觸發 Model 的事件,也不會自動更新 updated_at
Post::where('is_published', false)->update(['is_published' => true]);
這兩種方式的差異是面試與除錯的常見地雷:先取出模型再 save() 會觸發模型事件(如 updating、updated)並自動維護 updated_at;而直接在查詢上呼叫 update() 是一次 SQL 批量更新,不會觸發模型事件、也不會自動更新時間戳。需要事件或時間戳時,選前者;追求批量效能時,選後者。
刪除資料(Delete)
// 刪除單一模型
$post = Post::find(1);
$post->delete();
// 根據主鍵刪除多筆資料
Post::destroy([1, 2, 3]);
// 根據條件刪除
Post::where('views', '<', 100)->delete();
另外,Eloquent 還支援「軟刪除」(Soft Deletes):資料不會真的從資料庫消失,而是標記一個 deleted_at 時間戳。啟用後,一般查詢會自動排除被軟刪除的資料;需要時可用 withTrashed() 把它們撈回來,或用 restore() 還原。對於需要保留歷史紀錄、支援「復原」功能的系統來說,這簡直是救星。
Eloquent 的精髓:如何定義模型關聯(Relationships)?
如果 Eloquent 只能做 CRUD,那它跟一般的 Query Builder 沒兩樣。真正讓它封神的是強大的「關聯」功能——它讓你像操作物件一樣,輕鬆存取關聯的資料。
一對一(One to One)
例如,一個 User 模型只會有一個 Phone 模型。
// 在 User Model 中
public function phone()
{
return $this->hasOne(Phone::class);
}
// 在 Phone Model 中
public function user()
{
return $this->belongsTo(User::class);
}
// 使用:
$phone = User::find(1)->phone;
$user = Phone::find(1)->user;
一對多(One to Many)
例如,一篇文章 Post 可以有多則留言 Comment。
// 在 Post Model 中
public function comments()
{
return $this->hasMany(Comment::class);
}
// 在 Comment Model 中
public function post()
{
return $this->belongsTo(Post::class);
}
// 使用:
$comments = Post::find(1)->comments; // 這會回傳一個 Collection
foreach ($comments as $comment) {
// ...
}
多對多(Many to Many)
例如,一篇文章 Post 可以有多個標籤 Tag,一個標籤 Tag 也可以被用在多篇文章上。這需要一張中間表(Pivot Table),通常命名為 post_tag。
// 在 Post Model 中
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// 在 Tag Model 中
public function posts()
{
return $this->belongsToMany(Post::class);
}
// 使用:
$tags = Post::find(1)->tags;
屬性存取 vs. 方法呼叫:別搞混了
關聯有兩種用法,很多人在這裡卡住:
- 當成屬性存取(
$post->comments):會立即執行查詢並回傳結果(單一模型或 Collection),這叫「動態屬性」。 - 當成方法呼叫(
$post->comments()):回傳的是一個關聯查詢產生器,你可以繼續串接條件,例如$post->comments()->where('approved', true)->get(),只取出已核准的留言。
只想知道「有沒有關聯」或「有幾個」?
若只想篩選「擁有特定關聯的模型」,用 whereHas;若只想知道關聯的數量而不需要把關聯資料整包載入,用 withCount,它會在查詢結果上加一個 {關聯}_count 屬性,效能比載入整批資料再 count() 好得多。
// 只取出「至少有一則留言」的文章
$posts = Post::whereHas('comments')->get();
// 取出每篇文章,並附帶留言數量(不載入留言本體)
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
進階戰術:渴求式載入(Eager Loading)如何解決 N+1 問題?
這是所有 Eloquent 新手最容易踩的效能地雷,也是面試的必考題。
什麼是 N+1 查詢問題?
想像你要顯示 10 篇文章以及它們各自的作者,你可能會這樣寫:
$posts = Post::take(10)->get();
foreach ($posts as $post) {
// 這裡每次循環都會發送一次 SQL 查詢來找作者!
echo $post->author->name;
}
這段程式碼會發生什麼事?
- 第 1 次查詢:
SELECT * FROM posts LIMIT 10 - 接下來的 10 次查詢:
SELECT * FROM authors WHERE id = ?(在迴圈中執行 10 次)
總共執行了 1 + 10 = 11 次查詢,這就是可怕的 N+1 問題。如果 N 是 1000 呢?你的資料庫大概會直接罷工。問題的本質是:查詢次數隨資料筆數線性成長,在開發環境資料少時完全看不出來,一上線就爆炸。
用 with() 解決問題
解法非常簡單,就是使用「渴求式載入」(Eager Loading),告訴 Eloquent:「查詢文章的時候,順便把作者資料也一次打包帶走!」
// 正確的作法
$posts = Post::with('author')->take(10)->get();
foreach ($posts as $post) {
// 這裡不會再有額外的查詢
echo $post->author->name;
}
這樣 Eloquent 只會執行 2 次查詢:
- 第 1 次查詢:
SELECT * FROM posts LIMIT 10 - 第 2 次查詢:
SELECT * FROM authors WHERE id IN (1, 2, 3, ...)
無論文章有 10 篇還是 1000 篇,都只要 2 次查詢,效能天差地遠。
巢狀關聯與已載入模型怎麼辦?
渴求式載入還有幾個常用變化,遇到再對應使用即可:
- 巢狀關聯:用點記號一次載入多層,例如
Post::with('comments.user')->get()會同時把留言與留言的作者帶出來。 - 帶條件的渴求式載入:用閉包對被載入的關聯加條件,例如只載入已核准的留言:
Post::with(['comments' => fn ($q) => $q->where('approved', true)])->get()。 - 對已取出的模型補載入:當模型已經查出來、後來才發現需要關聯時,用
load()做「延遲的渴求式載入」,例如$posts->load('author'),一樣避免 N+1。
實務上,與其每次手動檢查,不如在開發環境用 Laravel 的 preventLazyLoading 機制,讓框架在偵測到延遲載入(lazy loading)時直接拋出例外,逼你在開發階段就把 N+1 修掉,而不是等上線才發現。
集合(Collections)的魔法:少寫 foreach
當你使用 get() 或 all(),或是存取 hasMany 這種關聯時,Eloquent 回傳的不是單純的陣列,而是一個 Illuminate\Support\Collection 物件。這個物件超乎想像地強大,內建數十種方法,可以用鏈式呼叫(chaining)優雅地處理資料。
$posts = Post::all();
// 篩選出標題長度大於 10 的文章
$longTitlePosts = $posts->filter(function ($post) {
return strlen($post->title) > 10;
});
// 將每篇文章的標題轉為大寫
$uppercasedTitles = $posts->map(function ($post) {
return strtoupper($post->title);
});
// 只取出所有文章的 id
$postIds = $posts->pluck('id');
善用 Collection 可以大幅減少你在 Controller 裡寫 foreach 的機會,讓程式碼更簡潔、更具表達力。
不過有一個重要分界要記住:Collection 的方法是在 PHP 記憶體中對「已取出的資料」做運算,而 where() 等查詢產生器的方法是轉成 SQL 交給資料庫。如果你的篩選條件可以下放到資料庫(例如 Post::where('is_published', true)->get()),就別先把整張表 all() 出來再用 Collection 過濾——前者只取需要的資料,後者會把整張表灌進記憶體。原則是:能在資料庫層做的篩選,就交給資料庫;Collection 用來處理已經合理取回的結果。
小結:超越 80% 新手的關鍵
我們從最基本的 Model 設定,一路走過 CRUD、強大的關聯、效能關鍵的 Eager Loading,最後到優雅的 Collection 操作。Eloquent 的世界博大精深,但掌握這幾個核心觀念——安全的 Mass Assignment、正確的關聯定義、用 with() 殲滅 N+1、把篩選下放到資料庫——你已經超越了大多數新手。記住,好的工具能讓你事半功倍,但前提是你得真正理解它。
如果你對 Laravel 開發、網站架構優化,或是如何將這些技術應用在你的專案上感興趣,我們的團隊擁有豐富的實戰經驗。無論是企業級系統開發、API 串接還是效能調校,我們都能提供專業的解決方案。立即聯繫浪花科技,聊聊如何讓你的專案更上一層樓!
延伸閱讀
常見問題
Laravel Eloquent ORM 是什麼?
如何建立 Eloquent 模型,以及它如何對應資料表?
$fillable 和 $guarded 有什麼差別?為何與安全有關?
Eloquent 的 all()、get() 與 chunk()、paginate() 該如何選用?
在查詢上直接呼叫 update() 和先取出模型再 save() 有什麼差別?
Eloquent 的軟刪除(Soft Deletes)是什麼?
訂閱免費電子報
把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。