Appearance
Dapper 轻量级 ORM 实践指南
Dapper 是 .NET 平台下最负盛名的“微型 ORM”(Micro-ORM)。它由 Stack Overflow 团队开发并开源,旨在提供接近原生 ADO.NET 的查询性能,同时解决手动填充 POCO 对象的繁琐工作。
1. 核心价值与优势
与 Entity Framework 等“全自动”重型 ORM 不同,Dapper 走的是“半自动”路线:
- 极致性能:底层通过 Emit 反射 IDataReader 的序列队列,快速产生对象,性能几乎等同于原生 DataReader。
- 轻量化:核心仅为一个单文件(或一个 DLL 引用),无复杂配置文件。
- 灵活性:开发者编写原始 SQL,完全掌控查询逻辑,适合复杂 SQL 优化场景。
- 多数据库支持:只要实现了
IDbConnection接口的数据库(SQL Server, MySQL, SQLite, Oracle 等)均可使用。 - 对象关系映射:支持一对一、一对多、多对多等多种复杂关系映射。
1.2 核心原理深度解析
Dapper 的高性能源于其对 System.Reflection.Emit 的深度利用。
- 动态方法生成:当第一次执行查询时,Dapper 会根据 SQL 结果集的结构和目标 POCO 类的属性,动态生成一个 IL 代码块。
- 高速缓存:这个生成的 IL 代码会被缓存起来。后续相同的查询将直接调用这个已编译的“硬编码”方法,避开了传统反射在循环中的巨大开销。
- 数据读取:它直接操作
IDataReader,以最快的方式将数据流填充到对象实例中。
2. 基础 CRUD 快速上手
2.1 初始化连接
Dapper 扩展了 IDbConnection 接口。它会自动管理连接的开启与关闭(如果执行时连接未打开,它会自动 Open,执行完自动 Close)。
csharp
string connStr = ConfigurationManager.ConnectionStrings["Default"].ConnectionString;
using (IDbConnection conn = new SqlConnection(connStr)) {
// 所有的 Dapper 扩展方法都在 conn 上直接调用
}注意:虽然 Dapper 会自动 Open,但在处理事务或执行多个相关查询时,显式调用
conn.Open()是更好的实践。
2.2 增删改 (Execute)
csharp
// 插入数据并获取自增 ID
string insertSql = "INSERT INTO Dogs(Name, Age) VALUES(@Name, @Age); SELECT CAST(SCOPE_IDENTITY() as int)";
int newId = conn.ExecuteScalar<int>(insertSql, new { Name = "Lucky", Age = 2 });
// 更新数据
string updateSql = "UPDATE Dogs SET Age = @Age WHERE Id = @Id";
int affectedRows = conn.Execute(updateSql, new { Age = 3, Id = 1 });
// 删除数据
string deleteSql = "DELETE FROM Dogs WHERE Id = @Id";
conn.Execute(deleteSql, new { Id = 1 });2.3 查询 (Query)
csharp
// 1. 返回强类型列表
string sql = "SELECT * FROM Dogs WHERE Age > @Age";
var list = conn.Query<Dog>(sql, new { Age = 1 }).ToList();
// 2. 返回动态类型 (dynamic)
var rows = conn.Query("SELECT Name, Age FROM Dogs");
foreach (var row in rows) {
Console.WriteLine(row.Name);
}
// 3. 返回单条记录
var dog = conn.QueryFirstOrDefault<Dog>("SELECT * FROM Dogs WHERE Id = @Id", new { Id = 1 });3. 事务处理实践
在涉及多表更新时,事务是保证数据一致性的关键。
csharp
using (var conn = new SqlConnection(connStr)) {
conn.Open();
using (var trans = conn.BeginTransaction()) {
try {
conn.Execute("INSERT INTO Logs(Msg) VALUES(@Msg)", new { Msg = "Start" }, transaction: trans);
conn.Execute("UPDATE Account SET Balance = Balance - 100 WHERE Id = 1", null, transaction: trans);
trans.Commit(); // 提交事务
} catch (Exception) {
trans.Rollback(); // 异常回滚
throw;
}
}
}4. 高级进阶技巧
3.1 批量操作
Dapper 支持直接传入集合进行批量处理:
csharp
var dogList = new[] {
new { Name = "A", Age = 1 },
new { Name = "B", Age = 2 }
};
// 内部会循环执行 SQL
conn.Execute("INSERT INTO Dogs(Name, Age) VALUES(@Name, @Age)", dogList);4.2 多表连接查询 (Multi-Mapping)
用于处理一对一或一对多关系。splitOn 参数至关重要,它告诉 Dapper 结果集中哪一列开始属于下一个对象。
csharp
string sql = @"SELECT p.Id, p.Title, u.Id, u.UserName
FROM Posts p
LEFT JOIN Users u ON u.Id = p.OwnerId";
var data = conn.Query<Post, User, Post>(sql,
(post, user) => {
post.Owner = user;
return post;
},
splitOn: "Id" // 默认是 "Id",如果主键叫 UserID,则需显式指定
).ToList();避坑指南:如果两个表都有
Id字段,SQL 查询中必须明确选择出来(如p.Id, u.Id),否则splitOn可能会匹配错误。
5. Dapper 常用扩展插件
为了进一步简化 SQL 编写,社区提供了许多优秀的扩展:
5.1 Dapper.Contrib
提供了一组同步和异步的 CRUD 扩展方法,适用于标准实体类。需要配合特性使用:
csharp
[Table("Dogs")]
public class Dog {
[Key] // 指定主键
public int Id { get; set; }
public string Name { get; set; }
[Write(false)] // 标记为只读字段,不参与插入或更新
public string ComputedField { get; set; }
}
// 使用方式
conn.Insert(new Dog { Name = "Lucky" });
var dog = conn.Get<Dog>(1);
dog.Name = "New Name";
conn.Update(dog);5.2 Dapper.SimpleCRUD
类似于 Contrib,但提供了更灵活的条件查询支持。
csharp
// 根据匿名对象条件获取列表
var dogs = conn.GetList<Dog>(new { Age = 2 });
// 统计数量
int count = conn.RecordCount<Dog>("WHERE Age > @Age", new { Age = 1 });5.3 Dapper.SqlBuilder
用于构建复杂的动态 SQL,避免繁琐的字符串拼接。
csharp
var builder = new SqlBuilder();
var selector = builder.AddTemplate("SELECT * FROM Dogs /**where**/ /**orderby**/");
if (age.HasValue) builder.Where("Age = @age", new { age });
if (!string.IsNullOrEmpty(name)) builder.Where("Name LIKE @name", new { name = $"%{name}%" });
builder.OrderBy("Age DESC");
var dogs = conn.Query<Dog>(selector.RawSql, selector.Parameters);6. 自定义类型处理器 (TypeHandlers)
当数据库存储的是 JSON 字符串或特殊的 Enum 时,可以使用 ITypeHandler 自动转换。
csharp
public class JsonTypeHandler : SqlMapper.ITypeHandler {
public void SetValue(IDbDataParameter parameter, object value) {
parameter.Value = JsonConvert.SerializeObject(value);
}
public object Parse(Type destinationType, object value) {
return JsonConvert.DeserializeObject(value.ToString(), destinationType);
}
}
// 注册处理器
SqlMapper.AddTypeHandler(new JsonTypeHandler());7. 避坑指南与性能优化
6.1 性能优化最佳实践
- 使用异步方法:在 Web 环境下,务必使用
QueryAsync和ExecuteAsync以提高系统吞吐量。 - 正确处理 NULL:Dapper 在参数映射时如果遇到 C# 值为
null,会自动转为数据库的DBNull。 - 避免过度使用 Dynamic:虽然
dynamic很方便,但强类型Query<T>的性能略高且具有编译期检查。 - 缓冲查询 (Buffered):
Query方法默认buffered: true,会将所有结果一次性加载到内存。对于超大数据集,建议设为false以使用流式处理。 - SQL 注入防御:始终使用参数化查询。Dapper 的参数化不仅是为了安全,更是为了让数据库引擎能缓存查询计划。
6.2 性能对比参考 (典型场景)
| 框架 | 查询耗时 (ms) | 内存占用 | 易用性 |
|---|---|---|---|
| ADO.NET (DataReader) | 47 | 最低 | 低 |
| Dapper | 49 | 极低 | 中 |
| Entity Framework 6 | 600+ | 高 | 高 |
| EF Core | 150+ | 中 | 高 |
数据仅供参考,反映了 Dapper 在简单查询场景下几乎等同于原生性能的特点。
6.3 常见问题排查
- 参数名匹配:Dapper 参数不区分大小写,但建议保持一致。
- In 语法:
WHERE Id IN @ids中,@ids必须是IEnumerable类型。 - Like 语法:正确写法是
LIKE @name且参数值为"%val%",而不是在 SQL 中拼接%。 - 超时处理:复杂统计查询务必设置
commandTimeout参数,防止数据库锁死导致应用崩溃。 - IDbConnection 状态:虽然 Dapper 内部会判断连接状态,但如果手动开启了事务,必须手动 Open 连接,否则会报错。
