Skip to content

图形更新 Graph Updates ​

目录 ​


什么是图形更新 ​

概念理解 ​

图形更新(Graph Updates) 是指处理**断开连接的实体图(Disconnected Entity Graph)**的变更并保存到数据库的技术场景。

csharp
// 连接状态 vs 断开连接状态

// ✅ 连接状态: DbContext 跟踪实体
var product = await context.Products.FindAsync(1);
product.Price = 199.99;  // ChangeTracker 自动检测
await context.SaveChangesAsync();  // ✅ 自动更新

// ❌ 断开连接: 实体脱离 DbContext 跟踪
var product = await GetProductFromApiAsync();  // 从 API 接收
// product 不在任何 DbContext 的跟踪中

product.Price = 199.99;
await context.SaveChangesAsync();  // 💥 EF Core 不知道这个变更!

典型场景 ​

场景特点挑战
Web API请求间无状态无法保持跟踪
Blazor Server长生命周期内存泄漏风险
批量导入大量实体性能问题
微服务通信跨服务传递序列化/反序列化
离线应用本地缓存后同步冲突检测

核心问题 ​

断开连接的实体图面临的问题:

1. 状态丢失
   - EF Core 不知道实体是新增、修改还是删除
   
2. 导航属性处理
   - 关联实体也需要正确的状态管理
   
3. 并发控制
   - 如何检测其他人是否修改了数据
   
4. 性能优化
   - 避免 N+1 查询和多余的数据库往返

Disconnected 实体图 ​

实体图示例 ​

csharp
// 完整的订单实体图
public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public decimal TotalAmount { get; set; }
    
    public Customer Customer { get; set; }          // 引用导航
    public ICollection<OrderItem> Items { get; set; } // 集合导航
}

public class OrderItem
{
    public int Id { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    
    public Product Product { get; set; }
}

// 从 API 接收的断开连接的实体图
{
  "id": 101,
  "orderDate": "2024-01-15T08:30:00Z",
  "totalAmount": 299.99,
  "customer": { "id": 1, "name": "John" },
  "items": [
    { "id": 201, "productId": 10, "quantity": 2, "unitPrice": 99.99 },
    { "id": 202, "productId": 20, "quantity": 1, "unitPrice": 100.01 }
  ]
}

手动附加实体 ​

csharp
// 方法 1: Attach + 设置状态
public async Task UpdateOrderAsync(Order order)
{
    // 附加根实体
    context.Orders.Attach(order);
    
    // 标记为已修改
    context.Entry(order).State = EntityState.Modified;
    
    // 附加关联实体
    if (order.Customer != null)
    {
        context.Customers.Attach(order.Customer);
        context.Entry(order.Customer).State = EntityState.Unchanged;
    }
    
    // 附加集合中的实体
    foreach (var item in order.Items)
    {
        context.OrderItems.Attach(item);
        context.Entry(item).State = EntityState.Modified;
        
        if (item.Product != null)
        {
            context.Products.Attach(item.Product);
            context.Entry(item.Product).State = EntityState.Unchanged;
        }
    }
    
    await context.SaveChangesAsync();
}

// ⚠️ 问题: 繁琐且容易出错

更新策略 ​

策略 1: 查询后更新(推荐) ​

csharp
// ✅ 最安全: 先查询再更新
public async Task UpdateOrderAsync(int orderId, OrderDto dto)
{
    // 步骤 1: 从数据库加载完整实体图
    var order = await context.Orders
        .Include(o => o.Items)
        .FirstOrDefaultAsync(o => o.Id == orderId);
    
    if (order == null)
        throw new NotFoundException($"Order {orderId} not found");
    
    // 步骤 2: 应用变更
    order.TotalAmount = dto.TotalAmount;
    order.OrderDate = dto.OrderDate;
    
    // 步骤 3: 处理集合(删除、添加、更新)
    var existingItemIds = order.Items.Select(i => i.Id).ToHashSet();
    var newItemIds = dto.Items.Select(i => i.Id).ToHashSet();
    
    // 删除不在 DTO 中的项
    var itemsToRemove = order.Items
        .Where(i => !newItemIds.Contains(i.Id))
        .ToList();
    
    foreach (var item in itemsToRemove)
    {
        order.Items.Remove(item);
        context.OrderItems.Remove(item);
    }
    
    // 添加新项
    foreach (var itemDto in dto.Items.Where(i => i.Id == 0))
    {
        order.Items.Add(new OrderItem
        {
            ProductId = itemDto.ProductId,
            Quantity = itemDto.Quantity,
            UnitPrice = itemDto.UnitPrice
        });
    }
    
    // 更新现有项
    foreach (var itemDto in dto.Items.Where(i => i.Id > 0))
    {
        var item = order.Items.First(i => i.Id == itemDto.Id);
        item.Quantity = itemDto.Quantity;
        item.UnitPrice = itemDto.UnitPrice;
    }
    
    // 步骤 4: 保存
    await context.SaveChangesAsync();
}

// 优点: 
// - 安全,不会丢失并发检查
// - ChangeTracker 自动检测变更
// - 代码清晰易懂

策略 2: 使用 TrackGraph API ​

csharp
// ✅ 高级: TrackGraph 自动遍历实体图
public async Task UpdateOrderWithTrackGraphAsync(Order order)
{
    context.ChangeTracker.TrackGraph(order, node =>
    {
        var entry = node.Entry;
        
        // 根据实体状态判断
        if (entry.IsKeySet)
        {
            // 有 ID,认为是已存在实体
            entry.State = EntityState.Modified;
        }
        else
        {
            // 无 ID,认为是新实体
            entry.State = EntityState.Added;
        }
    });
    
    await context.SaveChangesAsync();
}

// 自定义判断逻辑
context.ChangeTracker.TrackGraph(order, node =>
{
    var entry = node.Entry;
    
    if (node.Entry.Entity is OrderItem item)
    {
        if (item.Id == 0)
        {
            entry.State = EntityState.Added;
        }
        else if (item.Quantity == 0)
        {
            entry.State = EntityState.Deleted;
        }
        else
        {
            entry.State = EntityState.Modified;
        }
    }
    else
    {
        entry.State = EntityState.Unchanged;
    }
});

策略 3: 使用 JSON Patch ​

csharp
// Microsoft.AspNetCore.JsonPatch
[HttpPatch("orders/{id}")]
public async Task<IActionResult> PatchOrder(int id, [FromBody] JsonPatchDocument<Order> patchDoc)
{
    var order = await context.Orders.FindAsync(id);
    if (order == null)
        return NotFound();
    
    // 应用补丁
    patchDoc.ApplyTo(order, ModelState);
    
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    
    await context.SaveChangesAsync();
    
    return NoContent();
}

// 客户端发送:
// PATCH /api/orders/101
// [
//   { "op": "replace", "path": "/totalAmount", "value": 399.99 },
//   { "op": "add", "path": "/items/-", "value": { "productId": 30, "quantity": 1 } }
// ]

策略 4: 比较哈希检测变更 ​

csharp
public class ChangeDetectionService
{
    public async Task UpdateIfChangedAsync(OrderDto dto)
    {
        var order = await context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == dto.Id);
        
