You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.24 12:15:02