Dapper ORM 教程:轻量级、高性能的“Micro-ORM”
目录
- 什么是 Dapper?
- 为什么选择 Dapper?
- 环境准备
- 基础 CRUD 操作
- 查询单条数据
- 查询多条数据
- 参数化查询
- 插入数据
- 更新数据
- 删除数据
- 进阶用法
- 多映射与一对一查询
- 多映射与一对多查询
- 动态查询
- 存储过程调用
- 事务处理
- 最佳实践与技巧
IDbConnection的管理- Dapper 扩展方法(如 Dapper.Contrib)
- 异步方法
- 性能考虑
什么是 Dapper?
Dapper 是一个由 Sam Saffron (Stack Overflow 团队成员) 开发的轻量级、高性能的 Micro-ORM (微型对象关系映射) 库。

- Micro-ORM: 相比于 Entity Framework (EF) 或 NHibernate 这样功能全面的 ORM 框架,Dapper 只提供了最核心的数据映射功能,它不负责模型创建、数据库迁移、变更跟踪等复杂功能。
- 高性能: Dapper 的核心是一个扩展方法库,它通过 Emit 动态生成 IL 代码来执行数据读取和对象映射,其性能非常接近于手写 ADO.NET 代码,因此被誉为“King of Micro-ORMs”。
- 简单易用: Dapper 的 API 非常简洁,只需要学习几个核心方法,就能上手进行数据库操作。
核心思想: Dapper 不会改变你写 SQL 的方式,它只是帮你把执行 SQL 后返回的 DataReader 结果集,自动映射成你定义的 C# 对象。
为什么选择 Dapper?
| 特性 | Dapper | Entity Framework (EF Core) |
|---|---|---|
| 性能 | 极高,接近原生 ADO.NET | 良好,但通常低于 Dapper |
| 控制力 | 完全控制 SQL,可以写任意复杂的 SQL | 通过 LINQ 或 Fluent API,有时无法生成最优 SQL |
| 学习曲线 | 非常低,只需懂 SQL 和 C# | 较高,需要理解 EF 的概念和机制 |
| 功能 | 轻量,只做映射 | 重量级,包含 Change Tracking, Unit of Work, Migration 等 |
| 适用场景 | 读多写少、对性能要求高、SQL 复杂的项目 | 中小型项目、快速开发、对 SQL 不熟悉的团队 |
如果你追求极致的性能,需要精细控制 SQL,或者你的项目逻辑非常复杂以至于 EF 难以应对,Dapper 是绝佳选择。
环境准备
-
安装 NuGet 包: 在你的 .NET 项目 (如 .NET 6/7/8 Console, Web API, WinForms) 中,通过 NuGet 包管理器控制台或管理器界面安装
Dapper。Install-Package Dapper
-
创建数据模型: 我们将使用一个简单的
User模型作为示例。
(图片来源网络,侵删)public class User { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public DateTime CreatedAt { get; set; } } -
准备数据库: 创建一个数据库和一张
Users表。CREATE DATABASE MyDb; GO USE MyDb; GO CREATE TABLE Users ( Id INT PRIMARY KEY IDENTITY(1,1), Name NVARCHAR(100) NOT NULL, Email NVARCHAR(255) NOT NULL UNIQUE, CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE() ); GO
基础 CRUD 操作
所有 Dapper 的扩展方法都来自于 System.Data 命名空间下的 IDbConnection 接口,你需要一个实现了该接口的数据库连接对象(如 SqlConnection, MySqlConnection 等)。
using System;
using System.Data;
using System.Data.SqlClient;
using Dapper;
using System.Collections.Generic;
public class UserRepository
{
private readonly string _connectionString;
public UserRepository(string connectionString)
{
_connectionString = connectionString;
}
// 辅助方法:获取数据库连接
private IDbConnection GetConnection()
{
return new SqlConnection(_connectionString);
}
}
查询单条数据: QueryFirst, QueryFirstOrDefault
使用 QueryFirstOrDefault 是最安全的方式,当查询没有结果时返回 null,避免异常。
public User GetUserById(int id)
{
using (var connection = GetConnection())
{
// SQL 语句中必须包含与 User 属性名匹配的列
string sql = "SELECT Id, Name, Email, CreatedAt FROM Users WHERE Id = @Id";
// Dapper 会自动将 @Id 参数化,防止 SQL 注入
// QueryFirstOrDefault 会返回第一个匹配的用户,如果没有则返回 null
return connection.QueryFirstOrDefault<User>(sql, new { Id = id });
}
}
查询多条数据: Query, QueryAsync
Query 方法返回一个 IEnumerable<T>。

