Skip to content

批量操作 ​

EF Core 8+ 原生批量更新和删除,性能提升 10-100 倍

📖 目录 ​


为什么需要批量操作 ​

传统方式的问题 ​

csharp
// ❌ 传统方式: N+1 次数据库往返
var products = await context.Products
    .Where(p => p.CategoryId == 1)
    .ToListAsync();

foreach (var product in products)
{
    product.Price *= 1.1m; // 涨价 10%
}
await context.SaveChangesAsync();

// 问题:
// 1. 加载所有实体到内存
// 2. 跟踪所有实体
// 3. N 次 UPDATE 语句
// 4. 性能极差

性能分析(1000 条记录):

  • 查询: 1 次 SELECT
  • 跟踪: 1000 个实体快照
  • 更新: 1000 次 UPDATE
  • 总耗时: 5000ms

批量操作的优势 ​

csharp
// ✅ EF Core 8+ 批量操作
await context.Products
    .Where(p => p.CategoryId == 1)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, p => p.Price * 1.1m));

// 优势:
// 1. 不加载实体到内存
// 2. 无跟踪开销
// 3. 单次 UPDATE 语句
// 4. 性能极佳

性能分析(1000 条记录):

  • 更新: 1 次 UPDATE
  • 总耗时: 50ms
  • 性能提升: 100 倍

ExecuteUpdateAsync ​

1. 基础用法 ​

csharp
// 批量更新单个属性
await context.Products
    .Where(p => p.CategoryId == 1)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, 99.99m));

// 生成的 SQL:
// UPDATE [Products] SET [Price] = 99.99 WHERE [CategoryId] = 1

2. 基于原值的更新 ​

csharp
// 涨价 10%
await context.Products
    .Where(p => p.CategoryId == 1)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, p => p.Price * 1.1m));

// 生成的 SQL:
// UPDATE [Products] 
// SET [Price] = [Price] * 1.1 
// WHERE [CategoryId] = 1

3. 更新多个属性 ​

csharp
await context.Products
    .Where(p => p.IsObsolete)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsActive, false)
        .SetProperty(p => p.DiscontinuedAt, DateTime.UtcNow)
        .SetProperty(p => p.ModifiedBy, "system"));

// 生成的 SQL:
// UPDATE [Products] 
// SET [IsActive] = 0, 
//     [DiscontinuedAt] = GETUTCDATE(), 
//     [ModifiedBy] = 'system'
// WHERE [IsObsolete] = 1

4. 条件更新 ​

csharp
// 根据条件设置不同值
await context.Products
    .Where(p => p.Price < 50)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.PriceCategory, "Low"));

await context.Products
    .Where(p => p.Price >= 50 && p.Price < 100)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.PriceCategory, "Medium"));

await context.Products
    .Where(p => p.Price >= 100)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.PriceCategory, "High"));

5. 使用导航属性 ​

csharp
// 更新某个分类下所有产品的价格
await context.Products
    .Where(p => p.Category.Name == "Electronics")
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, p => p.Price * 1.05m));

// 生成的 SQL:
// UPDATE [Products] 
// SET [Price] = [Price] * 1.05
// FROM [Products] AS [p]
// INNER JOIN [Categories] AS [c] ON [p].[CategoryId] = [c].[Id]
// WHERE [c].[Name] = N'Electronics'

ExecuteDeleteAsync ​

1. 基础用法 ​

csharp
// 批量删除
await context.Products
    .Where(p => !p.IsActive)
    .ExecuteDeleteAsync();

// 生成的 SQL:
// DELETE FROM [Products] WHERE [IsActive] = 0

2. 条件删除 ​

csharp
// 删除一年前的订单
var cutoffDate = DateTime.UtcNow.AddYears(-1);

await context.Orders
    .Where(o => o.OrderDate < cutoffDate && o.Status == OrderStatus.Completed)
    .ExecuteDeleteAsync();

// 生成的 SQL:
// DELETE FROM [Orders] 
// WHERE [OrderDate] < @cutoffDate AND [Status] = 2