        // 计算当前数据库状态的哈希
        var currentHash = ComputeHash(order);
        
        // 计算传入 DTO 的哈希
        var incomingHash = ComputeHash(dto);
        
        // 只有变更时才更新
        if (currentHash != incomingHash)
        {
            // 应用变更
            ApplyChanges(order, dto);
            await context.SaveChangesAsync();
        }
    }
    
    private string ComputeHash(object obj)
    {
        using var sha256 = SHA256.Create();
        var json = JsonSerializer.Serialize(obj, new JsonSerializerOptions 
        { 
            Order = true,
            IgnoreNullValues = true 
        });
        
        var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(json));
        return Convert.ToBase64String(bytes);
    }
}

复杂场景处理 ​

场景 1: 级联删除孤儿记录 ​

csharp
// 配置级联删除
modelBuilder.Entity<Order>()
    .HasMany(o => o.Items)
    .WithOne(i => i.Order)
    .OnDelete(DeleteBehavior.Cascade);  // 删除订单时自动删除订单项

// 或者: 自动删除孤儿记录
modelBuilder.Entity<Order>()
    .HasMany(o => o.Items)
    .WithOne(i => i.Order)
    .OnDelete(DeleteBehavior.ClientCascade);

// 使用时:
var order = await context.Orders
    .Include(o => o.Items)
    .FirstOrDefaultAsync(o => o.Id == orderId);

// 移除某些项
order.Items.Remove(order.Items.First());

await context.SaveChangesAsync();
// ✅ 被移除的 OrderItem 自动从数据库删除

场景 2: 部分更新 ​

csharp
// 只更新特定字段
public async Task PartialUpdateAsync(int id, Dictionary<string, object> updates)
{
    var order = await context.Orders.FindAsync(id);
    
    foreach (var kvp in updates)
    {
        switch (kvp.Key)
        {
            case "TotalAmount":
                order.TotalAmount = Convert.ToDecimal(kvp.Value);
                break;
            case "OrderDate":
                order.OrderDate = Convert.ToDateTime(kvp.Value);
                break;
            // 只允许更新特定字段
        }
    }
    
    await context.SaveChangesAsync();
}

