在.NET生态中,ORM框架的选择往往让人纠结。Entity Framework功能强大但略显笨重,纯ADO.NET又需要重复造轮子。我五年前接手一个日均百万级查询的电商系统时,Entity Framework在高峰期频繁出现性能瓶颈,直到把核心模块改用Dapper才解决问题。
Dapper最吸引我的是它保持原生SQL的精准控制,同时提供对象映射的便利性。比如处理分页查询时,EF Core生成的SQL可能包含不必要的JOIN,而Dapper允许你直接优化关键语句:
csharp复制var sql = @"SELECT * FROM Products
WHERE CategoryId = @categoryId
ORDER BY Price DESC
OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY";
var products = connection.Query<Product>(sql, new {
categoryId = 5,
skip = 20,
take = 10
});
这种轻量级特性使它在以下场景特别突出:
去年我做过的基准测试显示,在相同硬件环境下查询10000条记录:
差异主要来自三个方面:
但要注意,Dapper的性能优势主要体现在读操作。对于需要级联更新的事务场景,EF的ChangeTracker反而更高效。我的经验法则是:
Dapper通过ADO.NET基础提供程序支持几乎所有主流数据库。我在最近的项目中同时操作SQL Server和MySQL时,只需要切换连接对象:
csharp复制// SQL Server
using var sqlConn = new SqlConnection(connStr);
var users = sqlConn.Query<User>("SELECT * FROM Users");
// MySQL
using var mysqlConn = new MySqlConnection(mysqlConnStr);
var products = mysqlConn.Query<Product>("SELECT * FROM Products");
跨数据库分页是个典型痛点。不同数据库的语法差异可以通过Dapper的灵活性解决:
csharp复制string GetPagedQuery(string dbType) => dbType switch {
"sqlserver" => "OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY",
"mysql" => "LIMIT @Limit OFFSET @Offset",
"postgresql" => "LIMIT @Limit OFFSET @Offset",
_ => throw new NotSupportedException()
};
处理一对多关系时,Dapper的多重映射功能比想象中强大。比如查询博客及其所有评论:
csharp复制var sql = @"SELECT b.*, c.*
FROM Blogs b
LEFT JOIN Comments c ON b.Id = c.BlogId
WHERE b.Id = @id";
var blogDict = new Dictionary<int, Blog>();
var result = connection.Query<Blog, Comment, Blog>(
sql,
(blog, comment) => {
if (!blogDict.TryGetValue(blog.Id, out var existingBlog)) {
existingBlog = blog;
existingBlog.Comments = new List<Comment>();
blogDict.Add(blog.Id, existingBlog);
}
if (comment != null) existingBlog.Comments.Add(comment);
return existingBlog;
},
new { id = 1 },
splitOn: "Id"
);
对于存储过程调用,Dapper比EF更直观:
csharp复制var parameters = new DynamicParameters();
parameters.Add("@userId", 123);
parameters.Add("@result", dbType: DbType.Int32, direction: ParameterDirection.Output);
connection.Execute("sp_GetUserStats", parameters, commandType: CommandType.StoredProcedure);
var result = parameters.Get<int>("@result");
Dapper处理事务有两种推荐方式。简单事务:
csharp复制using var trans = connection.BeginTransaction();
try {
connection.Execute("UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1", transaction: trans);
connection.Execute("UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2", transaction: trans);
trans.Commit();
} catch {
trans.Rollback();
throw;
}
批量插入使用表值参数能大幅提升性能:
csharp复制var dt = new DataTable();
dt.Columns.Add("Name");
dt.Columns.Add("Age");
foreach(var user in users) {
dt.Rows.Add(user.Name, user.Age);
}
var param = new SqlParameter("@tvp", dt) {
TypeName = "dbo.UserTableType",
SqlDbType = SqlDbType.Structured
};
connection.Execute("sp_InsertUsers", param, commandType: CommandType.StoredProcedure);
经过多个项目实践,我总结出这些Dapper优化技巧:
一个常见的反模式是N+1查询问题。优化前:
csharp复制var users = connection.Query<User>("SELECT * FROM Users");
foreach(var user in users) {
user.Orders = connection.Query<Order>("SELECT * FROM Orders WHERE UserId = @id", new { id = user.Id });
}
优化后使用IN查询:
csharp复制var users = connection.Query<User>("SELECT * FROM Users").ToList();
var userIds = users.Select(u => u.Id);
var orders = connection.Query<Order>("SELECT * FROM Orders WHERE UserId IN @ids", new { ids = userIds })
.ToLookup(o => o.UserId);
foreach(var user in users) {
user.Orders = orders[user.Id].ToList();
}
很多项目并非非此即彼。我在当前项目中这样组合使用:
通过共享DbContext的连接对象实现无缝集成:
csharp复制// 在EF Core中获取Dapper连接
var conn = context.Database.GetDbConnection();
var rawData = conn.Query<ReportItem>("...");
这种混合模式需要特别注意:
Dapper的简洁性也意味着需要更多手动监控。我推荐这些工具:
安装MiniProfiler只需几行代码:
csharp复制services.AddMiniProfiler(options => {
options.RouteBasePath = "/profiler";
options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter();
}).AddEntityFramework();
在开发环境配置连接字符串时加上这些参数可以获取更多信息:
code复制Application Name=MyApp;Workstation ID=MyDevPC;...
最近处理过一个性能问题:某查询在测试环境很快但生产环境很慢。最终发现是Dapper默认不启用MARS(Multiple Active Result Sets),在需要同时处理多个结果集时需要显式开启:
code复制Server=...;MultipleActiveResultSets=True;