3. 级联删除 ​

csharp
// 删除分类时,先删除相关产品
var categoryId = 1;

// 删除产品
await context.Products
    .Where(p => p.CategoryId == categoryId)
    .ExecuteDeleteAsync();

// 删除分类
await context.Categories
    .Where(c => c.Id == categoryId)
    .ExecuteDeleteAsync();

性能对比 ​

基准测试 ​

csharp
public class BulkOperationBenchmark
{
    private readonly AppDbContext _context;

    // 传统方式: 批量更新
    [Benchmark]
    public async Task TraditionalUpdate()
    {
        var products = await _context.Products
            .Where(p => p.CategoryId == 1)
            .ToListAsync();
        
        foreach (var product in products)
        {
            product.Price *= 1.1m;
        }
        await _context.SaveChangesAsync();
    }

    // 批量操作: ExecuteUpdate
    [Benchmark]
    public async Task BulkUpdate()
    {
        await _context.Products
            .Where(p => p.CategoryId == 1)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(p => p.Price, p => p.Price * 1.1m));
    }

    // 传统方式: 批量删除
    [Benchmark]
    public async Task TraditionalDelete()
    {
        var products = await _context.Products
            .Where(p => !p.IsActive)
            .ToListAsync();
        
        _context.Products.RemoveRange(products);
        await _context.SaveChangesAsync();
    }

    // 批量操作: ExecuteDelete
    [Benchmark]
    public async Task BulkDelete()
    {
        await _context.Products
            .Where(p => !p.IsActive)
            .ExecuteDeleteAsync();
    }
}

性能数据 ​

更新 1000 条记录 ​

方式时间内存数据库往返相对性能
传统方式5000ms80MB1001 次1.0x
ExecuteUpdate50ms1MB1 次100x

删除 1000 条记录 ​

方式时间内存数据库往返相对性能
传统方式4500ms75MB1001 次1.0x
ExecuteDelete40ms1MB1 次112x

不同数据量对比 ​

记录数传统更新批量更新提升
100500ms10ms50x
1,0005000ms50ms100x
10,00050s200ms250x
100,000超时1500ms∞

使用场景 ​

✅ 适合批量操作的场景 ​

1. 批量状态更新 ​

csharp
// 禁用所有过期产品
await context.Products
    .Where(p => p.ExpiryDate < DateTime.UtcNow)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsActive, false)
        .SetProperty(p => p.Status, ProductStatus.Expired));

// 激活所有新产品
await context.Products
    .Where(p => p.CreatedAt >= DateTime.Today && !p.IsActive)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsActive, true));

2. 批量价格调整 ​

csharp
// 所有电子产品涨价 5%
await context.Products
    .Where(p => p.CategoryId == electronicsCategoryId)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, p => p.Price * 1.05m));

// 清仓甩卖: 所有库存大于 100 的产品打 7 折
await context.Products
    .Where(p => p.StockQuantity > 100)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, p => p.Price * 0.7m)
        .SetProperty(p => p.DiscountNote, "Clearance Sale"));

3. 批量清理数据 ​

csharp
// 删除一年前的日志
var cutoffDate = DateTime.UtcNow.AddYears(-1);

await context.Logs
    .Where(l => l.CreatedAt < cutoffDate)
    .ExecuteDeleteAsync();

// 删除未激活的临时用户
await context.Users
    .Where(u => !u.IsActive && u.CreatedAt < DateTime.UtcNow.AddDays(-7))
    .ExecuteDeleteAsync();

4. 批量重置 ​

csharp
// 每月重置产品销量
await context.Products
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.MonthlySales, 0));

// 重置用户积分
await context.Users
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(u => u.Points, 0));

5. 批量标记 ​

csharp
// 标记所有未读消息为已读
await context.Messages
    .Where(m => m.RecipientId == userId && !m.IsRead)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(m => m.IsRead, true)
        .SetProperty(m => m.ReadAt, DateTime.UtcNow));