// 或使用投影只加载需要的字段
var order = await context.Orders
    .Where(o => o.Id == id)
    .Select(o => new { o.Id, o.TotalAmount })
    .FirstOrDefaultAsync();

场景 3: 批量更新断开连接的实体 ​

csharp
public async Task BulkUpdateAsync(List<ProductDto> products)
{
    // 方法 1: 逐个附加(慢)
    foreach (var dto in products)
    {
        var product = await context.Products.FindAsync(dto.Id);
        product.Price = dto.Price;
        product.Stock = dto.Stock;
    }
    await context.SaveChangesAsync();
    
    // 方法 2: 批量 SQL(快)
    foreach (var dto in products)
    {
        context.Database.ExecuteSqlRaw(
            "UPDATE Products SET Price = {0}, Stock = {1} WHERE Id = {2}",
            dto.Price, dto.Stock, dto.Id);
    }
    
    // 方法 3: EF Core 7+ ExecuteUpdate(最快)
    await context.Products
        .Where(p => products.Select(d => d.Id).Contains(p.Id))
        .ExecuteUpdateAsync(setters => setters
            .SetProperty(p => p.Price, p => GetPriceForProduct(p.Id, products))
            .SetProperty(p => p.Stock, p => GetStockForProduct(p.Id, products)));
}

场景 4: 处理并发冲突 ​

csharp
public async Task UpdateWithConcurrencyAsync(OrderDto dto, byte[] originalRowVersion)
{
    var order = await context.Orders.FindAsync(dto.Id);
    
    // 设置原始 RowVersion
    context.Entry(order).OriginalValues["RowVersion"] = originalRowVersion;
    
    // 应用变更
    order.TotalAmount = dto.TotalAmount;
    
    try
    {
        await context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        // 获取数据库中的当前值
        var databaseValues = ex.Entries.First().GetDatabaseValues();
        
        // 选择解决策略
        // 策略 1: 客户端获胜(强制覆盖)
        ex.Entries.First().OriginalValues.SetValues(databaseValues);
        await context.SaveChangesAsync();
        
        // 策略 2: 数据库获胜(放弃修改)
        // ex.Entries.First().Reload(ReloadMode.StoreWins);
        
        // 策略 3: 合并变更
        // MergeChanges(ex.Entries.First(), databaseValues);
    }
}

.NET 8/9/10 新特性 ​

.NET 8: 改进的变更检测 ​

csharp
// .NET 8 优化了断开连接实体的变更检测
// 对于大型实体图,性能提升 25-35%

context.ChangeTracker.TrackGraph(largeGraph, node =>
{
    node.Entry.State = EntityState.Modified;
});

await context.SaveChangesAsync();  // 更快!

.NET 9: 增强的 TrackGraph ​

csharp
// .NET 9 提供更智能的 TrackGraph
context.ChangeTracker.TrackGraph(order, node =>
{
    // 自动推断状态
    var state = InferEntityState(node.Entry.Entity);
    node.Entry.State = state;
    
    // 记录跟踪信息
    Console.WriteLine($"Tracking {node.Entry.Entity.GetType().Name} as {state}");
});

private EntityState InferEntityState(object entity)
{
    // 智能判断逻辑
    return entity is IHaveId e && e.Id == 0 
        ? EntityState.Added 
        : EntityState.Modified;
}

.NET 10: 智能图形更新(路线图) ​

预计特性:

  • 自动检测实体图变更
  • AI 驱动的冲突解决建议
  • 图形更新性能分析工具
  • 可视化实体关系图

最佳实践与陷阱 ​

最佳实践 ​

1. 优先使用"查询后更新" ​

csharp
// ✅ 推荐: 简单、安全、清晰
public async Task UpdateAsync(EntityDto dto)
{
    var entity = await context.Entities.FindAsync(dto.Id);
    if (entity == null) throw new NotFoundException();
    
    // 应用变更
    entity.Property1 = dto.Property1;
    entity.Property2 = dto.Property2;
    
    await context.SaveChangesAsync();
}

// ❌ 避免: 复杂的 TrackGraph,除非确实需要

2. 使用 DTO 而非实体 ​