public IEnumerable<User> GetAllUsers()
{
using (var connection = GetConnection())
{
string sql = "SELECT Id, Name, Email, CreatedAt FROM Users";
// Query 方法会自动将结果映射到 User 对象的集合
return connection.Query<User>(sql);
}
}
参数化查询
Dapper 强烈推荐使用参数化查询,它既安全又高效,有两种方式:
-
匿名对象 (推荐):
new { PropertyName = value }// 上面的 GetUserById 和 GetAllUsers 已经展示了这种方式 connection.Query<User>("SELECT * FROM Users WHERE Name = @Name", new { Name = "John Doe" }); -
动态对象:
new { dynamic }connection.Query<User>("SELECT * FROM Users WHERE CreatedAt > @Date", new { Date = DateTime.Now.AddDays(-7) });
插入数据: Execute
Execute 方法用于执行 INSERT, UPDATE, DELETE 等不返回结果集的命令,并返回受影响的行数。
public int AddUser(User user)
{
using (var connection = GetConnection())
{
string sql = "INSERT INTO Users (Name, Email) VALUES (@Name, @Email); SELECT CAST(SCOPE_IDENTITY() as int)";
// Execute 的最后一个参数是命令类型,这里我们返回新插入行的 ID
// Dapper 可以轻松处理 "SELECT ...; INSERT ..." 这样的复合语句
var newId = connection.Query<int>(sql, user).Single();
return newId;
}
}
注意: 为了获取新插入记录的 ID,我们使用了 ; SELECT SCOPE_IDENTITY() as int 这种 SQL Server 的标准做法,Dapper 会执行整个命令并返回 SELECT 查询的结果。
更新数据: Execute
public bool UpdateUser(User user)
{
using (var connection = GetConnection())
{
string sql = "UPDATE Users SET Name = @Name, Email = @Email WHERE Id = @Id";
// Execute 返回受影响的行数
int affectedRows = connection.Execute(sql, user);
return affectedRows > 0;
}
}
删除数据: Execute
public bool DeleteUser(int id)
{
using (var connection = GetConnection())
{
string sql = "DELETE FROM Users WHERE Id = @Id";
int affectedRows = connection.Execute(sql, new { Id = id });
return affectedRows > 0;
}
}
进阶用法
多映射与一对一查询
假设我们有 Post 和 Blog 两个模型,一个 Post 属于一个 Blog。
public class Blog
{
public int Id { get; set; }
public string Url { get; set; }
}
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; }
public string Title { get; set; }
public Blog Blog { get; set; } // 一对一关系
}
我们需要写一个 JOIN 查询,并告诉 Dapper 如何将结果映射到这两个对象。
public Post GetPostWithBlog(int postId)
{
using (var connection = GetConnection())
{
string sql = @"SELECT p.Id, p.BlogId, p.Title, b.Id, b.Url
FROM Posts p
INNER JOIN Blogs b ON p.BlogId = b.Id
WHERE p.Id = @PostId";
// Query 是一个泛型方法,可以指定多个类型
// 第一个参数是主要结果类型,后面的参数是关联类型
// splitOn 参数告诉 Dapper 从哪个列开始解析下一个对象 (默认是 "Id")
var post = connection.Query<Post, Blog, Post>(
sql,
(post, blog) =>
{
post.Blog = blog; // 手动关联对象
return post;
},
new { PostId = postId },
splitOn: "BlogId" // 指定 Blog 对象的 Id 列名
).FirstOrDefault();
return post;
}
}
多映射与一对多查询
一个 Blog 可以有多个 Post。
public class Blog
{
public int Id { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; } = new List<Post>(); // 一对多关系
}
我们需要使用 GroupBy 来将多个 Post 关联到一个 Blog。
public Blog GetBlogWithPosts(int blogId)
{
using (var connection = GetConnection())
{
string sql = @"SELECT b.Id, b.Url, p.Id, p.BlogId, p.Title
FROM Blogs b
LEFT JOIN Posts p ON b.Id = p.BlogId
WHERE b.Id = @BlogId";
// 使用 GroupBy 来处理一对多关系
var blogDictionary = new Dictionary<int, Blog>();
connection.Query<Blog, Post, Blog>(
sql,
(blog, post) =>
{
// 如果字典中没有这个 blog,则添加进去
if (!blogDictionary.TryGetValue(blog.Id, out var currentBlog))
{
currentBlog = blog;
currentBlog.Posts = new List<Post>();
blogDictionary.Add(currentBlog.Id, currentBlog);
}
// post 不为空 (防止 LEFT JOIN 导致的空行),则添加到列表
if (post != null)
{
currentBlog.Posts.Add(post);
}
return currentBlog;
},
new { BlogId = blogId },
splitOn: "PostId"
).Distinct().FirstOrDefault(); // 使用 Distinct 去除重复的 Blog 对象
return blogDictionary.Values.FirstOrDefault();
}
}
动态查询
Dapper 可以将查询结果映射为 dynamic 对象,这在处理不确定列名的查询时非常有用。
public dynamic GetDynamicUserData(int id)
{
using (var connection = GetConnection())
{
string sql = "SELECT Id, Name, Email FROM Users WHERE Id = @Id";
return connection.QueryFirstOrDefault(sql, new { Id = id });
}
}
// 使用
var user = GetDynamicUserData(1);
Console.WriteLine(user.Name); // 可以通过属性名访问
存储过程调用
调用存储过程非常简单,只需设置 commandType 即可。
public User GetUserByStoredProcedure(int id)
{
using (var connection = GetConnection())
{
var p = new DynamicParameters();
p.Add("@Id", id); // 输入参数
p.Add("@Name", dbType: DbType.String, direction: ParameterDirection.Output); // 输出参数
connection.Execute("sp_GetUserById", p, commandType: CommandType.StoredProcedure);
// 获取输出参数的值
string userName = p.Get<string>("@Name");
// 或者你也可以让存储过程返回一个结果集,然后用 Query 获取
// var user = connection.QueryFirstOrDefault<User>("sp_GetUserById", p, commandType: CommandType.StoredProcedure);
return new User { Name = userName }; // 这里简化了,实际中可能需要更复杂的逻辑
}
}
事务处理
Dapper 的事务处理与 ADO.NET 原生方式一致。
public void TransferFunds(int fromUserId, int toUserId, decimal amount)
{
using (var connection = GetConnection())
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
// 扣款
connection.Execute("UPDATE Accounts SET Balance = Balance - @Amount WHERE UserId = @FromUserId",
new { Amount = amount, FromUserId = fromUserId }, transaction);
// 付款
connection.Execute("UPDATE Accounts SET Balance = Balance + @Amount WHERE UserId = @ToUserId",
new { Amount = amount, ToUserId = toUserId }, transaction);
// 如果所有操作都成功,则提交事务
transaction.Commit();
}
catch
{
// 如果发生任何异常,则回滚事务
transaction.Rollback();
throw; // 重新抛出异常
}
}
}
}
最佳实践与技巧
IDbConnection 的管理
-
使用
using语句: 这是必须的!IDbConnection实现了IDisposable,使用using可以确保连接在使用完毕后被正确关闭和释放,避免连接泄露。// 正确 using (var connection = new SqlConnection(_connectionString)) { // ... do work ... } // 错误!连接不会被关闭 // var connection = new SqlConnection(_connectionString); // ... do work ... -
依赖注入: 在大型应用中,不要在每个方法中都创建连接,应该通过依赖注入容器(如 ASP.NET Core 的内置容器)将
IDbConnection或一个封装了连接的仓储类注入到需要的地方。
Dapper 扩展方法 (如 Dapper.Contrib)
官方的 Dapper 包只提供了核心的查询和执行方法,如果你想获得类似 EF 的 Insert, Update, Delete, GetAll 等便捷方法,可以使用 Dapper.Contrib 包。
Install-Package Dapper.Contrib
使用前,需要在连接上设置 Configure。
using Dapper.Contrib.Extensions; // 引入命名空间
// 在连接上配置
SqlMapperExtensions.TableNameMapper = (type) => type.Name; // 将类名映射为表名
public void AddUserWithContrib(User user)
{
using (var connection = GetConnection())
{
// 直接传入对象即可,Dapper.Contrib 会自动生成 INSERT 语句
connection.Insert(user);
}
}
public void UpdateUserWithContrib(User user)
{
using (var connection = GetConnection())
{
connection.Update(user);
}
}
注意: Dapper.Contrib 的自动映射功能很方便,但在复杂场景下(如列名与属性名不一致、需要复杂映射)不如手写 SQL 灵活。
异步方法
Dapper 提供了所有核心方法的异步版本,以避免 I/O 操作阻塞线程,这对于构建高性能的 Web 服务至关重要。
// 异步查询
public async Task<User> GetUserByIdAsync(int id)
{
using (var connection = GetConnection())
{
await connection.OpenAsync(); // 异步打开连接
return await connection.QueryFirstOrDefaultAsync<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = id });
}
}
// 异步执行
public async Task<int> AddUserAsync(User user)
{
using (var connection = GetConnection())
{
await connection.OpenAsync();
return await connection.QuerySingleAsync<int>(/* insert sql with select identity */);
}
}
性能考虑
-
批量操作: 当需要插入或更新大量数据时,不要在循环中调用
Execute,这会产生大量的数据库往返,性能极差,应该使用DataTable或字符串拼接的方式一次性执行。错误示范:
foreach(var user in users) { connection.Execute("INSERT ...", user); }正确示范 (使用 Table-Valued Parameter):
// 1. 将 List<User> 转换为 DataTable var table = new DataTable(); table.Columns.Add("Name", typeof(string)); table.Columns.Add("Email", typeof(string)); foreach (var user in users) { table.Rows.Add(user.Name, user.Email); } // 2. 创建参数 var tvp = new SqlParameter { ParameterName = "@Users", SqlDbType = System.Data.SqlDbType.Structured, Value = table, TypeName = "dbo.UserTableType" // 你的数据库中必须定义一个对应的表类型 }; // 3. 执行存储过程或 SQL connection.Execute("sp_InsertUsers", tvp, commandType: CommandType.StoredProcedure);
Dapper 是一个强大而灵活的工具,它完美地平衡了性能和开发效率,它不会强迫你使用某种特定的开发模式,而是将控制权完全交还给开发者。
何时使用 Dapper:
- 对性能有极致要求的应用。
- 数据库查询非常复杂,ORM 生成的 SQL 效率低下。
- 项目团队对 SQL 非常熟悉,希望获得完全的控制权。
- 作为现有 EF Core 项目的补充,用于处理性能关键的热点路径。
学习 Dapper 的建议:
- 掌握基础: 先熟练掌握
Query,Execute,QueryFirst等核心方法。 - 精通参数化: 始终使用参数化查询来保证安全和性能。
- 学习映射: 理解 Dapper 如何将列名映射到属性名,并掌握一对一、一对多的多映射技巧。
- 拥抱异步: 在任何可能阻塞 I/O 的地方,都优先使用异步方法。
- 实践事务: 理解事务的重要性,并学会在 Dapper 中正确使用它。
通过这份教程,你已经具备了使用 Dapper 进行日常开发的能力,多加练习,你会发现它将成为你工具箱中不可或缺的利器。