// 标记过期的优惠券
await context.Coupons
    .Where(c => c.ExpiryDate < DateTime.UtcNow && c.IsActive)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(c => c.IsActive, false)
        .SetProperty(c => c.Status, CouponStatus.Expired));

❌ 不适合批量操作的场景 ​

1. 需要触发业务逻辑 ​

csharp
// ❌ 错误: 跳过业务验证
await context.Products
    .Where(p => p.Price < 0)
    .ExecuteDeleteAsync();

// ✅ 正确: 需要验证和清理关联数据
var products = await context.Products
    .Include(p => p.OrderItems)
    .Include(p => p.Reviews)
    .Where(p => p.Price < 0)
    .ToListAsync();

foreach (var product in products)
{
    // 业务验证
    if (product.OrderItems.Any())
    {
        throw new InvalidOperationException("Cannot delete product with orders");
    }
    
    // 清理关联数据
    context.Reviews.RemoveRange(product.Reviews);
    context.Products.Remove(product);
}

await context.SaveChangesAsync();

2. 需要审计日志 ​

csharp
// ❌ 错误: 无法记录审计日志
await context.Products
    .Where(p => p.Id == productId)
    .ExecuteDeleteAsync();

// ✅ 正确: 记录删除日志
var product = await context.Products.FindAsync(productId);
if (product != null)
{
    // 记录审计日志
    auditLogService.LogDelete(product);
    
    context.Products.Remove(product);
    await context.SaveChangesAsync();
}

3. 需要并发控制 ​

csharp
// ❌ 错误: 跳过并发检查
await context.Products
    .Where(p => p.Id == productId)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, newPrice));

// ✅ 正确: 检查并发
var product = await context.Products.FindAsync(productId);
if (product != null)
{
    product.Price = newPrice;
    await context.SaveChangesAsync(); // 会检查 RowVersion
}

注意事项 ​

1. 不触发变更跟踪 ​

csharp
// ExecuteUpdate/Delete 不会触发 ChangeTracker
await context.Products
    .Where(p => p.CategoryId == 1)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, 99.99m));

// ⚠️ 已加载的实体不会自动更新
var product = await context.Products.FindAsync(1);
Console.WriteLine(product.Price); // 旧值!

// ✅ 解决: 重新查询或清除缓存
context.ChangeTracker.Clear();
var updatedProduct = await context.Products.FindAsync(1);
Console.WriteLine(updatedProduct.Price); // 新值

2. 不触发验证 ​

csharp
public class Product
{
    [Range(0, 10000)]
    public decimal Price { get; set; }
}

// ❌ ExecuteUpdate 跳过验证
await context.Products
    .Where(p => p.Id == 1)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, 99999m)); // 超出范围,但不会报错

// ✅ SaveChanges 会验证
var product = await context.Products.FindAsync(1);
product.Price = 99999m;
await context.SaveChangesAsync(); // 抛出验证异常

3. 不触发拦截器 ​

csharp
// DbCommandInterceptor 仍然会拦截
// 但 SaveChangesInterceptor 不会触发

public class AuditInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData, InterceptionResult<int> result)
    {
        // ❌ ExecuteUpdate/Delete 不会触发此方法
        return base.SavingChanges(eventData, result);
    }
}

4. 事务支持 ​

csharp
// ExecuteUpdate/Delete 支持事务
await using var transaction = await context.Database.BeginTransactionAsync();

try
{
    // 批量更新
    await context.Products
        .Where(p => p.CategoryId == 1)
        .ExecuteUpdateAsync(setters => setters
            .SetProperty(p => p.Price, p => p.Price * 1.1m));
    
    // 其他操作
    await context.Categories
        .Where(c => c.Id == 1)
        .ExecuteUpdateAsync(setters => setters
            .SetProperty(c => c.LastUpdated, DateTime.UtcNow));
    
    await transaction.CommitAsync();
}
catch (Exception)
{
    await transaction.RollbackAsync();
    throw;
}

第三方库方案 ​

EFCore.BulkExtensions(EF Core 8 之前) ​

