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 在哪裡切分欄位 |