Dapper 性能优化
大约 10 分钟约 2986 字
Dapper 性能优化
简介
Dapper 是 .NET 平台轻量级的 Micro-ORM,由 Stack Overflow 团队开发并开源。它以接近原生 ADO.NET 的性能和简洁的 API 设计著称,通过扩展 IDbConnection 接口提供强大的对象映射功能。本文将深入介绍 Dapper 的多映射、多结果集、存储过程、批量操作和连接管理等高级用法和性能优化技巧。
特点
多映射查询
一对一与一对多映射
// 1. 一对一映射(CustomMapping)
// 订单关联用户
var sql = @"
SELECT o.Id, o.OrderNo, o.TotalAmount, o.Status,
u.Id, u.Username, u.Email
FROM Orders o
INNER JOIN Users u ON o.UserId = u.Id
WHERE o.Status = @Status";
var orders = connection.Query<Order, User, Order>(
sql,
(order, user) =>
{
order.User = user;
return order;
},
new { Status = "paid" },
splitOn: "Id" // 指定从哪个列开始是 User 的字段
).ToList();
// 2. 一对多映射(使用字典去重)
var sql = @"
SELECT o.Id, o.OrderNo, o.TotalAmount,
oi.Id, oi.ProductName, oi.Quantity, oi.Price
FROM Orders o
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
WHERE o.UserId = @UserId";
var orderDict = new Dictionary<int, Order>();
var orders = connection.Query<Order, OrderItem, Order>(
sql,
(order, item) =>
{
if (!orderDict.TryGetValue(order.Id, out var existingOrder))
{
existingOrder = order;
existingOrder.OrderItems = new List<OrderItem>();
orderDict.Add(order.Id, existingOrder);
}
if (item != null)
{
existingOrder.OrderItems.Add(item);
}
return existingOrder;
},
new { UserId = userId },
splitOn: "Id"
).Distinct().ToList();
// 3. 多实体映射(三个以上实体)
var sql = @"
SELECT o.Id, o.OrderNo,
u.Id, u.Username,
oi.Id, oi.ProductName, oi.Quantity
FROM Orders o
INNER JOIN Users u ON o.UserId = u.Id
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
WHERE o.Id = @OrderId";
var result = connection.Query<Order, User, OrderItem, Order>(
sql,
(order, user, item) =>
{
order.User = user;
if (item != null)
{
order.OrderItems = order.OrderItems ?? new List<OrderItem>();
order.OrderItems.Add(item);
}
return order;
},
new { OrderId = orderId },
splitOn: "Id,Id"
).FirstOrDefault();
// 4. 自定义类型映射
// 使用 CustomPropertyTypeMap 指定列名到属性的映射
Dapper.SqlMapper.SetTypeMap(typeof(OrderItem),
new CustomPropertyTypeMap(
typeof(OrderItem),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
string.Equals(prop.Name, columnName,
StringComparison.OrdinalIgnoreCase))));
// 5. 列名转换为下划线命名
// 数据库: order_no, total_amount -> C#: OrderNo, TotalAmount
/*
public class UnderscoreNameMapper : FallbackTypeMapper
{
// 使用 Dapper.FluentMap 或手动配置列名映射
}
*/多结果集查询
GridReader 使用
// QueryMultiple 一次查询返回多个结果集
// 减少数据库往返次数
// 1. 基本多结果集查询
var sql = @"
SELECT * FROM Orders WHERE Id = @OrderId;
SELECT * FROM OrderItems WHERE OrderId = @OrderId;
SELECT * FROM Payments WHERE OrderId = @OrderId;
SELECT COUNT(*) FROM Orders WHERE UserId = @UserId;";
using var multi = connection.QueryMultiple(sql,
new { OrderId = orderId, UserId = userId });
var order = multi.ReadFirstOrDefault<Order>();
var items = multi.Read<OrderItem>().ToList();
var payments = multi.Read<Payment>().ToList();
var totalOrders = multi.ReadFirst<int>();
// 2. 订单详情页数据获取
var sql = @"
-- 订单基本信息
SELECT o.*, u.Username, u.Email
FROM Orders o
INNER JOIN Users u ON o.UserId = u.Id
WHERE o.Id = @OrderId;
-- 订单项列表
SELECT oi.*, p.Name AS ProductName, p.ImageUrl
FROM OrderItems oi
INNER JOIN Products p ON oi.ProductId = p.Id
WHERE oi.OrderId = @OrderId;
-- 物流信息
SELECT TOP 1 * FROM Shipments
WHERE OrderId = @OrderId
ORDER BY CreatedAt DESC;
-- 操作日志
SELECT * FROM OrderLogs
WHERE OrderId = @OrderId
ORDER BY CreatedAt DESC;";
using var multi = connection.QueryMultiple(sql, new { OrderId = orderId });
var orderInfo = multi.ReadFirstOrDefault<OrderDetailDto>();
var orderItems = multi.Read<OrderItemDto>().ToList();
var shipment = multi.ReadFirstOrDefault<Shipment>();
var logs = multi.Read<OrderLog>().ToList();
// 3. 分页数据 + 总数(一次查询解决)
var sql = @"
SELECT * FROM Orders
WHERE Status = @Status AND CreatedAt >= @Since
ORDER BY CreatedAt DESC
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY;
SELECT COUNT(*) FROM Orders
WHERE Status = @Status AND CreatedAt >= @Since;";
using var multi = connection.QueryMultiple(sql, new
{
Status = "paid",
Since = startDate,
Offset = (page - 1) * pageSize,
PageSize = pageSize
});
var orders = multi.Read<Order>().ToList();
var totalCount = multi.ReadFirst<int>();
// 4. 存储过程返回多个结果集
using var multi = connection.QueryMultiple("sp_GetDashboardData",
new { UserId = userId, Month = "2026-04" },
commandType: CommandType.StoredProcedure);
var recentOrders = multi.Read<Order>().ToList();
var topProducts = multi.Read<Product>().ToList();
var monthlyStats = multi.Read<MonthlyStat>().FirstOrDefault();存储过程调用
存储过程操作
// 1. 简单存储过程调用
var users = connection.Query<User>("sp_GetActiveUsers",
commandType: CommandType.StoredProcedure).ToList();
// 2. 带参数的存储过程
var orders = connection.Query<Order>("sp_GetOrdersByDateRange",
new
{
StartDate = new DateTime(2026, 1, 1),
EndDate = new DateTime(2026, 4, 30),
Status = "paid"
},
commandType: CommandType.StoredProcedure).ToList();
// 3. 带输出参数的存储过程
var parameters = new DynamicParameters();
parameters.Add("@UserId", userId, dbType: DbType.Int32, direction: ParameterDirection.Input);
parameters.Add("@TotalOrders", dbType: DbType.Int32, direction: ParameterDirection.Output);
parameters.Add("@TotalAmount", dbType: DbType.Decimal, direction: ParameterDirection.Output);
parameters.Add("@ReturnCode", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);
connection.Execute("sp_GetUserOrderSummary", parameters,
commandType: CommandType.StoredProcedure);
var totalOrders = parameters.Get<int>("@TotalOrders");
var totalAmount = parameters.Get<decimal>("@TotalAmount");
var returnCode = parameters.Get<int>("@ReturnCode");
// 4. 带 Table-Valued 参数的存储过程(SQL Server)
// 创建 DataTable 作为 TVP
var dataTable = new DataTable();
dataTable.Columns.Add("ProductId", typeof(int));
dataTable.Columns.Add("Quantity", typeof(int));
dataTable.Columns.Add("Price", typeof(decimal));
foreach (var item in orderItems)
{
dataTable.Rows.Add(item.ProductId, item.Quantity, item.Price);
}
var parameters = new DynamicParameters();
parameters.Add("@OrderId", orderId);
parameters.Add("@Items", dataTable.AsTableValuedParameter("dbo.OrderItemType"));
connection.Execute("sp_CreateOrderItems", parameters,
commandType: CommandType.StoredProcedure);
// 5. 事务中执行存储过程
using var transaction = connection.BeginTransaction();
try
{
connection.Execute("sp_UpdateInventory", new { OrderId = orderId },
transaction, commandType: CommandType.StoredProcedure);
connection.Execute("sp_UpdateOrderStatus", new { OrderId = orderId, Status = "completed" },
transaction, commandType: CommandType.StoredProcedure);
connection.Execute("sp_SendNotification", new { OrderId = orderId },
transaction, commandType: CommandType.StoredProcedure);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}批量操作优化
高性能批量处理
// 1. 批量插入(使用 UNNEST / VALUES 列表)
// 方式一:多次 Execute(简单但慢)
foreach (var item in items)
{
connection.Execute(@"
INSERT INTO OrderItems (OrderId, ProductId, Quantity, Price)
VALUES (@OrderId, @ProductId, @Quantity, @Price)", item);
}
// 方式二:传入集合,Dapper 自动展开(推荐)
connection.Execute(@"
INSERT INTO OrderItems (OrderId, ProductId, Quantity, Price)
VALUES (@OrderId, @ProductId, @Quantity, @Price)", items);
// Dapper 内部会循环执行,但复用同一个 prepared statement
// 方式三:使用 BulkCopy(SQL Server,最快)
using var bulkCopy = new SqlBulkCopy((SqlConnection)connection);
bulkCopy.DestinationTableName = "OrderItems";
bulkCopy.ColumnMappings.Add("OrderId", "OrderId");
bulkCopy.ColumnMappings.Add("ProductId", "ProductId");
bulkCopy.ColumnMappings.Add("Quantity", "Quantity");
bulkCopy.ColumnMappings.Add("Price", "Price");
bulkCopy.BatchSize = 5000;
bulkCopy.BulkCopyTimeout = 60;
var dataTable = new DataTable();
dataTable.Columns.Add("OrderId", typeof(int));
dataTable.Columns.Add("ProductId", typeof(int));
dataTable.Columns.Add("Quantity", typeof(int));
dataTable.Columns.Add("Price", typeof(decimal));
foreach (var item in items)
{
dataTable.Rows.Add(item.OrderId, item.ProductId, item.Quantity, item.Price);
}
await bulkCopy.WriteToServerAsync(dataTable);
// 2. 批量更新
// 方式一:使用 CASE WHEN
var updates = new List<Product> { /* ... */ };
var sql = @"
UPDATE Products
SET Price = CASE Id
" + string.Join("\n", updates.Select((p, i) =>
$"WHEN @Id{i} THEN @Price{i}")) + @"
END,
Stock = CASE Id
" + string.Join("\n", updates.Select((p, i) =>
$"WHEN @Id{i} THEN @Stock{i}")) + @"
END
WHERE Id IN (" + string.Join(",", updates.Select((p, i) => $"@Id{i}")) + ")";
var parameters = new DynamicParameters();
for (var i = 0; i < updates.Count; i++)
{
parameters.Add($"@Id{i}", updates[i].Id);
parameters.Add($"@Price{i}", updates[i].Price);
parameters.Add($"@Stock{i}", updates[i].Stock);
}
connection.Execute(sql, parameters);
// 3. 批量删除
connection.Execute(@"
DELETE FROM OrderItems
WHERE OrderId IN @OrderIds",
new { OrderIds = orderIds.ToArray() });
// Dapper 将 IN @OrderIds 展开为 IN (@OrderIds1, @OrderIds2, ...)
// 4. 使用临时表进行批量操作(SQL Server)
// 先将批量数据写入临时表,再通过 JOIN 操作
var sql = @"
-- 创建临时表
CREATE TABLE #TempPrices (ProductId INT, NewPrice DECIMAL(10,2));
-- 批量插入临时表(使用 BulkCopy 或多条 INSERT)
INSERT INTO #TempPrices VALUES (@ProductId, @NewPrice);
-- 通过临时表批量更新
UPDATE p
SET p.Price = t.NewPrice
FROM Products p
INNER JOIN #TempPrices t ON p.Id = t.ProductId;
DROP TABLE #TempPrices;";
connection.Execute(sql, priceUpdates);
// 性能对比(插入 10000 条记录)
// | 方式 | 耗时 | 说明 |
// |------------------|------------|-------------------|
// | 逐条 Execute | ~30s | 最慢 |
// | 集合 Execute | ~10s | Dapper 自动展开 |
// | VALUES 列表拼接 | ~2s | 构建一条大 SQL |
// | SqlBulkCopy | ~0.3s | 最快(SQL Server) |连接管理
连接管理最佳实践
// 1. 使用 using 确保连接释放
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var orders = await connection.QueryAsync<Order>(
"SELECT * FROM Orders WHERE UserId = @UserId",
new { UserId = userId });
// 2. 连接池配置
// 在连接字符串中配置连接池参数
/*
var connectionString = "Server=.;Database=MyApp;Trusted_Connection=True;" +
"Min Pool Size=5;" + // 最小连接数
"Max Pool Size=100;" + // 最大连接数
"Connection Timeout=30;" + // 连接超时(秒)
"Connection Lifetime=300;"; // 连接最大生存时间(秒)
*/
// 3. 封装数据库访问层
/*
public class DapperRepository
{
private readonly string _connectionString;
public DapperRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
// 使用 Func 委托封装连接管理
private async Task<T> WithConnection<T>(Func<IDbConnection, Task<T>> func)
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
return await func(connection);
}
private async Task WithConnection(Func<IDbConnection, Task> func)
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await func(connection);
}
// 使用示例
public Task<Order?> GetOrderAsync(int id)
{
return WithConnection(conn =>
conn.QueryFirstOrDefaultAsync<Order>(
"SELECT * FROM Orders WHERE Id = @Id",
new { Id = id }));
}
public Task<IEnumerable<Order>> GetOrdersByUserAsync(int userId)
{
return WithConnection(conn =>
conn.QueryAsync<Order>(
"SELECT * FROM Orders WHERE UserId = @UserId ORDER BY CreatedAt DESC",
new { UserId = userId }));
}
public Task<int> CreateOrderAsync(Order order)
{
return WithConnection(conn =>
conn.ExecuteAsync(@"
INSERT INTO Orders (OrderNo, UserId, TotalAmount, Status)
VALUES (@OrderNo, @UserId, @TotalAmount, @Status)",
order));
}
}
*/
// 4. 事务管理
/*
public async Task TransferOrderAsync(int fromUserId, int toUserId, int orderId)
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var transaction = connection.BeginTransaction();
try
{
// 取消原用户的订单关联
await connection.ExecuteAsync(@"
UPDATE Orders SET UserId = @ToUserId WHERE Id = @OrderId",
new { ToUserId = toUserId, OrderId = orderId },
transaction);
// 记录转移日志
await connection.ExecuteAsync(@"
INSERT INTO TransferLogs (OrderId, FromUserId, ToUserId)
VALUES (@OrderId, @FromUserId, @ToUserId)",
new { OrderId = orderId, FromUserId = fromUserId, ToUserId = toUserId },
transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
*/
// 5. 使用 UnitOfWork 模式管理事务
/*
public class UnitOfWork : IUnitOfWork
{
private readonly IDbConnection _connection;
private IDbTransaction? _transaction;
public UnitOfWork(string connectionString)
{
_connection = new SqlConnection(connectionString);
_connection.Open();
_transaction = _connection.BeginTransaction();
}
public IDbConnection Connection => _connection;
public IDbTransaction Transaction => _transaction!;
public void Commit() => _transaction?.Commit();
public void Rollback() => _transaction?.Rollback();
public void Dispose()
{
_transaction?.Dispose();
_connection?.Dispose();
}
}
*/优点
缺点
总结
Dapper 以其极致的性能和简洁的设计成为 .NET 数据访问层的重要选择。通过多映射查询可以灵活处理复杂的关联关系,QueryMultiple 减少了数据库的往返次数,存储过程调用支持企业级数据库开发,批量操作优化可以高效处理大数据量场景。在需要高性能数据访问的项目中,Dapper 是 EF Core 的有力补充。
关键知识点
- 数据库主题一定要同时看数据模型、读写模式和执行代价。
- 很多性能问题不是 SQL 语法问题,而是索引、统计信息、事务和数据分布问题。
- 高可用、备份、迁移和治理与查询优化同样重要。
- 先把数据模型、访问模式和执行代价绑定起来理解。
项目落地视角
- 所有优化前后都保留执行计划、样本 SQL 和关键指标对比。
- 上线前准备回滚脚本、备份点和校验方案。
- 把连接池、锁等待、慢查询和容量增长纳入日常巡检。
- 保留执行计划、样本 SQL、索引定义和优化前后指标。
常见误区
- 脱离真实数据分布讨论索引或分片。
- 只看单条 SQL,不看整条业务链路的事务和锁。
- 把测试环境结论直接等同于生产环境结论。
- 脱离真实数据分布设计索引。
进阶路线
- 继续向执行计划、存储引擎、复制机制和数据治理层深入。
- 把主题与 ORM、缓存、消息队列和归档策略联动起来思考。
- 沉淀成数据库设计规范、SQL 审核规则和变更流程。
- 继续深入存储引擎、复制机制、归档与冷热分层治理。
适用场景
- 当你准备把《Dapper 性能优化》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合数据建模、查询优化、事务控制、高可用和迁移治理。
- 当系统开始遇到慢查询、锁冲突、热点数据或容量增长时,这类主题价值最高。
落地建议
- 先分析真实查询模式、数据量级和写入特征,再决定索引或分片策略。
- 所有优化结论都结合执行计划、样本数据和监控指标验证。
- 高风险操作前准备备份、回滚脚本与校验 SQL。
排错清单
- 先确认瓶颈在 CPU、I/O、锁等待、网络还是 SQL 本身。
- 检查执行计划是否走错索引、是否发生排序或全表扫描。
- 排查长事务、隐式类型转换、统计信息过期和参数嗅探。
复盘问题
- 如果把《Dapper 性能优化》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Dapper 性能优化》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Dapper 性能优化》最大的收益和代价分别是什么?
