☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
資料庫 中級

EF Core 進階操作

上一章學了 EF Core 基礎,現在要深入了解Fluent API、載入策略、進階設定


Fluent API vs Data Annotations

兩種設定 Entity 的方式,Fluent API 更強大、更靈活。

// === Data Annotations 方式(用 Attribute)===
public class Student                       // 學生 Entity
{
    public int Id { get; set; }            // 主鍵(慣例自動識別)

    [Required]                             // 必填
    [MaxLength(100)]                       // 最大長度 100
    public string Name { get; set; } = ""; // 學生姓名

    [EmailAddress]                         // Email 格式驗證
    public string Email { get; set; } = ""; // 學生信箱
}
// === Fluent API 方式(在 DbContext 中設定)===
protected override void OnModelCreating(ModelBuilder modelBuilder) // 覆寫模型建構方法
{
    modelBuilder.Entity<Student>(entity =>  // 設定 Student Entity
    {
        entity.HasKey(s => s.Id);           // 設定主鍵
        entity.Property(s => s.Name)        // 設定 Name 屬性
            .IsRequired()                   // 必填
            .HasMaxLength(100);             // 最大長度 100
        entity.HasIndex(s => s.Email)       // 在 Email 上建立索引
            .IsUnique();                    // 唯一索引
        entity.Property(s => s.Email)       // 設定 Email 屬性
            .HasMaxLength(200);             // 最大長度 200
    });
}

選哪個? 簡單驗證用 Data Annotations,複雜關聯設定用 Fluent API。團隊中建議統一風格。


導航屬性讓你用 C# 物件的方式存取關聯資料

// 一對多關係:一個學生有多筆選課記錄
public class Student                       // 學生 Entity
{
    public int Id { get; set; }            // 主鍵
    public string Name { get; set; } = ""; // 姓名

    // 導航屬性:一個學生 → 多筆選課
    public ICollection<Enrollment> Enrollments { get; set; } // 選課集合
        = new List<Enrollment>();           // 初始化空集合
}

public class Enrollment                    // 選課 Entity
{
    public int Id { get; set; }            // 主鍵
    public int StudentId { get; set; }     // 外鍵:學生 ID
    public int CourseId { get; set; }      // 外鍵:課程 ID
    public int Score { get; set; }         // 分數

    // 導航屬性:多對一
    public Student Student { get; set; } = null!; // 所屬學生
    public Course Course { get; set; } = null!;   // 所屬課程
}

載入策略:Eager / Lazy / Explicit

想像你去圖書館借書:

  • Eager Loading:一次把主書和所有參考書都搬回來
  • Lazy Loading:先拿主書,需要參考書時再跑一趟圖書館
  • Explicit Loading:先拿主書,你明確說要參考書時才去拿

Eager Loading(預先載入)— 推薦 ✅

// 一次查詢就把關聯資料載入
var students = await db.Students           // 查詢學生
    .Include(s => s.Enrollments)           // 同時載入選課記錄
        .ThenInclude(e => e.Course)        // 再載入每筆選課的課程資料
    .Where(s => s.Score > 80)              // 篩選分數大於 80
    .ToListAsync();                        // 執行查詢並轉為 List
// 只產生 1 個 SQL 查詢(含 JOIN)

Lazy Loading(延遲載入)— 小心使用 ⚠️

// 需要安裝套件並設定
// dotnet add package Microsoft.EntityFrameworkCore.Proxies

// 在 DbContext 中啟用
optionsBuilder.UseLazyLoadingProxies();    // 啟用延遲載入代理

// 導航屬性必須加 virtual
public class Student                       // 學生 Entity
{
    public int Id { get; set; }            // 主鍵
    public virtual ICollection<Enrollment> Enrollments { get; set; } // 加 virtual!
        = new List<Enrollment>();           // 初始化
}

// 存取時自動查詢(但可能產生 N+1 問題!)
foreach (var s in students)                // 迴圈每個學生
{
    Console.WriteLine(s.Enrollments.Count); // 每次存取都會發一個 SQL 查詢!
}

Explicit Loading(明確載入)

var student = await db.Students            // 先查詢學生
    .FirstAsync(s => s.Id == 1);           // 取得 ID=1 的學生

await db.Entry(student)                    // 取得該 Entity 的追蹤資訊
    .Collection(s => s.Enrollments)        // 指定要載入的集合
    .LoadAsync();                          // 明確載入選課記錄
