Appearance
图形更新 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();总结
核心要点
- 图形更新: 处理断开连接的实体图的变更
- 推荐策略: 查询后更新(简单、安全)
- 高级 API: TrackGraph 自动遍历实体图
- 并发控制: 始终使用 RowVersion 检测冲突
- 性能优化: 批量加载,避免 N+1 查询
策略选择指南
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 单个实体更新 | 查询后更新 | 简单安全 |
| 复杂实体图 | TrackGraph | 自动遍历 |
| 部分字段更新 | JSON Patch | RESTful |
| 批量更新 | 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)
{
// 处理冲突
}下一步
- 📖 阅读 自定义迁移操作
- 🔧 学习 客户端vs服务端评估
- 🚀 了解 单元测试