.NET EF 6中无需关联类删除多对多关系条目方法咨询
我正在开发一个包含大量多对多关系的项目,希望避免为每个关系创建关联类。使用Code First+Fluent API配置的数据库关系正常,通过DbContext.Update()能添加多对多条目,但移除列表条目后调用Update()无法删除数据库中对应关系。因为没有关联类,也没法用DbContext.Remove(),想找不用关联类和Include()的增删方法。
多对多表配置代码
public class FullStackBootcampSetTubeDBContext : DbContext { // 创建数据库连接 // 定义实体DbSet属性 protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 其他实体表配置 modelBuilder.Entity<Channel>().HasMany(i => i.Subscribers).WithMany(j => j.SubscribedTo).UsingEntity<Dictionary<string, object>>( "ChannelSubscriber", k => k .HasOne<User>() .WithMany() .HasForeignKey("UserId") .HasConstraintName("FK_ChannelSubscriber_Users_UserId") .OnDelete(DeleteBehavior.Cascade), k => k .HasOne<Channel>() .WithMany() .HasForeignKey("ChannelId") .HasConstraintName("FK_ChannelSubscriber_Channels_ChannelId") .OnDelete(DeleteBehavior.ClientCascade)); // 其他实体表配置 } }
通用CRUD仓储类代码
public class EfEntityRepositoryBase<TEntity, TContext> : IEntityRepository<TEntity> where TEntity : class, IEntity, new() where TContext : DbContext, new() { public void Add(TEntity entity) { using (TContext DbContext = new TContext()) { DbContext.Add<TEntity>(entity); DbContext.SaveChanges(); } } public void Delete(TEntity entity) { using (TContext DbContext = new TContext()) { DbContext.Remove<TEntity>(entity); DbContext.SaveChanges(); } } public virtual TEntity Get(Expression<Func<TEntity, bool>> filter) { using (TContext DbContext = new TContext()) { return DbContext.Set<TEntity>().SingleOrDefault(filter); } } public List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null) { using (TContext DbContext = new TContext()) { return filter == null ? DbContext.Set<TEntity>().ToList() : DbContext.Set<TEntity>().Where(filter).ToList(); } } public void Update(TEntity entity) { using (TContext DbContext = new TContext()) { DbContext.Update<TEntity>(entity); DbContext.SaveChanges(); } } }
IEntityRepository是该类的接口,我对.NET EF不太熟悉,不确定这些方法的实现是否正确。
删除订阅频道的业务代码
public IResult DeleteSubscribedChannel(int id, int channelId) { try { var user = _userDao.Get(p => p.Id == id); var channel = _channelDao.Get(p => p.Id == channelId); if (user != null) { if (channel != null) { if (user.SubscribedTo.Where((channel) => channel.Id == channelId) != null) { user.SubscribedTo = user.SubscribedTo.Where((channel) => channel.Id != channelId).ToList(); channel.SubscriberCount--; _channelDao.Update(channel); _userDao.Update(user); return new SuccessResult(Messages.UserUnsubscribedTr); } else { return new ErrorResult(Messages.UserIsNotSubscribed); } } return new ErrorResult(Messages.NoChannel); } else { return new ErrorResult(Messages.NoUser); } } catch (Exception Ex) { return new ErrorResult(Ex.Message); } }
userDao和channelDao是上述仓储类的实例,用于执行CRUD操作。
编辑
我修改了EfEntityRepositoryBase中的Update方法以包含关联关系,但还是无法删除多对多条目。更新后的方法如下:
public void Update(TEntity entity, Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null) { using (TContext DbContext = new TContext()) { DbSet<TEntity> dbSet = DbContext.Set<TEntity>(); IQueryable<TEntity> entities = dbSet; if (include != null) { entities = include(entities); } dbSet.Update(entity); DbContext.SaveChanges(); } }
另外,重写方法并从数据库重新获取用户后替换列表能解决问题,但这违反了通用仓储模式。修改后的Update方法仍能添加多对多条目,但无法删除。
解决方案
核心问题分析
问题根源在于:
- 仓储类每次操作都新建
DbContext,导致从Get方法获取的实体处于非跟踪状态,Update方法无法识别多对多集合的删除操作(EF Core需要跟踪集合的变更才能生成删除关联表的SQL)。 - 即使修改了
Update方法添加include参数,也只是查询了数据,并没有将传入的实体附加到上下文并跟踪集合变更。
无需关联类的解决方法
方法1:修改仓储类,复用上下文跟踪实体
调整仓储类的生命周期,确保Get和Update使用同一个DbContext,让EF Core能跟踪实体集合的变更:
// 改为注入DbContext而非每次新建,或者在业务层使用同一个上下文实例 public class EfEntityRepositoryBase<TEntity, TContext> : IEntityRepository<TEntity> where TEntity : class, IEntity, new() where TContext : DbContext, new() { private readonly TContext _dbContext; // 构造函数注入DbContext(推荐依赖注入方式) public EfEntityRepositoryBase(TContext dbContext) { _dbContext = dbContext; } public void Add(TEntity entity) { _dbContext.Add<TEntity>(entity); _dbContext.SaveChanges(); } public void Delete(TEntity entity) { _dbContext.Remove<TEntity>(entity); _dbContext.SaveChanges(); } public virtual TEntity Get(Expression<Func<TEntity, bool>> filter) { return _dbContext.Set<TEntity>().SingleOrDefault(filter); } public List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null) { return filter == null ? _dbContext.Set<TEntity>().ToList() : _dbContext.Set<TEntity>().Where(filter).ToList(); } public void Update(TEntity entity) { // 附加实体并标记为修改 _dbContext.Attach(entity); _dbContext.Entry(entity).State = EntityState.Modified; // 显式告知EF跟踪集合变更 _dbContext.Entry(entity).Collection(e => ((dynamic)e).SubscribedTo).IsModified = true; _dbContext.SaveChanges(); } }
注意:这里的((dynamic)e).SubscribedTo是为了适配通用仓储,如果你不想用动态类型,可以在接口中添加获取集合的方法,或者针对特定实体重写Update。
方法2:直接操作关联表(无需关联类)
EF Core允许通过DbContext.Set<Dictionary<string, object>>("关联表名")直接操作无关联类的多对多中间表:
// 在业务代码中直接删除中间表记录 public IResult DeleteSubscribedChannel(int id, int channelId) { try { using(var context = new FullStackBootcampSetTubeDBContext()) { // 直接删除中间表中的对应记录 context.Set<Dictionary<string, object>>("ChannelSubscriber") .Remove(new Dictionary<string, object> { {"UserId", id}, {"ChannelId", channelId} }); // 更新频道订阅数 var channel = context.Channel.Find(channelId); if(channel != null) { channel.SubscriberCount--; context.Update(channel); } context.SaveChanges(); return new SuccessResult(Messages.UserUnsubscribedTr); } } catch (Exception Ex) { return new ErrorResult(Ex.Message); } }
这种方式不需要跟踪实体,直接操作中间表,适合不想修改仓储类的场景。
方法3:优化现有业务代码,确保上下文跟踪
如果你坚持使用现有仓储的生命周期,在业务层重新获取带跟踪的实体并更新集合:
public IResult DeleteSubscribedChannel(int id, int channelId) { try { using(var context = new FullStackBootcampSetTubeDBContext()) { // 跟踪用户实体并包含订阅集合 var user = context.User.Include(u => u.SubscribedTo).SingleOrDefault(p => p.Id == id); var channel = context.Channel.Find(channelId); if (user == null) return new ErrorResult(Messages.NoUser); if (channel == null) return new ErrorResult(Messages.NoChannel); var subscribedChannel = user.SubscribedTo.FirstOrDefault(c => c.Id == channelId); if(subscribedChannel == null) return new ErrorResult(Messages.UserIsNotSubscribed); // 移除集合中的条目,EF会自动跟踪删除 user.SubscribedTo.Remove(subscribedChannel); channel.SubscriberCount--; context.SaveChanges(); return new SuccessResult(Messages.UserUnsubscribedTr); } } catch (Exception Ex) { return new ErrorResult(Ex.Message); } }
这种方式虽然需要用Include,但只在特定业务方法中使用,不需要修改所有仓储的CRUD方法。
内容的提问来源于stack exchange,提问作者enanas

