Skip to content

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 的深度利用。

  1. 动态方法生成:当第一次执行查询时,Dapper 会根据 SQL 结果集的结构和目标 POCO 类的属性,动态生成一个 IL 代码块。
  2. 高速缓存:这个生成的 IL 代码会被缓存起来。后续相同的查询将直接调用这个已编译的“硬编码”方法,避开了传统反射在循环中的巨大开销。
  3. 数据读取:它直接操作 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 性能优化最佳实践 ​

  1. 使用异步方法:在 Web 环境下,务必使用 QueryAsync 和 ExecuteAsync 以提高系统吞吐量。
  2. 正确处理 NULL:Dapper 在参数映射时如果遇到 C# 值为 null,会自动转为数据库的 DBNull。
  3. 避免过度使用 Dynamic:虽然 dynamic 很方便,但强类型 Query<T> 的性能略高且具有编译期检查。
  4. 缓冲查询 (Buffered):Query 方法默认 buffered: true,会将所有结果一次性加载到内存。对于超大数据集,建议设为 false 以使用流式处理。
  5. SQL 注入防御:始终使用参数化查询。Dapper 的参数化不仅是为了安全,更是为了让数据库引擎能缓存查询计划。

6.2 性能对比参考 (典型场景) ​

框架查询耗时 (ms)内存占用易用性
ADO.NET (DataReader)47最低低
Dapper49极低中
Entity Framework 6600+高高
EF Core150+中高

数据仅供参考,反映了 Dapper 在简单查询场景下几乎等同于原生性能的特点。

6.3 常见问题排查 ​

  • 参数名匹配:Dapper 参数不区分大小写,但建议保持一致。
  • In 语法:WHERE Id IN @ids 中,@ids 必须是 IEnumerable 类型。
  • Like 语法:正确写法是 LIKE @name 且参数值为 "%val%",而不是在 SQL 中拼接 %。
  • 超时处理:复杂统计查询务必设置 commandTimeout 参数,防止数据库锁死导致应用崩溃。
  • IDbConnection 状态:虽然 Dapper 内部会判断连接状态,但如果手动开启了事务,必须手动 Open 连接,否则会报错。

8. 参考资料 ​

方舟 · Ark