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

Dapper 微型 ORM

什麼是 Dapper?

如果 EF Core 是自排車(自動幫你處理 SQL、追蹤變更、管理關聯),那 Dapper 就是手排車(你自己寫 SQL,它只幫你把結果對應到 C# 物件)。

Dapper 是由 Stack Overflow 團隊開發的微型 ORM,特點是:

  • 極快:效能接近原生 ADO.NET
  • 輕量:只有一個 NuGet 套件
  • 簡單:就是 IDbConnection 的擴充方法
  • ⚠️ 需要自己寫 SQL
# 安裝 Dapper
dotnet add package Dapper                  # 安裝 Dapper 套件
dotnet add package Microsoft.Data.SqlClient # 安裝 SQL Server 連線套件

基本操作

Query — 查詢多筆

using Dapper;                              // 引用 Dapper
using Microsoft.Data.SqlClient;            // 引用 SQL Server 連線

// 建立資料庫連線
using var conn = new SqlConnection(connectionString); // 建立連線物件

// 查詢所有學生
var students = await conn.QueryAsync<Student>( // 查詢並對應到 Student 類別
    "SELECT Id, Name, Email, Score FROM Students" // SQL 查詢語句
);

foreach (var s in students)                // 遍歷結果
{
    Console.WriteLine($"{s.Name}: {s.Score}"); // 輸出姓名和分數
}

QueryFirstOrDefault — 查詢單筆

// 查詢單一學生
var student = await conn.QueryFirstOrDefaultAsync<Student>( // 查詢第一筆或 null
    "SELECT * FROM Students WHERE Id = @Id", // 使用參數化查詢
    new { Id = 1 }                         // 傳入參數(匿名物件)
);

if (student is null)                       // 檢查是否找到
{
    Console.WriteLine("找不到學生");       // 沒找到的處理
}

Execute — 新增 / 修改 / 刪除

// 新增學生
var rowsAffected = await conn.ExecuteAsync( // 執行 SQL 並回傳影響列數
    @"INSERT INTO Students (Name, Email, Score) -- 新增語句
       VALUES (@Name, @Email, @Score)",        // 使用參數
    new { Name = "小賢", Email = "xian@test.com", Score = 95 } // 參數值
);
Console.WriteLine($"新增了 {rowsAffected} 筆"); // 輸出影響列數

// 批次新增(傳入 List)
var newStudents = new List<Student>        // 建立多筆學生資料
{
    new() { Name = "小明", Email = "ming@test.com", Score = 88 }, // 學生 1
    new() { Name = "小華", Email = "hua@test.com", Score = 92 },  // 學生 2
};

var count = await conn.ExecuteAsync(       // 批次執行
    @"INSERT INTO Students (Name, Email, Score) -- 新增語句
       VALUES (@Name, @Email, @Score)",        // 使用參數
    newStudents                            // 傳入整個 List,Dapper 會逐筆執行
);

參數化查詢(防 SQL Injection)

這是使用 Dapper(或任何 ORM)最重要的安全觀念。

// ❌ 絕對不要這樣做!字串串接 → SQL Injection 風險
var name = "'; DROP TABLE Students; --";  // 惡意輸入
var sql = $"SELECT * FROM Students WHERE Name = '{name}'"; // 直接串接 😱
// 最終 SQL: SELECT * FROM Students WHERE Name = ''; DROP TABLE Students; --'

// ✅ 使用參數化查詢
var student = await conn.QueryFirstOrDefaultAsync<Student>( // 安全的查詢
    "SELECT * FROM Students WHERE Name = @Name", // 用 @Name 參數佔位
    new { Name = name }                    // Dapper 會安全地處理參數值
);
// Dapper 會自動將參數值進行轉義,防止 SQL Injection

Multi-Mapping(多表對應)

當 JOIN 查詢回傳多張表的資料時,Dapper 可以自動對應到不同的 C# 物件。

// JOIN 查詢:學生 + 選課 + 課程
var sql = @"
    SELECT s.Id, s.Name, s.Email,          -- 學生欄位
           e.Id, e.Score,                  -- 選課欄位
           c.Id, c.CourseName              -- 課程欄位
    FROM Students s
    INNER JOIN Enrollments e ON s.Id = e.StudentId  -- 連接選課表
    INNER JOIN Courses c ON e.CourseId = c.Id       -- 連接課程表
    WHERE s.Id = @StudentId";              -- 篩選條件

var enrollments = await conn.QueryAsync<Student, Enrollment, Course, Enrollment>(
    sql,                                   // SQL 查詢
    (student, enrollment, course) =>       // 對應函式(三個物件合併)
    {
        enrollment.Student = student;      // 設定選課的學生
        enrollment.Course = course;        // 設定選課的課程
        return enrollment;                 // 回傳選課記錄
    },
    new { StudentId = 1 },                 // 參數
    splitOn: "Id,Id"                     // 告訴 Dapper 在哪裡切分欄位
);

Dapper vs EF Core:什麼時候用哪個?

場景 推薦 原因
快速開發 CRUD EF Core 自動產生 SQL,開發速度快
複雜報表查詢 Dapper 手寫 SQL 更靈活
效能敏感的 API Dapper 效能接近原生 ADO.NET
需要 Change Tracking EF Core 自動追蹤變更
呼叫 Stored Procedure Dapper 語法更簡單
新手學習 EF Core 不需要會 SQL 也能開始

💡 實務建議:很多團隊會兩個一起用!EF Core 做一般 CRUD,Dapper 做複雜查詢和報表。

// 在同一個專案中混用 EF Core 和 Dapper
public class StudentService                // 學生服務
{
    private readonly AppDbContext _db;      // EF Core 的 DbContext
    private readonly IDbConnection _conn;  // Dapper 用的連線

    // 簡單 CRUD 用 EF Core
    public async Task<Student?> GetById(int id) // 用 EF Core 查詢單筆
    {
        return await _db.Students          // 使用 DbContext
            .Include(s => s.Enrollments)   // 載入關聯資料
            .FirstOrDefaultAsync(s => s.Id == id); // 依 ID 查詢
    }

    // 複雜報表用 Dapper
    public async Task<IEnumerable<StudentReport>> GetReport() // 用 Dapper 查報表
    {
        return await _conn.QueryAsync<StudentReport>( // 使用 Dapper
            @"SELECT s.Name, COUNT(e.Id) AS CourseCount, -- 手寫複雜 SQL
                      AVG(e.Score) AS AvgScore
               FROM Students s
               LEFT JOIN Enrollments e ON s.Id = e.StudentId
               GROUP BY s.Name
               HAVING AVG(e.Score) > 60"  // 只要平均及格的
        );
    }
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:SQL Injection — 字串串接

// ❌ 絕對不要用字串串接組 SQL!
public async Task<Student?> Search(string keyword) // 搜尋學生
{
    var sql = "SELECT * FROM Students WHERE Name LIKE '%" + keyword + "%'"; // 危險!
    return await conn.QueryFirstOrDefaultAsync<Student>(sql); // SQL Injection 風險
}
// ✅ 用參數化查詢
public async Task<Student?> Search(string keyword) // 搜尋學生
{
    var sql = "SELECT * FROM Students WHERE Name LIKE @Keyword"; // 參數化
    return await conn.QueryFirstOrDefaultAsync<Student>( // 安全查詢
        sql,
        new { Keyword = $"%{keyword}%" } // Dapper 安全處理參數
    );
}

❌ 錯誤 2:忘記 Dispose 連線

// ❌ 沒有 using,連線不會被釋放
var conn = new SqlConnection(connectionString); // 建立連線
var data = await conn.QueryAsync<Student>(sql);  // 查詢
// conn 永遠不會被關閉!連線池耗盡 → 系統當掉
// ✅ 用 using 確保連線被釋放
using var conn = new SqlConnection(connectionString); // using 確保自動釋放
var data = await conn.QueryAsync<Student>(sql);       // 查詢完成後自動關閉連線

❌ 錯誤 3:Multi-Mapping 忘記設定 splitOn

// ❌ 沒設定 splitOn,Dapper 不知道哪些欄位屬於哪個物件
var result = await conn.QueryAsync<Student, Course, Student>( // 多表對應
    "SELECT s.*, c.* FROM Students s JOIN Courses c ON ...", // 查詢
    (s, c) => { s.Course = c; return s; }  // 對應函式
    // 忘記 splitOn 參數 → 預設只用 "Id" 切分,可能對應錯誤
);
// ✅ 明確指定 splitOn
var result = await conn.QueryAsync<Student, Course, Student>( // 多表對應
    "SELECT s.Id, s.Name, c.Id, c.CourseName FROM Students s JOIN Courses c ON ...",
    (s, c) => { s.Course = c; return s; }, // 對應函式
    splitOn: "Id"                        // 告訴 Dapper 在第二個 Id 欄位切分
);

💡 重點整理

概念 說明
Dapper 微型 ORM,效能極佳
Query 查詢多筆並對應到類別
Execute 執行新增/修改/刪除
參數化查詢 防 SQL Injection 的關鍵
Multi-Mapping JOIN 結果對應到多個物件
splitOn 告訴 Dapper 在哪裡切分欄位

💡 大家的想法 · 0

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