csharp
// ✅ 推荐: DTO 传输,实体内部使用
public class OrderDto
{
    public int Id { get; set; }
    public decimal TotalAmount { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public async Task UpdateAsync(OrderDto dto)
{
    var order = await context.Orders
        .Include(o => o.Items)
        .FirstOrDefaultAsync(o => o.Id == dto.Id);
    
    // Map DTO to Entity
    order.TotalAmount = dto.TotalAmount;
    // ...
    
    await context.SaveChangesAsync();
}

// ❌ 避免: 直接接收实体
public async Task UpdateAsync(Order order)  // 💥 安全风险!

3. 验证所有权 ​

csharp
// ✅ 推荐: 验证用户有权修改
public async Task UpdateAsync(int orderId, OrderDto dto, int userId)
{
    var order = await context.Orders
        .FirstOrDefaultAsync(o => o.Id == orderId && o.CustomerId == userId);
    
    if (order == null)
        throw new UnauthorizedAccessException("You don't own this order");
    
    // 应用变更...
}

4. 使用事务保证一致性 ​

csharp
// ✅ 推荐: 复杂更新使用事务
using var transaction = await context.Database.BeginTransactionAsync();

try
{
    var order = await context.Orders
        .Include(o => o.Items)
        .FirstOrDefaultAsync(o => o.Id == orderId);
    
    // 多个操作...
    
    await context.SaveChangesAsync();
    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

常见陷阱 ​

陷阱 1: 忘记加载导航属性 ​

csharp
// ❌ 错误: 未 Include 就修改集合
var order = await context.Orders.FindAsync(orderId);
// 💥 order.Items 是 null 或空集合!

foreach (var item in dto.Items)
{
    order.Items.Add(...);  // 💥 NullReferenceException
}

// ✅ 正确: Include 加载
var order = await context.Orders
    .Include(o => o.Items)
    .FirstOrDefaultAsync(o => o.Id == orderId);

陷阱 2: 重复附加实体 ​

csharp
// ❌ 错误: 重复附加
context.Orders.Attach(order);
context.Entry(order).State = EntityState.Modified;

context.Orders.Attach(order);  // 💥 InvalidOperationException!

// ✅ 正确: 检查是否已跟踪
if (!context.ChangeTracker.Entries<Order>().Any(e => e.Entity.Id == order.Id))
{
    context.Orders.Attach(order);
}

陷阱 3: 忽略并发控制 ​

csharp
// ❌ 错误: 不检查并发
var order = await context.Orders.FindAsync(orderId);
order.TotalAmount = newAmount;
await context.SaveChangesAsync();  // 💥 可能覆盖他人的修改!

// ✅ 正确: 使用 RowVersion
context.Entry(order).OriginalValues["RowVersion"] = originalVersion;
await context.SaveChangesAsync();  // ✅ 检测并发冲突

陷阱 4: 性能问题 ​

csharp
// ❌ 错误: N+1 查询
foreach (var dto in dtos)
{
    var entity = await context.Entities.FindAsync(dto.Id);  // 💥 每次循环都查询
    entity.Name = dto.Name;
}
await context.SaveChangesAsync();

// ✅ 正确: 批量加载
var ids = dtos.Select(d => d.Id).ToList();
var entities = await context.Entities
    .Where(e => ids.Contains(e.Id))
    .ToListAsync();  // 单次查询

foreach (var dto in dtos)
{
    var entity = entities.First(e => e.Id == dto.Id);
    entity.Name = dto.Name;
}
await context.SaveChangesAsync();

总结 ​

核心要点 ​

  1. 图形更新: 处理断开连接的实体图的变更
  2. 推荐策略: 查询后更新(简单、安全)
  3. 高级 API: TrackGraph 自动遍历实体图
  4. 并发控制: 始终使用 RowVersion 检测冲突
  5. 性能优化: 批量加载,避免 N+1 查询

策略选择指南 ​

场景推荐策略原因
单个实体更新查询后更新简单安全
复杂实体图TrackGraph自动遍历
部分字段更新JSON PatchRESTful
批量更新ExecuteUpdate高性能
离线同步哈希检测减少不必要更新

代码模板 ​

csharp
// 模板 1: 查询后更新
var entity = await context.Entities
    .Include(e => e.Children)
    .FirstOrDefaultAsync(e => e.Id == id);

// 应用变更
entity.Property = newValue;

// 处理集合
var toRemove = entity.Children.Where(...).ToList();
foreach (var child in toRemove)
{
    entity.Children.Remove(child);
    context.Children.Remove(child);
}

await context.SaveChangesAsync();

// 模板 2: TrackGraph
context.ChangeTracker.TrackGraph(graph, node =>
{
    node.Entry.State = node.Entry.IsKeySet 
        ? EntityState.Modified 
        : EntityState.Added;
});

await context.SaveChangesAsync();

// 模板 3: 并发控制
context.Entry(entity).OriginalValues["RowVersion"] = originalVersion;

try
{
    await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    // 处理冲突
}

下一步 ​

基于 MIT 许可发布