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

一对多关系中软删除的实现难题及优化需求

优化关联实体软删除的几种可行方案

方案1:EF Core SaveChanges拦截器(通用简洁,适配多层级关联)

通过自定义拦截器,在保存变更时自动递归标记所有关联的BaseModel派生实体为软删除状态,无需手动处理每个实体的导航属性。

实现拦截器

public class SoftDeleteInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is null) 
            return base.SavingChangesAsync(eventData, result, cancellationToken);

        // 捕获所有被标记为删除的BaseModel实体
        var deletedEntities = eventData.Context.ChangeTracker.Entries<BaseModel>()
            .Where(e => e.State == EntityState.Deleted);

        foreach (var entry in deletedEntities)
        {
            // 替换硬删除为软删除标记
            entry.State = EntityState.Modified;
            entry.Entity.IsDeleted = true;

            // 递归处理关联实体
            MarkRelatedEntitiesAsDeleted(entry);
        }

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private void MarkRelatedEntitiesAsDeleted(EntityEntry entry)
    {
        foreach (var navigation in entry.Navigations)
        {
            // 处理集合类型导航属性
            if (navigation.IsCollection)
            {
                if (navigation.CurrentValue is IEnumerable<BaseModel> relatedEntities)
                {
                    foreach (var relatedEntity in relatedEntities)
                    {
                        var relatedEntry = entry.Context.Entry(relatedEntity);
                        if (relatedEntry.State != EntityState.Deleted && relatedEntry.State != EntityState.Modified)
                        {
                            relatedEntry.State = EntityState.Modified;
                            relatedEntity.IsDeleted = true;
                            MarkRelatedEntitiesAsDeleted(relatedEntry);
                        }
                    }
                }
            }
            // 处理单个实体类型导航属性
            else
            {
                if (navigation.CurrentValue is BaseModel relatedEntity)
                {
                    var relatedEntry = entry.Context.Entry(relatedEntity);
                    if (relatedEntry.State != EntityState.Deleted && relatedEntry.State != EntityState.Modified)
                    {
                        relatedEntry.State = EntityState.Modified;
                        relatedEntity.IsDeleted = true;
                        MarkRelatedEntitiesAsDeleted(relatedEntry);
                    }
                }
            }
        }
    }
}

注册拦截器到DbContext

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.AddInterceptors(new SoftDeleteInterceptor());
}

简化后的业务代码

public async Task<string> Handle(DeleteCourseRequest request, CancellationToken cancellationToken)
{
    var course = await _baseService.GetByPropertyAsyncWithoutMap(c => c.Id == request.Id);
    // 直接调用删除,拦截器自动处理所有关联实体
    await _baseService.Delete(course);
    return "Done";
}

提示:无需手动Include导航属性,EF Core追踪或延迟加载会自动识别关联实体。

方案2:批量更新(性能最优,避免加载实体)

如果追求极致性能,直接生成SQL批量更新语句,跳过实体加载步骤,适合固定关联结构的场景。

public async Task<string> Handle(DeleteCourseRequest request, CancellationToken cancellationToken)
{
    // 更新主实体
    await _dbContext.Courses
        .Where(c => c.Id == request.Id)
        .ExecuteUpdateAsync(s => s.SetProperty(c => c.IsDeleted, true), cancellationToken);

    // 更新关联的Centers
    await _dbContext.Centers
        .Where(c => c.CourseId == request.Id)
        .ExecuteUpdateAsync(s => s.SetProperty(c => c.IsDeleted, true), cancellationToken);

    // 更新关联的Sections
    await _dbContext.Sections
        .Where(s => s.CourseId == request.Id)
        .ExecuteUpdateAsync(s => s.SetProperty(c => c.IsDeleted, true), cancellationToken);

    // 多层级关联示例:更新Sections下的子实体
    await _dbContext.SubSections
        .Where(ss => ss.SectionId.In(_dbContext.Sections.Where(s => s.CourseId == request.Id).Select(s => s.Id)))
        .ExecuteUpdateAsync(s => s.SetProperty(c => c.IsDeleted, true), cancellationToken);

    return "Done";
}

优势:无内存实体加载开销,执行效率最高;缺点:需手动编写每个关联实体的更新逻辑。

方案3:BaseService封装通用递归方法

若不想用拦截器,可在基础服务中封装递归标记方法,需提前Include所有需要处理的导航属性。

封装通用方法

public class BaseService<T> where T : BaseModel
{
    // ... 现有方法 ...

    public void MarkEntityAndRelatedAsDeleted(T entity, DbContext context)
    {
        entity.IsDeleted = true;
        var entry = context.Entry(entity);

        foreach (var navigation in entry.Navigations)
        {
            if (navigation.IsCollection)
            {
                if (navigation.CurrentValue is IEnumerable<BaseModel> relatedEntities)
                {
                    foreach (var related in relatedEntities)
                    {
                        if (!related.IsDeleted)
                            MarkEntityAndRelatedAsDeleted(related, context);
                    }
                }
            }
            else
            {
                if (navigation.CurrentValue is BaseModel relatedEntity && !relatedEntity.IsDeleted)
                    MarkEntityAndRelatedAsDeleted(relatedEntity, context);
            }
        }
    }
}

业务代码调用

public async Task<string> Handle(DeleteCourseRequest request, CancellationToken cancellationToken)
{
    var course = await _baseService.GetByPropertyAsyncWithoutMap(c => c.Id == request.Id, 
        c => c.Include(c => c.Centers)
              .Include(c => c.Sections)
              .ThenInclude(s => s.SubSections)); // 按需Include多层级导航

    _baseService.MarkEntityAndRelatedAsDeleted(course, _dbContext);
    await _dbContext.SaveChangesAsync(cancellationToken);
    return "Done";
}

关键配套配置

确保全局查询过滤器自动过滤软删除数据,避免业务代码重复判断:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (typeof(BaseModel).IsAssignableFrom(entityType.ClrType))
        {
            modelBuilder.Entity(entityType.ClrType)
                .HasQueryFilter(e => EF.Property<bool>(e, "IsDeleted") == false);
        }
    }
}

内容的提问来源于stack exchange,提问作者Mohamed Alaa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 21:55:03