EF Core CosmosDB Upsert(更新/插入)处理方案咨询
我之前开发了一个包,在EF创建的SQL数据库中运行良好。近期尝试将其用于CosmosDB及Guid类型主键,这是我首次接触CosmosDB。
保存记录时,我通过检查主键值判断操作类型:若为类型默认值则视为插入,否则视为更新,代码如下:
public virtual TModel Save(TModel data) { _dynamic.Add(data); _context.Entry(data).State = data.Id.Equals(default(TKey)) ? EntityState.Added : EntityState.Modified; _context.SaveChanges(); return data; }
这么做是出于性能考量,避免更新前从数据库拉取数据,但EF Core似乎没有内置的Upsert功能?
我发现CosmosDB插入时Id属性已被填充(因为CosmosDB不自动生成Id,依赖EF提供),这合理但破坏了我的原有逻辑。
核心问题
有没有不依赖Id未设置状态的更好Upsert处理方式?
补充信息
传入的TModel data来自用户请求,此时:
- 尚未从数据库拉取任何数据
- 上下文内无其他待变更记录
- 另有一个类似的批量添加数据后调用
SaveChanges的方法,以控制工作量
采用Upsert模式是因为用户通过单一API端点保存数据,无论新增还是更新记录。原方法通过指定操作类型规避数据库Upsert的性能问题,但CosmosDB插入时Id已生成,导致该逻辑失效,可能需要深入了解CosmosDB内部及与EF的交互逻辑。
针对CosmosDB+EF Core的Upsert场景,有几种替代方案可以避开依赖主键默认值的逻辑:
1. 轻量查询判断+批量字段更新
先通过一次轻量查询判断记录是否存在,存在则直接更新字段,不存在则插入:
public virtual TModel Save(TModel data) { var exists = _context.Set<TModel>().Any(e => e.Id == data.Id); if (exists) { // 直接更新指定字段,无需加载实体到上下文 _context.Set<TModel>() .Where(e => e.Id == data.Id) .ExecuteUpdate(setters => setters // 按需映射需要更新的字段,批量场景可通过反射自动映射 .SetProperty(e => e.Property1, data.Property1) .SetProperty(e => e.Property2, data.Property2)); } else { _context.Add(data); _context.SaveChanges(); } return data; }
优点:无需加载实体,性能损耗低;缺点:需手动维护更新字段,批量场景可通过反射简化映射逻辑。
2. 调用CosmosDB原生Upsert API
直接借助EF Core的Cosmos扩展获取容器,调用原生Upsert能力:
using Microsoft.EntityFrameworkCore.Cosmos; public virtual async Task<TModel> SaveAsync(TModel data) { var container = _context.Database.GetCosmosClient().GetContainer("你的数据库名", "你的容器名"); var response = await container.UpsertItemAsync(data, new PartitionKey(data.分区键字段)); return response.Resource; }
优点:逻辑简洁,完全利用CosmosDB原生Upsert特性;缺点:依赖CosmosDB SDK,失去EF Core的数据库抽象能力,跨库兼容性下降。
3. 结合上下文追踪+查询判断
利用上下文干净的特性(无其他待变更记录),先检查上下文是否已追踪目标实体,再结合查询判断状态:
public virtual TModel Save(TModel data) { var trackedEntry = _context.ChangeTracker.Entries<TModel>().FirstOrDefault(e => e.Entity.Id == data.Id); if (trackedEntry != null) { _context.Entry(trackedEntry.Entity).CurrentValues.SetValues(data); } else { var exists = _context.Set<TModel>().Any(e => e.Id == data.Id); _context.Entry(data).State = exists ? EntityState.Modified : EntityState.Added; } _context.SaveChanges(); return data; }
这种方案既保留EF的抽象能力,又避开了主键默认值的限制,适配你当前上下文无其他记录的场景。
4. 批量保存适配方案
针对批量场景,先批量查询所有待处理ID的存在性,再批量设置实体状态:
public virtual void BulkSave(List<TModel> dataList) { var ids = dataList.Select(d => d.Id).ToList(); var existingIds = _context.Set<TModel>() .Where(e => ids.Contains(e.Id)) .Select(e => e.Id) .ToList(); foreach (var data in dataList) { _context.Entry(data).State = existingIds.Contains(data.Id) ? EntityState.Modified : EntityState.Added; } _context.SaveChanges(); }
仅需一次查询即可处理批量数据,性能损耗可控。
内容的提问来源于stack exchange,提问作者Randy Hall

