~/blog/laravel-eloquent-orm-complete-guide-from-beginner-to-pro.md
Laravel 與後端開發 · 2025 / 08 / 17

手寫 SQL 還是 Eloquent?Laravel ORM 模型、關聯與效能優化實戰手冊

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
手寫 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 還有 firstOrCreateupdateOrCreate 兩個常用方法:前者「找不到才建立」,後者「找到就更新、找不到就建立」,在處理匯入或同步資料時特別實用。

讀取資料(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() 會觸發模型事件(如 updatingupdated)並自動維護 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 串接還是效能調校,我們都能提供專業的解決方案。立即聯繫浪花科技,聊聊如何讓你的專案更上一層樓!

延伸閱讀

// FAQ

常見問題

Laravel Eloquent ORM 是什麼?
Eloquent ORM 是 Laravel 內建的物件關聯對映工具,讓你用 PHP 物件導向語法操作資料庫,而不必手寫 SQL。它採用 Active Record 模式,一個 Model 對應資料庫的一張資料表,把冷冰冰的 SQL 指令變成可讀性高、易維護的物件導向程式碼。
如何建立 Eloquent 模型,以及它如何對應資料表?
可用 Artisan 指令 php artisan make:model Post 建立模型,加上 -m 參數會順便建立對應的 migration 檔案。Eloquent 信奉「約定優於配置」,預設情況下單數大寫的 Post 模型會自動對應到複數小寫的 posts 資料表。若名稱或主鍵不符慣例,可用 $table、$primaryKey 等屬性覆寫,僅在偏離慣例時才需設定。
$fillable 和 $guarded 有什麼差別?為何與安全有關?
兩者都用來防止 Mass Assignment(批量賦值)漏洞,需二選一。$fillable 是白名單,只有列在其中的欄位才能被批量賦值;$guarded 是黑名單,列在其中的欄位不能被批量賦值。實務上推薦使用 $fillable,因為白名單採「預設拒絕」,新增欄位若忘了加進名單,頂多功能不通,而不會默默開出安全破口。
Eloquent 的 all()、get() 與 chunk()、paginate() 該如何選用?
all() 與 get() 會把所有符合條件的資料一次全部載入記憶體,資料量大時相當危險。處理大量資料時應改用 chunk() 分批處理,或在分頁時用 paginate(),避免一次把整張表灌進記憶體導致資源耗盡。
在查詢上直接呼叫 update() 和先取出模型再 save() 有什麼差別?
先用 find() 取出模型再 save() 會觸發模型事件(如 updating、updated)並自動維護 updated_at 時間戳;而直接在查詢上呼叫 update() 是一次 SQL 批量更新,不會觸發模型事件,也不會自動更新時間戳。需要事件或時間戳時選前者,追求批量效能時選後者。
Eloquent 的軟刪除(Soft Deletes)是什麼?
軟刪除指資料不會真的從資料庫消失,而是標記一個 deleted_at 時間戳。啟用後一般查詢會自動排除被軟刪除的資料,需要時可用 withTrashed() 把它們撈回來,或用 restore() 還原。對於需要保留歷史紀錄、支援「復原」功能的系統非常實用。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

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

$
// final.exec()

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