当前位置: 首页 > news >正文

写 EF Core 查询,90% 的人第一步就错了:刚子教你避开所有坑

今天刚子不跟你扯理论,直接上实战代码,把 EF Core 复杂查询的几个核心技巧给你讲明白。顺便聊聊性能优化的几条铁律,省得你下次再踩坑。


先说核心:EF Core 复杂查询的3个核心技巧

处理复杂查询,你只需要记住这几招:

  1. 关联查询:用Include+ThenInclude一次性加载多级关联数据
  2. 动态筛选:用表达式树在运行时动态拼查询条件
  3. 性能优化:用AsNoTrackingSelect投影、AsSplitQuery控制数据加载

这些学会了,90% 的复杂查询场景你都能搞定。

刚子大白话:写 EF Core 查询,关键不是你写得多花哨,而是你要知道它生成的 SQL 长啥样。把 EF Core 当成一个带类型安全的 SQL 生成器,这才是正确心态。


场景一:多表关联查询(Include + ThenInclude)

基础用法:加载关联数据

例如我的博客系统:一个 Blog 有多个 Post,每个 Post 有一个 Author。

// 加载 Blog、关联的 Post、每个 Post 的 Author var blogs = await context.Blogs .Include(b => b.Posts) .ThenInclude(p => p.Author) .ToListAsync();

这个查询会把三层数据一次加载出来,生成的 SQL 是一个 JOIN 查询,把三张表一次性查完。

Include 也能过滤?当然可以

EF Core 支持在 Include 里对关联集合做过滤:

// 只加载今年发布的文章 var blogs = await context.Blogs .Include(b => b.Posts.Where(p => p.PublishDate.Year == DateTime.Now.Year)) .ToListAsync();

多级关联要多个 ThenInclude

如果需要加载更深层级的关联,继续链式调用ThenInclude就行:

// Blog → Posts → Author → AuthorDetails var blogs = await context.Blogs .Include(b => b.Posts) .ThenInclude(p => p.Author) .ThenInclude(a => a.Details) .ToListAsync();

划重点:Include + ThenInclude 链越长,生成的 JOIN 越复杂。如果要加载多个集合导航属性,注意笛卡尔积爆炸的问题。遇到这种情况,可以用AsSplitQuery()把一个大查询拆成多个小查询。


场景二:动态查询(表达式树)

为什么需要动态查询?

业务需求经常变:用户按多个条件筛选,但这些条件可能选也可能不选。用静态查询写一堆if?太丑了,还容易漏条件。

动态查询的核心是用Expression<Func<T, bool>>在运行时拼接查询条件。

手写一个动态筛选器

public async Task<List<Product>> SearchProductsAsync( string? name = null, decimal? minPrice = null, decimal? maxPrice = null, int? categoryId = null) { var query = context.Products.AsQueryable(); if (!string.IsNullOrEmpty(name)) query = query.Where(p => p.Name.Contains(name)); if (minPrice.HasValue) query = query.Where(p => p.Price >= minPrice.Value); if (maxPrice.HasValue) query = query.Where(p => p.Price <= maxPrice.Value); if (categoryId.HasValue) query = query.Where(p => p.CategoryId == categoryId.Value); return await query.ToListAsync(); }

这样写没问题,但条件越多代码越臃肿。更好的方式是用表达式树工具库,或者自己封装一个PredicateBuilder

PredicateBuilder 的实现原理

public static class PredicateBuilder { public static Expression<Func<T, bool>> True<T>() { return f => true; } public static Expression<Func<T, bool>> False<T>() { return f => false; } public static Expression<Func<T, bool>> Or<T>( this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); return Expression.Lambda<Func<T, bool>>( Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); } public static Expression<Func<T, bool>> And<T>( this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); return Expression.Lambda<Func<T, bool>>( Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); } }

用起来就很优雅了:

var predicate = PredicateBuilder.True<Product>(); if (!string.IsNullOrEmpty(name)) predicate = predicate.And(p => p.Name.Contains(name)); if (minPrice.HasValue) predicate = predicate.And(p => p.Price >= minPrice.Value); if (maxPrice.HasValue) predicate = predicate.And(p => p.Price <= maxPrice.Value); var products = await context.Products .Where(predicate) .ToListAsync();

划重点:千万别把自定义方法塞进表达式树。EF Core 不认识你的MyUtil.IsAdult(x),整段逻辑会被静默跳过,甚至退化为客户端求值——先查出全部数据,再在内存里过滤。性能直接崩。