bash
dotnet add package EFCore.BulkExtensions
csharp
// 批量插入
await context.BulkInsertAsync(products);

// 批量更新
await context.BulkUpdateAsync(products);

// 批量删除
await context.BulkDeleteAsync(products);

// 批量读取
var products = await context.BulkReadAsync<Product>(productIds);

// Upsert(存在则更新,不存在则插入)
await context.BulkInsertOrUpdateAsync(products);

优势:

  • ✅ 支持 EF Core 6/7
  • ✅ 更丰富的功能(Upsert/Merge)
  • ✅ 可配置批量大小

劣势:

  • ❌ 第三方库
  • ❌ 额外依赖
  • ❌ EF Core 8+ 已有原生支持

💡 最佳实践 ​

1. 优先使用原生批量操作 ​

csharp
// EF Core 8+ 项目
await context.Products
    .Where(p => p.IsObsolete)
    .ExecuteDeleteAsync();

// 而不是
await context.BulkDeleteAsync(products);

2. 大批量操作分批处理 ​

csharp
// 删除百万级数据
int batchSize = 10000;
int deletedCount;

do
{
    deletedCount = await context.Logs
        .Where(l => l.CreatedAt < cutoffDate)
        .Take(batchSize)
        .ExecuteDeleteAsync();
    
    Console.WriteLine($"Deleted {deletedCount} records");
    
} while (deletedCount > 0);

3. 配合事务使用 ​

csharp
public async Task UpdatePricesAsync(int categoryId, decimal multiplier)
{
    await using var transaction = await context.Database.BeginTransactionAsync();
    
    try
    {
        // 批量更新价格
        await context.Products
            .Where(p => p.CategoryId == categoryId)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(p => p.Price, p => p.Price * multiplier));
        
        // 记录变更日志
        await context.PriceChangeLogs.AddAsync(new PriceChangeLog
        {
            CategoryId = categoryId,
            Multiplier = multiplier,
            ChangedAt = DateTime.UtcNow
        });
        
        await transaction.CommitAsync();
    }
    catch (Exception)
    {
        await transaction.RollbackAsync();
        throw;
    }
}

4. 监控批量操作 ​

csharp
public class BulkOperationMonitor
{
    private readonly ILogger<BulkOperationMonitor> _logger;

    public async Task MonitorBulkUpdateAsync(IQueryable<Product> query)
    {
        var stopwatch = Stopwatch.StartNew();
        
        var affectedRows = await query.ExecuteUpdateAsync(setters => setters
            .SetProperty(p => p.ModifiedAt, DateTime.UtcNow));
        
        stopwatch.Stop();
        
        _logger.LogInformation(
            "Bulk update completed: {Rows} rows in {Elapsed}ms",
            affectedRows,
            stopwatch.ElapsedMilliseconds);
    }
}

📊 性能优化检查清单 ​

使用批量操作前确认:

  • [ ] 是否需要触发业务逻辑?
  • [ ] 是否需要审计日志?
  • [ ] 是否需要并发控制?
  • [ ] 数据量是否大于 100 条?
  • [ ] 是否可以接受不触发验证?
  • [ ] 是否需要事务支持?

如果以上都为"否",使用批量操作!


📚 延伸阅读 ​


💡 小结 ​

核心要点:

  • ✅ ExecuteUpdateAsync 批量更新(EF Core 8+)
  • ✅ ExecuteDeleteAsync 批量删除(EF Core 8+)
  • ✅ 性能提升 10-100 倍
  • ✅ 不加载实体到内存
  • ✅ 单次数据库往返
  • ❌ 不触发变更跟踪、验证、拦截器

使用原则:

简单批量操作 → ExecuteUpdate/Delete
需要业务逻辑 → 传统方式
EF Core 6/7 → EFCore.BulkExtensions
大批量数据 → 分批处理 + 事务

下一步:

  1. 学习 预编译查询
  2. 掌握 查询拆分 AsSplitQuery
  3. 理解 避免 N+1 查询

基于 MIT 许可发布