如何使用Dapper结合Change Tracking保存已变更的对象列表?
Dapper实现列表变更全量同步的实操方案
Dapper从设计之初就是走极简路线的轻量数据库访问工具,完全不会内置类似ADO.NET DataAdapter、EF Core那套全量变更跟踪、自动同步的能力,你之前想到的按操作分类维护变更列表的思路是完全可行的,通过少量通用封装,就能实现和传统DataAdapter完全一致的功能,而且可控性、执行性能都更好,具体实现如下:
1. 无需手动维护三类列表的状态识别方案
不用单独存新增、修改、删除三个独立的列表,给实体加个简单的状态标记就能统一管理所有变更:
- 先定义一套通用的实体状态枚举,让所有需要同步到数据库的业务实体继承带状态的基类:
// 实体变更状态 public enum EntityChangeState { 未变更, 新增, 修改, 删除 } // 带变更跟踪能力的实体基类 public abstract class TrackedEntityBase { public EntityChangeState ChangeState { get; set; } = EntityChangeState.未变更; }
- 业务实体
Customer只需要继承这个基类就行。 - 针对DataGridView绑定的场景,基于内置的
BindingList<T>派生一个自动更新状态的列表作为数据源,不需要手动写一堆事件订阅:- 用户在界面新增行时,自动把对应实体的状态标记为新增
- 用户修改单元格值时,自动把当前实体的状态从未变更更新为修改——注意不要覆盖已经是新增、删除的实体状态
- 用户删除行时,不要直接把实体从绑定列表删掉,直接把对应实体的状态标记为删除就行,这就从根本上解决了“删除的行从列表消失、丢失生成DELETE语句需要的主键信息”的问题,绑定展示时只要过滤掉状态为删除的条目,完全不影响用户的操作体验。
2. 事务一致性与主键回填的实现
所有同步操作必须挂在同一个数据库事务下执行,保证任意一条SQL执行失败时全量回滚,同时支持把新增行生成的主键自动回填到实体,同步刷新到DataGridView,核心实现代码如下:
using var connection = new SqlConnection("你的数据库连接串"); connection.Open(); // 开启事务,所有增删改操作共用同一个事务上下文 using var transaction = connection.BeginTransaction(); try { // 先筛出所有标记为删除的实体,批量执行删除 var deleteList = customerList.Where(c => c.ChangeState == EntityChangeState.删除).ToList(); if (deleteList.Any()) { // Dapper支持直接传入实体列表批量执行同一条SQL await transaction.ExecuteAsync( "DELETE FROM Customer WHERE Id = @Id", deleteList); } // 再筛出所有标记为修改的实体,批量执行更新 var updateList = customerList.Where(c => c.ChangeState == EntityChangeState.修改).ToList(); if (updateList.Any()) { await transaction.ExecuteAsync( @"UPDATE Customer SET CustomerName = @CustomerName, Phone = @Phone, Address = @Address WHERE Id = @Id", updateList); } // 最后筛出所有标记为新增的实体,执行插入并回填主键 var insertList = customerList.Where(c => c.ChangeState == EntityChangeState.新增).ToList(); if (insertList.Any()) { foreach (var customer in insertList) { // Dapper可以直接返回插入生成的自增主键,赋值回实体 customer.Id = await transaction.ExecuteScalarAsync<int>( @"INSERT INTO Customer (CustomerName, Phone, Address) VALUES (@CustomerName, @Phone, @Address); SELECT CAST(SCOPE_IDENTITY() AS INT)", customer); } } // 所有操作无异常,提交事务 transaction.Commit(); // 提交完成后重置状态:把非删除的实体状态改回未变更,把标记为删除的实体从列表移除 foreach (var customer in customerList.Where(c => c.ChangeState != EntityChangeState.删除)) { customer.ChangeState = EntityChangeState.未变更; } customerList.RemoveAll(c => c.ChangeState == EntityChangeState.删除); } catch { // 任意一步执行失败,回滚所有变更 transaction.Rollback(); throw; }
3. 进一步精简的优化方向
- 如果觉得手写单表的基础CRUD SQL太繁琐,可以搭配Dapper生态的轻量扩展(比如Dapper.Contrib),直接调用通用的
InsertAsync、UpdateAsync、DeleteAsync方法,不需要为每个单表写重复的基础SQL,性能和原生Dapper完全一致,没有额外的框架开销。 - 大批量插入的场景,可以配合数据库的
OUTPUT子句,一次性读取所有插入行生成的主键,不需要循环逐条执行插入,进一步提升执行性能。
内容的提问来源于stack exchange,提问作者GuidoG
相关产品推荐
相关产品推荐