场景三:分页 + 排序 + 过滤

分页是高频场景,EF Core 配合 LINQ 写起来很顺手:

public async Task<PagedResult<Product>> GetPagedProductsAsync( int pageIndex = 1, int pageSize = 10, string? sortBy = "Id", string? sortDirection = "asc", string? searchTerm = null) { var query = context.Products.AsQueryable(); // 过滤 if (!string.IsNullOrEmpty(searchTerm)) query = query.Where(p => p.Name.Contains(searchTerm)); // 排序(注意:这里用了字符串反射,生产环境建议用 switch 或字典映射) query = sortDirection?.ToLower() == "desc" ? query.OrderByDescending(GetSortExpression(sortBy)) : query.OrderBy(GetSortExpression(sortBy)); // 分页 var totalCount = await query.CountAsync(); var items = await query .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); return new PagedResult<Product> { Items = items, TotalCount = totalCount, PageIndex = pageIndex, PageSize = pageSize }; }

划重点:分页查询必须在SkipTake之前先做排序,否则 EF Core 会抛异常。另外,GetSortExpression这个函数要注意防止 SQL 注入,最好用白名单映射。


场景四:分组与聚合查询

按分类统计产品数量

var categoryStats = await context.Products .GroupBy(p => p.CategoryId) .Select(g => new { CategoryId = g.Key, ProductCount = g.Count(), AvgPrice = g.Average(p => p.Price), TotalRevenue = g.Sum(p => p.Price * p.SalesCount) }) .ToListAsync();

这个查询 EF Core 会翻译成一条带GROUP BY的 SQL 语句,直接在数据库端完成聚合计算,性能很好。

刚子大白话:能用GroupBy就别自己写循环算,数据库干这个比 C# 快多了。


性能优化:这 5 条铁律记住

1. 只读查询用AsNoTracking()

EF Core 默认会跟踪每个实体的变更,这在只读场景下完全是浪费。

var products = await context.Products .AsNoTracking() .Where(p => p.Price > 100) .ToListAsync();

加上AsNoTracking,EF Core 不会记录这些实体的状态变化,内存占用和 CPU 开销都大幅降低。

2. 只取需要的字段(投影)

不要每次都Select *,用投影只拿你真正需要的字段:

var productInfos = await context.Products .Select(p => new ProductDto { Id = p.Id, Name = p.Name, Price = p.Price }) .ToListAsync();

3. 用Select投影还能顺便加载关联数据

var orderInfos = await context.Orders .Select(o => new { OrderId = o.Id, CustomerName = o.Customer.Name, TotalAmount = o.Items.Sum(i => i.Price * i.Quantity), ItemCount = o.Items.Count() }) .ToListAsync();

这种方式比Include更精准,因为你只拿你需要的数据,SQL 生成的 JOIN 也更精简。

4. N+1 问题用Include解决

// ❌ 错误:会触发 N+1 次查询 // 场景:获取所有订单,并逐一输出客户名称 // 如果启用了延迟加载,以下代码会导致 1 次查询获取订单 + N 次查询获取每个订单的客户 var orders = await context.Orders.ToListAsync(); foreach (var order in orders) { Console.WriteLine(order.Customer); // 每次访问都触发一次查询 } // ✅ 正确:一次性预加载 // 场景:获取所有订单及对应的客户,仅需一次查询 var ordersWithCustomer = await context.Orders .Include(o => o.Customer) .ToListAsync();

Include显式预加载关联数据,把原本 1+N 次查询压成 1 次 JOIN 查询。

5. 集合过多时用AsSplitQuery()

如果一个查询包含多个集合导航属性,默认的单查询模式会产生笛卡尔积爆炸。这时用AsSplitQuery()拆分成多个 SQL:

var blogs = await context.Blogs .Include(b => b.Posts) .Include(b => b.Comments) .AsSplitQuery() .ToListAsync();

EF Core 会分别查询 Blog、Posts、Comments 三张表,然后在内存中组装,避免数据重复膨胀。


复杂查询铁律

场景推荐方案注意事项
多表关联加载Include+ThenInclude链别太长,注意笛卡尔积
动态多条件筛选表达式树 / PredicateBuilder别塞自定义方法,会被静默忽略
只读数据查询AsNoTracking()+Select投影减少内存开销
避免 N+1预加载 + 禁用延迟加载Include一次搞定
多集合查询AsSplitQuery()防笛卡尔积爆炸
数据量大分页 + 索引Skip/Take前必须排序
复杂聚合GroupBy/ 聚合函数EF Core 会翻译成 SQL
实在搞不定原生 SQL (FromSqlRaw)最后手段,别滥用

刚子结语

别把 EF Core 当成黑盒。你写出来的 LINQ 查询最终都会翻译成 SQL,不理解 SQL,你就写不出高效的 EF Core 查询。

我刚学 EF Core 的时候,也踩过 N+1、笛卡尔积、客户端求值这些坑。后来我养成了一个习惯:每个复杂查询都去检查生成的 SQL 长啥样

你可以用 EF Core 自带的日志功能:

optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging();

看一眼生成的 SQL,你就知道哪里写得不对了。

刚子的经验:写复杂查询的时候,先想清楚“我要的数据结构是什么”,再用 LINQ 去表达。把 EF Core 当成带类型安全的 SQL 生成器,别把它当成万能魔法箱。

如果你觉得这篇有用,点个赞、转给还在被 EF Core 复杂查询折磨的兄弟

我是刚子,一个写了六年 .NET 代码的程序员。咱们下回见!
原文链接:写 EF Core 查询,90% 的人第一步就错了:刚子教你避开所有坑 - 码农刚子的开发笔记

合集: C#/.NET开发者宝典 , C#/.NET 编程指南

标签: EFCore, EF

免责声明:本内容来自平台创作者,博客园系信息发布平台,仅提供信息存储空间服务。

好文要顶 关注我 收藏该文 微信分享

码农刚子
粉丝 - 61 关注 - 11

+加关注

9

« 上一篇: 序列化 JSON 时崩了?99% 是 EF 延迟加载惹的祸,三种解法拿走不谢
» 下一篇: 推荐一个开箱即用的.NET权限管理平台:Magic.NET

posted @ 2026-04-22 08:02 码农刚子 阅读(944) 评论(9) 收藏 举报

http://www.cnnetsun.cn/news/3004999.html

相关文章:

  • Python多核并发实战:绕过GIL的4种生产级方案
  • NewTab Redirect! 终极指南:5个场景彻底重塑你的浏览器工作流
  • 大数据量 Excel 导出性能优化:SXSSFWorkbook 流式写入实战
  • LMXCMS 1.4 SQL注入漏洞实战审计:从原理到修复
  • HeidiSQL 12.20 发布:修复多项问题,新增 SQLite 默认值关键字支持!
  • 4G 报警器和传统有线报警器比,哪个更靠谱?
  • Gemma 4 E2B/E4B端侧AI部署实战:离线、确定性与隐私可控的硬核指南
  • 从进化视角看 AI 与人脑:智能演化的底层同构规律
  • Faster-Whisper-GUI:基于PySide6的语音识别加速框架架构解析与日语场景优化实践
  • LessMSI:Windows安装包逆向工程与内容提取利器
  • 靠谱正规的开发小程序公司有哪些?
  • 公司网络卡顿怎么办?从现象到根因的完整排查与解决指南-爱包干™
  • Ryzen AI 笔记本跑大模型,Ollama 一行命令搞定
  • Java反序列化漏洞实战:从Shiro RememberMe到RCE利用链剖析
  • Crew AI源码分析 Day1 学习过程中上下文记忆的问题+环境安装
  • C语言 — 整型提升和算数转换
  • AI时代岗位价值再锚定:从防替代到重构职责的操作手册
  • Topit:让你的Mac窗口永远在最前方,工作效率提升300%的秘密武器
  • 锚定双碳热点,绿色智慧园区开启低碳运营新范式
  • ReAct Agent 完整实现:从零构建能查天气、算数学的智能助手
  • AlibabaProtect 服务彻底卸载指南
  • Midjourney V7实操指南:Personalization Profile与Draft Mode深度解析
  • 【经典面试】C++ Core Dump该怎么办?
  • Gemini 3.1 Pro工程实战指南:200万上下文与原生多模态如何落地技术工作流
  • 现代密码学实验四
  • AI回答采集任务调度与数据质量管理实践
  • 基于 EtherCAT + CiA402 的双机械臂10°周期运动流程解析
  • 如何3步实现智能屏幕翻译:终极跨语言沟通解决方案
  • WEF未来就业报告实操指南:从任务重构到6个月技能升级
  • 终极屏幕翻译工具:告别复制粘贴,实现真正的框选即译