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

软删除场景下如何高效获取关联数据?避免遍历大数据集

优化软删除关联数据的查询效率

你现在的问题在于把所有关联的SchoolBranches和Classes(包括已软删除的)都加载到内存后再过滤,当数据量较大时,这会带来不必要的内存占用和IO开销,效率自然低下。我们可以直接在数据库层面就过滤掉软删除的子数据,避免无效数据的传输和内存处理。

下面是几种可行的优化方案,按推荐程度排序:


方案1:使用EF Core 5+支持的带过滤的Include(推荐)

从EF Core 5开始,官方支持在Include方法中直接添加过滤条件,这样关联数据在查询数据库时就会自动过滤掉已软删除的记录,不需要再在内存中循环处理。

修改你的代码如下:

public async Task<DataSourceResult> GetAll(DataSourceRequest request)
{
    var query = _unitOfWork.SchoolsRepository.GetAll()
        .Where(x => !x.IsDeleted)
        // 过滤未删除的SchoolBranches
        .Include(x => x.SchoolBranches.Where(sb => !sb.IsDeleted))
        // 过滤未删除的Classes,注意ThenInclude要基于上面过滤后的SchoolBranches
        .ThenInclude(sb => sb.Classes.Where(c => !c.IsDeleted));

    var data = await query.ToDataSourceResultAsync(request);

    if (data.Data == null || !((IEnumerable<Schools>)data.Data).Any())
    {
        throw new FriendlyExceptionHandler("No data found!", HttpStatusCode.BadRequest);
    }

    return data;
}

注意:如果你的EF Core版本低于5.0,这个方法不支持,需要看下面的方案。


方案2:全局查询过滤器(适合全系统统一过滤软删除数据)

如果你的系统中所有实体都使用IsDeleted字段做软删除,并且默认都要过滤已删除的记录,那么可以给每个实体配置全局查询过滤器,这样所有查询都会自动排除已软删除的数据,不需要每次手动加Where条件。

步骤1:在DbContext中配置过滤器

比如对于Schools、SchoolBranches、Classes实体:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 给Schools添加全局过滤
    modelBuilder.Entity<Schools>().HasQueryFilter(s => !s.IsDeleted);
    // 给SchoolBranches添加全局过滤
    modelBuilder.Entity<SchoolBranches>().HasQueryFilter(sb => !sb.IsDeleted);
    // 给Classes添加全局过滤
    modelBuilder.Entity<Classes>().HasQueryFilter(c => !c.IsDeleted);

    // 其他配置...
}

步骤2:简化查询代码

配置完后,你的查询代码可以简化成这样,不需要手动加任何过滤条件,关联数据也会自动排除已删除的:

public async Task<DataSourceResult> GetAll(DataSourceRequest request)
{
    var query = _unitOfWork.SchoolsRepository.GetAll()
        .Include(x => x.SchoolBranches)
        .ThenInclude(y => y.Classes);

    var data = await query.ToDataSourceResultAsync(request);

    if (data.Data == null || !((IEnumerable<Schools>)data.Data).Any())
    {
        throw new FriendlyExceptionHandler("No data found!", HttpStatusCode.BadRequest);
    }

    return data;
}

优点:一劳永逸,所有查询都自动过滤软删除数据;缺点:如果某些场景需要查询已删除的数据,需要手动调用IgnoreQueryFilters()来取消过滤。


方案3:投影查询(灵活控制返回字段)

如果不需要返回完整的实体对象,或者想更灵活地控制返回的数据结构,可以使用投影查询(Select)直接构造需要的DTO或者匿名对象,只包含未删除的子数据。

示例代码:

public async Task<DataSourceResult> GetAll(DataSourceRequest request)
{
    var query = _unitOfWork.SchoolsRepository.GetAll()
        .Where(x => !x.IsDeleted)
        .Select(school => new Schools
        {
            // 复制School的必要字段
            Id = school.Id,
            Name = school.Name,
            // 其他字段...
            IsDeleted = school.IsDeleted,
            // 过滤未删除的SchoolBranches
            SchoolBranches = school.SchoolBranches
                .Where(sb => !sb.IsDeleted)
                .Select(sb => new SchoolBranches
                {
                    // 复制SchoolBranches的必要字段
                    Id = sb.Id,
                    SchoolId = sb.SchoolId,
                    // 其他字段...
                    IsDeleted = sb.IsDeleted,
                    // 过滤未删除的Classes
                    Classes = sb.Classes
                        .Where(c => !c.IsDeleted)
                        .ToList()
                })
                .ToList()
        });

    var data = await query.ToDataSourceResultAsync(request);

    if (data.Data == null || !((IEnumerable<Schools>)data.Data).Any())
    {
        throw new FriendlyExceptionHandler("No data found!", HttpStatusCode.BadRequest);
    }

    return data;
}

优点:只查询需要的字段,减少数据传输量;缺点:如果实体字段较多,代码会比较繁琐,适合需要定制返回结构的场景。


为什么原来的方案效率低?

原来的代码先把所有关联数据(包括已删除的)从数据库加载到内存,然后通过两层foreach循环过滤,这会带来三个问题:

  • IO开销大:数据库需要返回更多的数据,网络传输耗时更长;
  • 内存开销大:内存中要存储大量无效的已删除数据,然后再过滤,浪费内存资源;
  • CPU开销大:两层循环需要遍历所有加载的子数据,数据量大时CPU占用高。

而上面的方案都是在数据库层面完成过滤,只返回需要的有效数据,从根源上解决了效率问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:07:23