EntityFramework Core使用Include加载关联实体调用SaveChangesAsync被自动删除问题
问题描述
我从数据库读取数据后,对数据库上下文执行SaveChangesAsync()时,随关联关系一同加载的实体会被从数据库中删除。
我的测试代码如下:
Category tools = new("Tools"); ProductType p = new("Hammer", 25, tools); Ware w1 = new("S1", p, new("Floor")); Ware w2 = new("S2", p, new("Floor")); Ware w3 = new("S3", p, new("Floor")); Ware w4 = new("S4", p, new("Floor")); Ware w5 = new("S5", p, new("Floor")); p.AddWare(w1); p.AddWare(w2); p.AddWare(w3); p.AddWare(w4); p.AddWare(w5); unitOfWork.ProductTypeRepository.Create(p); ProductType selected = unitOfWork.ProductTypeRepository.GetByIdAsync(46).Result; ProductType selected2 = unitOfWork.ProductTypeRepository.GetByIdAsyncWithRelationships(50).Result; Console.WriteLine("____"); var done = unitOfWork.SaveChangesAsync().Result; Console.WriteLine(done);
上述代码中,selected对应的数据未被删除,而selected2对应的数据被删除了。
两个查询方法的代码如下:
public async Task<ProductType> GetByIdAsync(int id) { return await _shopDbContext.ProductTypes.SingleOrDefaultAsync(p => p.ProductTypeId == id); }
以及带关联加载的方法:
public async Task<ProductType> GetByIdAsyncWithRelationships(int id) { return await _shopDbContext.ProductTypes .Include(p => p.OfferProductTypes) .ThenInclude(op => op.Offer) .Include(p => p.Wares) .Include(p => p.Category) .SingleOrDefaultAsync(p => p.ProductTypeId == id); }
从MSSQL事件探查器中可以捕获到对应的删除语句,先是删除关联的5条Ware记录,再删除ProductType主记录。
我在UnitOfWork的SaveChangesAsync()中添加了变更跟踪日志代码:
public Task<int> SaveChangesAsync() { var ct = _context.ChangeTracker; foreach(var e in ct.Entries()) { Console.WriteLine(e.Entity.GetType().Name + " : " +e.State); } return _context.SaveChangesAsync(); }
大部分时候日志只输出ProductType : Unchanged,但偶尔也会输出完整的变更状态,能看到新增的实体后面跟着对应的Deleted状态标记。
我尝试绕过UnitOfWork直接调用数据库上下文,所有使用Include()和ThenInclude()加载关联实体的查询结果都会被删除。
解决方案
这个问题本质是EF Core变更跟踪的状态冲突,核心原因和修复方案如下:
- 新增实体未提交就查询导致的状态冲突
你在调用Create(p)之后没有先调用SaveChanges提交新增操作,就直接查询同主键的实体。此时同一个上下文的变更跟踪器里会同时存在你刚创建的待新增实体,和查询返回的已存在实体。如果两者主键相同,EF会合并状态,如果你自定义的实体集合初始化逻辑把查询加载的关联集合清空,EF会认为你主动移除了所有关联数据,触发级联删除。
修复方式:新增实体后先提交保存,再执行查询操作。 - 实体集合属性的错误初始化
检查你的ProductType实体类中Wares等集合导航属性的定义,如果你的写法是:
public List<Ware> Wares {get; set;} = new List<Ware>();
当EF用Include加载关联数据时,会先执行集合初始化创建空列表,再把数据库查询到的Ware添加进去。如果你的实体类重写了Equals/GetHashCode但逻辑错误,或者AddWare方法有额外的过滤逻辑,EF会误判关联被移除,触发删除。
修复方式:把集合属性改成无初始化的虚属性,或者使用 backing field 避免EF加载时触发集合重置:
private List<Ware> _wares = new List<Ware>(); public virtual IReadOnlyList<Ware> Wares => _wares.AsReadOnly(); // 用AddWare方法操作_wares集合
- 临时验证方案
如果需要快速确认问题,可以在关联查询时加上AsNoTracking()关闭变更跟踪,修改后的查询方法如下:
public async Task<ProductType> GetByIdAsyncWithRelationships(int id) { return await _shopDbContext.ProductTypes .AsNoTracking() .Include(p => p.OfferProductTypes) .ThenInclude(op => op.Offer) .Include(p => p.Wares) .Include(p => p.Category) .SingleOrDefaultAsync(p => p.ProductTypeId == id); }
如果修改后不再触发删除,就可以确认是变更跟踪冲突导致的问题。
内容的提问来源于stack exchange,提问作者Benjamin Larsen
相关产品推荐
相关产品推荐