// 分兩次查詢,但你可以控制何時載入

Shadow Properties(影子屬性)

影子屬性是不出現在 Entity 類別中,但存在於資料庫的欄位。

// 在 Fluent API 中定義影子屬性
modelBuilder.Entity<Student>()             // 設定 Student Entity
    .Property<DateTime>("CreatedAt")     // 定義影子屬性 CreatedAt
    .HasDefaultValueSql("GETDATE()");    // 預設值為目前時間

modelBuilder.Entity<Student>()             // 設定 Student Entity
    .Property<DateTime>("UpdatedAt");    // 定義影子屬性 UpdatedAt

// 在 SaveChanges 中自動更新
public override int SaveChanges()          // 覆寫 SaveChanges
{
    foreach (var entry in ChangeTracker.Entries()) // 遍歷所有追蹤的 Entity
    {
        if (entry.State == EntityState.Modified)   // 如果是修改狀態
        {
            entry.Property("UpdatedAt")          // 設定 UpdatedAt 影子屬性
                .CurrentValue = DateTime.Now;      // 更新為目前時間
        }
    }
    return base.SaveChanges();             // 呼叫原始的 SaveChanges
}

Value Conversions(值轉換)

把 C# 型別自動轉換成資料庫型別。

// 把 Enum 存成字串
public enum StudentStatus                  // 學生狀態列舉
{
    Active,                                // 在學
    Graduated,                             // 畢業
    Suspended                              // 休學
}

modelBuilder.Entity<Student>()             // 設定 Student Entity
    .Property(s => s.Status)               // Status 屬性
    .HasConversion<string>();              // 存到資料庫時轉成字串
// 資料庫存 "Active",C# 讀取時自動轉回 Enum

Global Query Filters(全域查詢篩選)

自動在每次查詢時加上篩選條件,適合做軟刪除

// 在 OnModelCreating 中設定
modelBuilder.Entity<Student>()             // 設定 Student Entity
    .HasQueryFilter(s => !s.IsDeleted);    // 自動過濾已刪除的資料

// 之後所有查詢都會自動加上 WHERE IsDeleted = 0
var students = await db.Students.ToListAsync(); // 只會回傳未刪除的學生

// 需要查看已刪除資料時,用 IgnoreQueryFilters
var allStudents = await db.Students        // 查詢學生
    .IgnoreQueryFilters()                  // 忽略全域篩選
    .ToListAsync();                        // 包含已刪除的學生

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:N+1 問題

// ❌ 沒有用 Include,每次存取導航屬性都會查一次資料庫
var students = await db.Students.ToListAsync(); // 查詢 1:取得所有學生
foreach (var s in students)                     // 迴圈 N 個學生
{
    var count = s.Enrollments.Count;            // 查詢 2~N+1:每個學生各查一次!
}
// 如果有 100 個學生 → 101 次 SQL 查詢 😱
// ✅ 用 Include 一次載入
var students = await db.Students               // 查詢學生
    .Include(s => s.Enrollments)               // 同時載入選課記錄
    .ToListAsync();                            // 只有 1 次 SQL 查詢 ✅
foreach (var s in students)                    // 迴圈 N 個學生
{
    var count = s.Enrollments.Count;           // 已經載入,不需要額外查詢
}

❌ 錯誤 2:Lazy Loading 效能陷阱

// ❌ 開啟 Lazy Loading 後,在迴圈中存取導航屬性
var students = await db.Students.ToListAsync();   // 取得所有學生
var result = students.Select(s => new            // 投影
{
    s.Name,                                      // 學生姓名
    CourseCount = s.Enrollments.Count             // 這裡觸發 Lazy Loading!
}).ToList();                                     // N+1 問題再次出現
// ✅ 用投影(Select)在資料庫端完成計算
var result = await db.Students                   // 查詢學生
    .Select(s => new                             // 在資料庫端投影
    {
        s.Name,                                  // 學生姓名
        CourseCount = s.Enrollments.Count         // 在 SQL 中計算 COUNT
    })
    .ToListAsync();                              // 一次查詢完成 ✅

💡 重點整理

概念 說明
Fluent API 在 DbContext 中用程式碼設定模型
Eager Loading 用 Include 一次載入關聯資料
Lazy Loading 存取時自動載入(有 N+1 風險)
Shadow Property 不在 Entity 中但存在於 DB 的屬性
Global Query Filter 每次查詢自動套用的篩選條件

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