直接写干货。这篇我聊聊工作中的常客——Dapper。它的江湖地位不用多讲,整个.NET生态里知名度最高、使用面最广的轻量级ORM之一。开发圈里有人把它比作“数据库世界的快递小哥”:没有重型框架那么多条条框框,但它够快、够直接,接单就送,使命必达。这种比喻其实挺贴切,Dapper的设计哲学就是这么朴素——不塞给你一堆用不上的抽象和生成逻辑,省去繁琐的映射代码,把SQL的执行效率和结果映射做到极致,然后把控制权完全交还给你。
这篇文章适合谁看?后端.NET开发、刚入行想搞明白ORM到底解决什么问题的学生,还有做微服务或者高并发接口,被EF Core性能和复杂配置折磨得够呛,想找一条“既有手感又不太费劲”的取数方案的技术选型决策者。文中我会从Dapper的角色定位开始,顺一遍它的核心几个用法和踩过的坑,最后给一份能直接落地的增删改查实践和常见错误排查实录,希望对你有用。
1. 内容整体设计与思路拆解
1.1 先从“快递小哥”这个比喻说起
“快递小哥”这个提法,我第一次听到是在一次技术群里讨论“数据库访问层到底选什么”的时候。有位老哥来了一句:“EF Core就像顺丰冷链,啥都能运但包装多,成本高;Dapper就是同城闪送,不搞虚的,一单一跑。”话糙理不糙。
把Dapper理解为“快递小哥”,核心在于理解它的分工原则:不管你的数据表结构多复杂,关系多少层,Dapper只做两件核心事——把SQL发给数据库,把返回的结果集塞进你的对象模型。它不负责帮你自动生成SQL,不负责建库建表迁移,也不搞复杂的懒加载方案。你要查什么、怎么查、要不要分页、在哪加索引,全部自己说了算。SQL性能出了问题,你直接拿SQL去数据库工具里调;业务代码有问题,断点打到SQL执行前一帧。整个链路干净利落,没有任何黑盒。
这种“少即是多”的设计,带来的实际收益是:学习成本趋近于零。如果你熟悉ADO.NET,那Dapper的API几乎不需要学,只是把原来手工逐字段赋值的过程省掉了。原来写DataReader要写一长串while循环加GetString、GetInt32,Dapper一句connection.Query<T>(sql)就完事,剩下你只需要关心SQL本身写得好不好、数据库表结构设计得优不优。
1.2 为什么是Dapper:在性能与生产力之间找平衡
选型本质上是在做权衡。ORM全家桶(比如EF Core)提供了一整套CRUD、状态追踪、自动迁移、导航属性,开发效率确实高,但它们带来的代价也肉眼可见:内存中的状态管理消耗、SQL生成未必符合最优执行计划、排查问题时你需要额外了解框架内部的缓存机制和转换规则。在某些高性能场景下,EF Core生成的SQL还会出现令人头疼的N+1查询,或者需要大量引入AsNoTracking、拆分查询这类“反框架”写法去优化。
Dapper的路线跟它正好相反:把SQL保留在最显眼的位置,让每个开发者都清楚知道数据库将会收到什么命令。你写什么,它就执行什么。由于省掉了状态追踪和表达式树的层层翻译开销,Dapper在绝大多数情况下非常接近手写ADO.NET的性能,同时又保住了对象映射的手感。如果项目里需要跑复杂报表查询、数据量大、表关系灵活,Dapper这种“贴着数据库一线上场跑单”的特性,会省掉你巨量的调优时间。
这里要提醒一点,Dapper并不是要跟EF Core拼得你死我活。实际工程里两者可以共存:写简单的CQRS查询模型时用Dapper,写复杂的领域模型更新时用EF Core。以较少学习和运维成本拿到80%场景下的高查询性能,这种组合在很多中型项目里相当常见,也是我在不少线上项目里验证过的稳妥搭配。
1.3 Dapper与主流ORM的选型对决
为了有直观请感,我把实际中常用到的几类数据访问方案放同一张表里对比一下:
| 方案 | 性能 | 开发效率 | SQL可控性 | 学习曲线 | 适合场景 |
|---|---|---|---|---|---|
| ADO.NET原生 | 最高 | 低 | 绝对可控 | 中 | 极致性能、底层框架封装 |
| Dapper | 很高 | 高 | 绝对可控 | 极低 | 高性能API、报表查询、快速迭代 |
| EF Core | 中高(需调优) | 很高 | 弱(需干预) | 较高 | 业务复杂、领域模型重的系统 |
| SqlSugar | 高 | 高 | 较可控 | 低 | 国内中小项目、快速交付 |
| 纯手写SQL+反射 | 中高 | 中 | 绝对可控 | 中 | 特殊定制场景 |
这张表格的结论很清楚:如果你追求的是“接近手写SQL的性能”和“快速映射对象的效率”之间的平衡,Dapper基本是首选。执行一个简单查询,Dapper的开销比EF Core低一个数量级都不夸张,因为EF Core要把表达式树翻译成SQL,还要做状态追踪,Dapper则是一条路走到底。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 建立连接:打包好“快递”包裹
前面说了,Dapper是扩展方法,依附在IDbConnection接口上工作。这意味着你可以用它操作多种数据库:SQL Server、MySQL、PostgreSQL、SQLite、Oracle,甚至国产数据库达梦、人大金仓。Dapper本身不发起连接,你用什么数据库,就引用什么ADO.NET驱动,通过标准的连接串开一个连接对象,然后它在这个连接对象基础上施展拳脚。
开头引入的写法非常简单:
csharp复制using var connection = new SqlConnection(connectionString);
// 或
// using var connection = new MySqlConnection(connectionString);
连接是数据库操作里的“大件快递”,频繁创建销毁很贵。Dapper特别配合这个场景——它背后依赖的ADO.NET连接池会自动复用物理连接。每次Open()一个SqlConnection,实际拿到的可能是池里已有的连接,不需要重新握手建连。所以不用刻意把连接对象存成静态字段,连接池机制比那套做法更科学。
2.2 Query系列:查询派件的几种姿势
查询业务以Query为核心展开。Dapper对查询结果的处理极其灵活,可以根据场景在泛型对象、动态类型、原始标量之间任意切换。
最常用的两种:
Query<T>:映射到强类型集合。比如connection.Query<User>(sql)返回IEnumerable<User>,每条记录对应一个对象。QueryFirst<T>/QuerySingle<T>:返回单条结果。前者取第一行,后者要求结果集必须只有一行,多一行或者少一行都直接抛异常。
在实际项目中,Query<T>主要用于列表页、报表等批量数据场景,QueryFirst<T>适合拿用户信息、配置详情这一类的单条查询。还有QueryFirstOrDefault<T>和QuerySingleOrDefault<T>,用于可能查不到数据的情况,避免异常打断流程。
2.3 Execute系列:往数据库里“派单”
数据写操作对应Execute系列。插入、更新、删除都走这一个方法,返回受影响的行数。写法依然是以SQL为主,参数用匿名对象或者DynamicParameters传入。
csharp复制var sql = "UPDATE Users SET LastLoginTime = @now WHERE Id = @id";
int rows = connection.Execute(sql, new { now = DateTime.Now, id = userId });
这个方法没什么神秘之处,本质就是Command.ExecuteNonQuery()。所以判断更新是否成功,直接看返回值是否大于0即可。需要事务的话,在方法参数里传入事务对象,Dapper会在那个事务边界内执行。
2.4 事务:多包裹合并保平安
一个快递小哥不会把客户的三件快递分三次毫无关联地送,而是打包成一趟完成。Dapper里处理多个写操作,逻辑也类似,需要打包到一个事务里,保证要么全部成功,要么全部回滚。
Dapper用起来简洁的地方在于,直接把IDbTransaction实例传给每一个Execute即可:
csharp复制using var connection = new SqlConnection(connectionString);
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
connection.Execute("INSERT INTO Orders (OrderNo) VALUES (@OrderNo)", new { OrderNo = "SO1001" }, transaction);
connection.Execute("UPDATE Inventory SET Stock = Stock - 1 WHERE Sku = @Sku", new { Sku = "SKU-01" }, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
这个场景在电商系统里太常见了:创建订单和扣减库存必须同生共死,不然就会出现有订单没库存,或者库存减了订单没生成的尴尬局面。用毛巾包好,一单单发出去,任何一单出了问题全部撤回来,数据才靠得住。
2.5 参数化:保护数据安全的底线
Dapper在细节上最值得称道的设计,就是它强制引导使用者做参数化查询。上文的new { now = DateTime.Now, id = userId }匿名对象里@now、@id就是参数占位符,Dapper会自动将这些参数加到命令对象里。
为什么这一点重要?因为字符串拼接SQL的需求太普遍了——搜索、过滤、排序——但直接拼字符串等于给SQL注入留后门。将用户输入原封不动塞进Execute或Query的参数里,让数据库驱动处理好转义与处理,安全系数直接拉满。
我见过不少团队在切换到Dapper之前,习惯写string sql = "SELECT * FROM Users WHERE Name = '" + name + "'"这种代码。切到Dapper之后,我要求组内所有SQL一律参数化,一个例外都不放行。这不仅是代码规范的问题,是数据安全的底线问题。
3. 实操过程与核心环节实现
3.1 准备环境与数据库表结构
这一段我会用一套实际可跑的“用户管理”例子来演示。因为热词里出现了不少MySQL和国产数据库的搜索记录,我统一以MySQL为例,表结构如下:
sql复制CREATE TABLE `user_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_name` varchar(50) NOT NULL,
`gender` tinyint DEFAULT NULL,
`age` int DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
视情况可以换成达梦、人大金仓,SQL方言差别不大,Dapper大部分API可以直接迁移。
3.2 搭建项目引入Dapper
新建一个控制台项目,或者直接在已有Web项目里操作。在包管理器控制台执行:
bash复制Install-Package Dapper
或者用.NET CLI:
bash复制dotnet add package Dapper
针对MySQL还需要额外引入驱动包:
bash复制dotnet add package MySql.Data
3.3 完整实战:增删改查与查询列表
下面把这些组合成一个用户信息管理的小模块,完整代码块可以直接复制运行:
csharp复制using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using MySql.Data.MySqlClient;
public class UserInfo
{
public long Id { get; set; }
public string UserName { get; set; }
public int? Gender { get; set; }
public int? Age { get; set; }
public DateTime? CreatedAt { get; set; }
}
public class UserRepository
{
private readonly string _connStr;
public UserRepository(string connStr)
{
_connStr = connStr;
}
// 查询列表
public List<UserInfo> GetUsers(int pageIndex, int pageSize)
{
using var conn = new MySqlConnection(_connStr);
string sql = @"SELECT id AS Id, user_name AS UserName, gender AS Gender,
age AS Age, created_at AS CreatedAt
FROM user_info
ORDER BY id DESC
LIMIT @offset, @limit";
return conn.Query<UserInfo>(sql, new { offset = (pageIndex - 1) * pageSize, limit = pageSize }).AsList();
}
// 查询单条
public UserInfo GetById(long id)
{
using var conn = new MySqlConnection(_connStr);
string sql = @"SELECT id AS Id, user_name AS UserName, gender AS Gender,
age AS Age, created_at AS CreatedAt
FROM user_info
WHERE id = @id";
return conn.QueryFirstOrDefault<UserInfo>(sql, new { id });
}
// 新增
public long Insert(UserInfo user)
{
using var conn = new MySqlConnection(_connStr);
string sql = @"INSERT INTO user_info (user_name, gender, age, created_at)
VALUES (@UserName, @Gender, @Age, @CreatedAt);
SELECT LAST_INSERT_ID();";
return conn.ExecuteScalar<long>(sql, user);
}
// 更新
public int Update(UserInfo user)
{
using var conn = new MySqlConnection(_connStr);
string sql = @"UPDATE user_info
SET user_name = @UserName,
gender = @Gender,
age = @Age
WHERE id = @Id";
return conn.Execute(sql, user);
}
// 删除
public int Delete(long id)
{
using var conn = new MySqlConnection(_connStr);
string sql = @"DELETE FROM user_info WHERE id = @id";
return conn.Execute(sql, new { id });
}
}
class Program
{
static void Main()
{
var repo = new UserRepository("server=127.0.0.1;port=3306;database=test;uid=root;pwd=123456;charset=utf8mb4;");
var newUser = new UserInfo
{
UserName = "张三",
Gender = 1,
Age = 28,
CreatedAt = DateTime.Now
};
long newId = repo.Insert(newUser);
Console.WriteLine($"新增ID:{newId}");
var user = repo.GetById(newId);
Console.WriteLine($"查得用户:{user.UserName}, 年龄:{user.Age}");
user.Age = 29;
var rows = repo.Update(user);
Console.WriteLine($"更新行数:{rows}");
var list = repo.GetUsers(1, 10);
Console.WriteLine($"列表总数:{list.Count}");
}
}
这里有一个非常核心的细节:查询SQL里用id AS Id, user_name AS UserName做列别名。MySQL默认的列名是下划线风格,而C#类属性是PascalCase风格,两者不通过别名对齐的话,映射结果全是默认值。有同学喜欢在Dapper配置里加自动映射器,把下划线转成驼峰,那我建议能避免就避免,靠显式别名让字段对应关系一眼可读,后续维护成本更低。其实Dapper本身是支持列名到属性的不区分大小写匹配的,但下划线和驼峰之间不会自动转换,显式别名是最直白、零配置的解法。
3.4 多表查询与视图模型的灵活映射
Dapper不会帮你管理表之间的关系,但它在单次查询多表结果这件事上反而更利落。比如查询用户及其最近的订单,直接写JOIN,然后把结果映射到一个组合模型上:
csharp复制public class UserOrderView
{
public long Id { get; set; }
public string UserName { get; set; }
public long OrderId { get; set; }
public string OrderNo { get; set; }
public decimal Amount { get; set; }
}
public List<UserOrderView> GetUserOrders(long userId)
{
using var conn = new MySqlConnection(_connStr);
string sql = @"SELECT u.id AS Id, u.user_name AS UserName,
o.id AS OrderId, o.order_no AS OrderNo,
o.amount AS Amount
FROM user_info u
JOIN user_order o ON o.user_id = u.id
WHERE u.id = @userId
ORDER BY o.id DESC";
return conn.Query<UserOrderView>(sql, new { userId }).AsList();
}
这种方式就是很多人说的“轻量级查询模型”:不为每一个视图建EF实体,不搞导航属性,直接让SQL来决定返回形状,Dapper只负责搬运。在做报表、后台管理列表、数据导出这类需求时,这种写法会让你的效率成倍提升。
3.5 DynamicParameters:复杂参数的默认选择
当参数不只两个、且需要类型和方向控制时,匿名对象就不够用了。Dapper提供了DynamicParameters类,支持传入方向(ParameterDirection.Input/Output等)和数据库类型。
下面的例子演示了如何使用输出参数:
csharp复制var p = new DynamicParameters();
p.Add("@UserName", "李四");
p.Add("@Gender", 0);
p.Add("@Age", 22);
p.Add("@NewId", dbType: DbType.Int64, direction: ParameterDirection.Output);
connection.Execute(@"
INSERT INTO user_info (user_name, gender, age, created_at)
VALUES (@UserName, @Gender, @Age, NOW());
SET @NewId = LAST_INSERT_ID();", p);
long insertedId = p.Get<long>("@NewId");
Console.WriteLine($"插入的ID:{insertedId}");
这里要留意,MySQL在一条批处理SQL中允许用SET @变量 = LAST_INSERT_ID()给输出参数赋值。但并不是所有数据库都这么友好,用Oracle的时候,占位符参数名不能带@,要换成:。Dapper只是把你的参数传给数据库驱动,方言的语法细节还是各自为准。跨库的时候,SQL文本和参数占位符风格就得跟着目标数据库走,这一点没有捷径,只能多查对应数据库的手册。
3.6 存储过程与多结果集
调用存储过程时,CommandType必须显式指定,否则Dapper默认按文本SQL对待:
csharp复制var p = new DynamicParameters();
p.Add("@userId", 10);
p.Add("@totalCount", dbType: DbType.Int32, direction: ParameterDirection.Output, size: 16);
using var multi = connection.QueryMultiple("sp_get_user_and_orders", p,
commandType: CommandType.StoredProcedure);
var user = multi.ReadSingle<UserInfo>();
var orders = multi.Read<OrderInfo>().ToList();
int total = p.Get<int>("@totalCount");
我特别想强调一个实用场景:存储过程里带着好几条返回结果集时,哪怕只用其中一部分数据,也尽量把所有结果集读完。比如读了ReadSingle之后还有未消费的结果集,就直接让输出参数把剩余结果集忽略掉——连接上有命令未读取完会影响后续操作,这在Dapper下也可能触发“已有一个打开的DataReader”之类的错误。所以QueryMultiple用完后,别偷懒跳过中间的Read,该读的读完,或者至少尽快释放。
4. 常见问题与排查技巧实录
4.1 高频报错:Excutereader 要求已打开且可用的 connection
最近“dapper executereader 要求已打开且可用的 connection。连接的当前状态为打开”这个话题在开发者社区里被高频搜索,说明这是大家上手Dapper时最容易撞上的一道坎。这个报错的本质是:在你调用Query或Execute时,连接对象并没有处于可用状态。
实际排查时,我会按下面几个方向逐层查:
- 有没有在using块内操作:最常见的问题是,
using块已经执行完、连接自动关闭了,代码却还在尝试执行。我见过有同事把conn存到字段里,然后在另一个方法里直接使用,出口处连接已经释放,结果抛的线索就是这句话。 - 连接是否被提前关闭:有的开发者习惯手动
conn.Close(),再往下调用Dapper的扩展方法,自然报错。要记住,Dapper的扩展方法不会自动打开连接,它只会在你传入的连接上执行命令。 - 事务状态是否异常:如果事务已经
Commit或Rollback,连接因此被释放,而后又继续使用,也会出现这个错。 - 连接字符串是否配置正确:如果
MySqlConnection构造时用的连接串写错(比如端口不对、密码错误),Open()会抛出认证异常,而不是这个“已打开”错误。所以先确认你能不能在数据库工具里用同一套连接串连上。
要知道,Dapper在被调用时会先判断ConnectionState,如果连接是关闭的,部分版本会尝试自动打开,但在Command执行时如果连接又变成了关闭或者非可用状态,就会冒出这个错误。最严谨的写法是:连接的生命周期严格限定在一个方法内,用using管理,所有Dapper调用都在这个块中完成。
csharp复制// 推荐写法:连接生命周期与操作生命周期保持一致
public UserInfo GetUser(long id)
{
using var conn = new MySqlConnection(_connStr);
string sql = "...";
return conn.QueryFirstOrDefault<UserInfo>(sql, new { id });
}
4.2 其他高频问题速查表
结合热词信息,我把项目里经常遇到的问题整理成一个速查表,方便大家直接对照排查:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 数据查出来全是null | 列名下划线与属性命名风格不一致 | SQL中显式使用AS别名对齐 |
| 插入返回的自增ID始终为0 | 没有使用ExecuteScalar<long>或没有拼接SELECT LAST_INSERT_ID() |
按上述示例处理返回ID |
| 已有一个打开的DataReader与该Command关联 | 在同一个连接上迭代一个查询结果时执行另一个查询 | 使用不同连接,或把结果先ToList,或启用MARS |
| 连接池耗尽,报超时 | 连接未及时释放;事务悬挂 | 用using释放,事务代码块内尽早提交/回滚 |
| 分页偏移量不正确 | LIMIT @offset, @limit参数类型未指定 |
确保offset、limit为int型,不要用字符串拼接 |
4.3 三个写入代码前的好习惯
养成这几个习惯,Dapper项目能少踩不少坑:
第一,SQL优先写进数据库工具里验证。调试SQL时直接在数据库工具里执行一遍,确认执行计划和结果集都OK,再粘进代码,不要代码与SQL来回调试。用DBeaver、Navicat这类数据库工具先验证查询,再保留一份SQL作为最终代码文本,这个工作流能节省很多时间。
第二,能用强类型对象映射就不要全程dynamic。虽然Dapper支持dynamic返回,但到了业务层,动态类型的字段名打错时编译期完全发现不了,运行时才炸锅。强类型不仅安全,还让字段关系清晰,重构时能依赖编译器的力量。
第三,把数据库调用集中封装到仓储或数据层,不要散落在控制器或页面代码里。这不是Dapper的特殊要求,而是工程化的通用约束。Dapper的灵活特性很容易让开发者随处使用,一旦业务增多,到处散落的SQL会把项目搅成一锅粥。
4.4 大型列表查询的缓冲区思维
Dapper的Query<T>默认会一次性把整张结果集读入内存,然后返回一个缓冲集合。如果查询数据量特别大,比如10万行以上,建议使用QueryBuffered: false,让它按需从数据库流式读取:
csharp复制var rows = connection.Query<OrderItem>(sql, buffered: false);
这个参数很多初学者不知道。缓冲模式便于多次枚举时避免重复读数据库,但不适合大结果集;非缓冲模式则一边遍历一边读,适合做大量数据导出、导入时要边读边处理边的场景。实际的教训是:我曾在日志导出场景里用默认缓冲方式查近百万条记录,内存占用直接冲到1GB,换成非缓冲模式后压力骤降。所以对于一次性大批量数据操作,记得关注这个选项。
5. 性能调优与最佳实践经验
5.1 查询优化:让快递小哥少跑路
Dapper的优点在于你可以完全控制SQL,所以性能优化的重心就回到了数据库本身。项目里几条重要经验:
- 查询条件字段建合适的索引,避免全表扫描;
- 分页查询用
LIMIT配合主键排序,而不是一次把全表数据拉到内存再分页; - 使用
EXPLAIN分析执行计划,发现慢查询及时调整SQL或索引; - 大字段(如BLOB、TEXT)只在真正需要时查出来,列表页不要
SELECT *,把大头数据留在详情页再取。
这些数据库优化技巧,配合Dapper直通SQL的特性,效果立竿见影。每个查询在代码里都能一眼看出它将要做什么,任何一个慢查询稍加分析就能定位到索引或写法层面的问题,不用怀疑是不是有ORM自动生成的过程躲在幕后变魔术。
5.2 事务与并发锁的正确姿势
数据库并发控制是后端开发绕不开的话题。热词里那么多关于“数据库并发锁”“数据库死锁”的搜索,说明很多项目里都遇到过这类头疼问题。Dapper不替你做锁管理,但它能让你在需要时使用数据库自己的锁能力。
比如扣减库存,一个简单的防超卖写库方式:
csharp复制using var conn = new MySqlConnection(_connStr);
conn.Open();
using var transaction = conn.BeginTransaction();
try
{
string updateSql = @"UPDATE inventory
SET stock = stock - @count
WHERE sku = @sku AND stock >= @count";
int affected = conn.Execute(updateSql, new { sku, count }, transaction);
if (affected == 0)
{
transaction.Rollback();
throw new Exception("库存不足");
}
conn.Execute("INSERT INTO order_item (sku, count) VALUES (@sku, @count)",
new { sku, count }, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
UPDATE ... WHERE stock >= @count这一句,依靠条件更新把并发“锁”的粒度收敛到特定行,在MySQL InnoDB里是行级锁,同一个SKU的并发更新会被串行化。这种做法比“先SELECT再判断库存再UPDATE”要安全得多,因为后者的读和写之间存在时间窗口,底层的并发问题会被放大。
5.3 小心数据库方言差异这个“隐藏坑”
Dapper跨库能力一直被高估。它不负责统一SQL方言。从MySQL切到达梦或人大金仓,很多分页SQL、日期函数、参数占位符都可能不兼容。最稳妥的方式是:把数据库访问和SQL隔离到一个独立的数据层,必要时为不同数据库编写不同的SQL实现,或者引入简单的方言策略。
我在支持国产数据库的过程中踩过的典型例子是:达梦数据库对LIMIT的写法支持跟MySQL很像,但某些版本里对SELECT LAST_INSERT_ID()或者自动主键的获取方式并不通用,需要改用它的序列或IDENTITY特性。这类问题没有银弹,只能靠提前在对应数据库上把SQL验证一遍来提前发现。
5.4 日志与监控:给快递路线留个底
线上出问题的时候,没有日志寸步难行。Dapper不提供内置日志,但在连接级别可以接上拦截器,或者干脆在SQL执行外层包一层日志记录。最简单的方式是利用Stopwatch记录耗时,同时把SQL文本和参数打出来,排查慢SQL时特别有用。
csharp复制public static class DbLogger
{
public static List<T> QueryAndLog<T>(this IDbConnection conn, string sql, object param = null)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = conn.Query<T>(sql, param).ToList();
sw.Stop();
if (sw.ElapsedMilliseconds > 500)
{
Console.WriteLine($"[SLOW] {sql} 耗时:{sw.ElapsedMilliseconds}ms");
}
return result;
}
}
这个扩展方法是我在小项目里快速加慢SQL日志的常用套路。生产环境更推荐用APM工具,或者把SQL文本送进日志平台,反正核心思路是一致:说清楚送到数据库的“单子”长什么样子、花了多长时间,优化和复盘才有据可查。
6. 一些实在的经验总结
最后聊点个人体会。
Dapper在项目里让我最舒服的一点,倒不是查数据有多快,而是当系统出了问题时,定位路径非常短。我不用先去理解框架帮我做了什么状态管理,不用去看表达式树生成的SQL到底在干什么,我只需要看数据库工具里执行同一句SQL的结果,再看代码逻辑,基本就能锁定问题出在哪。这种透明性,在维护老系统和排查线上问题时省下的时间,远比刚开始时少写几行代码重要得多。
但要提醒一句:Dapper不是万能解,它要求团队里写数据访问代码的人至少具备一定SQL功底,懂得基本的索引、事务和连接管理理念。如果团队普遍对SQL不熟,缺乏编写高效查询和排查慢SQL的能力,反而容易产出质量更差的代码。这种情境下,也许EF Core等更高层的框架更适合作为默认方案来保护你。
如果你是刚接触Dapper,我的建议很直接:先把官方文档过一遍,然后拿一个真实但简单的表,把查询、插入、更新、删除、事务、存储过程这些操作在自己的机器上挨个跑一遍,把各种报错亲手触发一次并解决一遍。这么一趟下来,你对SQL的执行流程、参数的传递机制、事务的边界,都会形成比看十篇文章都管用的手感。接下来碰到生产环境里各种突发状况,你就会发现,真正懂Dapper的人,从来不背API,他们只是很清楚SQL和连接这两件“快递单”该怎么填、怎么送。
