刚入行那会儿,我跟很多人一样 EF Core 一把梭。直到有一次做个报表接口,数据量不大但联了七八张表,EF 生成的 SQL 跑了 6 秒。换成 Dapper 手写 SQL,同样的查询 0.4 秒。从那之后,我手里所有对性能有要求的项目,数据访问层全是 Dapper。
这篇文章从零开始,把 Dapper 同步写法的核心讲清楚,顺带聊聊实际项目里真正会出问题的地方。

一句话:你把 SQL 丢给它,它把结果映射成你的实体对象。
没有状态跟踪,没有变更检测,没有延迟加载——所以它快。Stack Overflow 自己就在生产环境跑它,日请求量你想想看。
Dapper 本体:
dotnet add package Dapper
然后按数据库装驱动:
Microsoft.Data.SqlClientMySqlConnectorNpgsqlMicrosoft.Data.SqliteOracle.ManagedDataAccess.Core先说个真实的事。
去年我们一个订单服务,某天下午突然开始大面积超时,日志里全是 Timeout expired。查了半天发现新来的同事在一个定时任务里写了 new SqlConnection(connStr),没有 using,也没调 Close()。这个任务每 30 秒跑一次,每次泄漏一个连接。跑了半天,连接池 100 个连接全部占满——剩下所有接口都在排队等连接。

所以这句请记住:Dapper 不会帮你管理连接,你必须手动 using。
标准写法就下面这样,别纠结,照抄:
string connectionString = "Server=.;Database=TestDB;Integrated Security=true;";
using (IDbConnection db = new SqlConnection(connectionString))
{
db.Open();
// 你的操作
}
另外,永远不要在类里搞一个 static SqlConnection 当全局单例用。连接池本身就是干这个的,你不需要自己缓存连接。
下面两个类后面全系列都在用:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
}
public class Address
{
public int UserId { get; set; }
public string City { get; set; }
public string Street { get; set; }
}
User 里有个 Address 导航属性,联表映射的时候用来装结果。

查多条用 Query<T>,查单条用 QueryFirstOrDefault<T>。区别很简单:
// 查列表
var users = db.Query<User>("SELECT * FROM Users WHERE Age > @Age", new { Age = }).ToList();
// 查单条——查不到返回 null,不会抛异常,日常最常用
var user = db.QueryFirstOrDefault<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = });
Dapper 的传参靠匿名对象,new { Age = 18 } 自动变成 @Age 参数。你别手拼 SQL 字符串,拼了就是注入漏洞。
QueryFirst、QueryFirstOrDefault、QuerySingleOrDefault 三个的区别:第一个查不到抛异常,第二个查不到返回 null,第三个查到多条抛异常。日常用第二个最多;按主键查的时候可以用 QuerySingleOrDefault,万一主键重复多返回一行能立刻报错,比悄悄用第一条强。
统一用 Execute(),返回受影响行数。插入更新删除全一个写法:
db.Execute("INSERT INTO Users (Name, Email, Age) VALUES (@Name, @Email, @Age)",
new { Name = "Bob", Email = "bob@test.com", Age = });
db.Execute("UPDATE Users SET Name = @Name WHERE Id = @Id",
new { Name = "Robert", Id = });
db.Execute("DELETE FROM Users WHERE Id = @Id", new { Id = });
没什么花头,就这么用。
联表的坑在于 splitOn 参数。它告诉 Dapper:从结果集的第几列开始,后面的字段映射给第二个实体。
string sql = @"SELECT u.Id, u.Name, a.City, a.Street
FROM Users u JOIN Addresses a ON u.Id = a.UserId";
var users = db.Query<User, Address, User>(sql,
(user, addr) => { user.Address = addr; return user; },
splitOn: "City"
).Distinct().ToList();
Id、Name → User,City、Street → Address。splitOn 填的是 Address 的第一个字段名。
新手容易在这里翻车:splitOn 拼错了,或者列的先后顺序跟代码里实体顺序对不上,查出来的数据就全 null。这个问题我见过不下五次——没有报错,就是字段全是空,半天才反应过来。
多一行参数的事:
var user = db.Query<User>("GetUserById", new { Id = },
commandType: CommandType.StoredProcedure).FirstOrDefault();
事务的核心就一条:几条 SQL 共用一个 IDbTransaction,全部成功才提交,一条失败全回滚。

using (IDbConnection db = new SqlConnection(connectionString))
{
db.Open();
using (var tran = db.BeginTransaction())
{
try
{
db.Execute("INSERT INTO Users(Name) VALUES (@Name)",
new { Name = "TestUser" }, transaction: tran);
db.Execute("UPDATE Logs SET Status='Done' WHERE Id=@Id",
new { LogId = }, transaction: tran);
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
}
}
注意:操作里必须传 transaction: tran,漏传的话那条 SQL 就不在事务里了,回滚回不来,数据就脏了。
第一章就到这。核心 API 这点东西其实不复杂,Dapper 的优势从来不是"功能多",而是"够用且快"。
下一篇讲异步写法。这是 Web API 逃不掉的话题,而且异步用不好比同步还危险——.Result 死锁的故事下章细说。
(点击关注,修炼不迷路👇)
▌转载请注明出处,渡人渡己
🌟 感谢道友结缘! 若本文助您突破修为瓶颈,不妨【打赏灵丹】或【转发功德】,让更多道友共参.NET天道玄机。修真之路漫漫,我们以代码为符,共绘仙途!