软删除场景下如何高效获取关联数据?避免遍历大数据集
你现在的问题在于把所有关联的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

