You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET EF 6中无需关联类删除多对多关系条目方法咨询

EF Core 无关联类多对多关系删除问题解决方案

我正在开发一个包含大量多对多关系的项目,希望避免为每个关系创建关联类。使用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方法仍能添加多对多条目,但无法删除。


解决方案

核心问题分析

问题根源在于:

  1. 仓储类每次操作都新建DbContext,导致从Get方法获取的实体处于非跟踪状态,Update方法无法识别多对多集合的删除操作(EF Core需要跟踪集合的变更才能生成删除关联表的SQL)。
  2. 即使修改了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 17:05:59