Include
设置导航属性不意味着一定要使用 Include 的方式加载数据。
适合使用 Include 的场景¶
1. 小规模关联数据¶
- 关联表数据量不大,不会导致网络传输压力
- 例如:用户与其关联的几个订单、一篇文章与其评论(评论数合理)
2. 一对一关系¶
- 两个实体一一对应,几乎总是一起使用
- 例如:用户与用户资料、订单与发货地址
- 避免额外的数据库往返
3. 确定知道需要关联数据¶
- 业务逻辑明确需要该数据
- 例如:显示订单详情页面时,需要订单项(OrderItems)
- 不是可选的"也许需要"
4. 一级或二级深度关系¶
Include().ThenInclude()最多深到二级- 再往下就会导致数据传输过多,性能下降
5. 过滤或限制条件¶
- 配合
Where()使用,先过滤再加载 - 这样能减少实际加载的关联数据量
不适合使用 Include 的场景¶
1. 大规模一对多关系¶
- 一个父实体有成千上万个子实体
- 例如:一个商品有数万条库存记录
- 一次加载会非常低效,占用大量内存
2. 可选的辅助信息¶
- 不是核心业务需要,只是"可能用到"
- 例如:用户列表中可能需要用户关联的徽章、统计数据等
- 应该按需加载或在需要时单独查询
3. 多对多关系的完整加载¶
- 特别是关联表很大的情况
- 容易导致笛卡尔积问题,数据重复膨胀
- 例如:用户与其所有权限、角色关系
4. 深层嵌套关系¶
- 三级或更深的关系链
- 数据膨胀、查询复杂度高、性能急剧下降
5. 只需要标量值或统计信息¶
- 不需要完整的关联对象
- 例如:只需要订单数量、总金额,不需要订单详情
- 用
Select()投影效率更高
6. 循环引用的关系¶
- 双向导航且都用
Include会加载大量冗余数据 - 例如:Product 包含 Category,Category 包含所有 Products
- 导致数据爆炸
实践建议¶
- 这个关联数据会被使用吗?(不是"可能")
- 关联数据的规模有多大?
- 网络带宽会成为瓶颈吗?
- 内存占用会很大吗?
- 这个查询会被频繁执行吗?
替代方案¶
- 投影(Select):只取需要的字段,最高效
- 显式加载(Explicit Loading):用户交互时再加载
- 延迟加载:配置后自动加载,但要防止 N+1 问题
- 分离查询:分两次查询,避免 Join 导致的数据膨胀
经验法则¶
- Include 最适合用于"必需且数据量小"的关系
- 如果拿不准,先用投影,性能有问题再优化
- 监控 SQL 查询,看是否有数据重复或过度加载
总的来说,Include 是为了减少数据库往返,但代价是可能加载过多数据。要在"往返次数"和"数据传输量"之间找到平衡。
Examples¶
// 示例1:简单投影 - 只取需要的字段
// 场景:显示订单列表,只需要订单基本信息和客户名称
var orders = dbContext.Orders
.Select(o => new OrderListDto
{
OrderId = o.Id,
OrderDate = o.OrderDate,
TotalAmount = o.TotalAmount,
CustomerName = o.Customer.Name // 跨越关系取值
})
.ToList();
// 示例2:投影关联集合
// 场景:显示订单详情,包含该订单的所有项目
var orderDetails = dbContext.Orders
.Where(o => o.Id == orderId)
.Select(o => new OrderDetailDto
{
OrderId = o.Id,
OrderDate = o.OrderDate,
CustomerName = o.Customer.Name,
Items = o.OrderItems.Select(oi => new OrderItemDto
{
ProductName = oi.Product.Name,
Quantity = oi.Quantity,
UnitPrice = oi.UnitPrice,
Subtotal = oi.Quantity * oi.UnitPrice
}).ToList() // 嵌套投影
})
.FirstOrDefault();
// 示例3:匿名类型投影
// 场景:快速查询,不需要定义 DTO
var productSummary = dbContext.Products
.Select(p => new
{
p.Id,
p.Name,
p.Price,
CategoryName = p.Category.Name,
OrderCount = p.OrderItems.Count() // 聚合
})
.ToList();
// 示例4:条件投影
// 场景:根据条件返回不同的字段
var userInfo = dbContext.Users
.Select(u => new UserDto
{
UserId = u.Id,
Username = u.Name,
Email = u.Email,
IsAdmin = u.Roles.Any(r => r.Name == "Admin"), // 条件判断
RoleCount = u.Roles.Count()
})
.ToList();
// 示例5:与 Include 对比
// ❌ 不好的做法 - 加载完整对象图
var ordersWithInclude = dbContext.Orders
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.ToList(); // 加载了大量不需要的数据
// ✅ 更好的做法 - 只投影需要的字段
var ordersWithProjection = dbContext.Orders
.Select(o => new
{
o.Id,
o.OrderDate,
CustomerName = o.Customer.Name,
ItemCount = o.OrderItems.Count,
Items = o.OrderItems.Select(oi => new
{
oi.Product.Name,
oi.Quantity,
oi.UnitPrice
}).ToList()
})
.ToList();
// 示例6:分页 + 投影
// 场景:列表显示通常需要分页,投影能保证高效
var page = dbContext.Products
.Where(p => p.IsActive)
.OrderByDescending(p => p.CreatedDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(p => new ProductListItemDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name,
ReviewCount = p.Reviews.Count(),
AverageRating = p.Reviews.Any() ? p.Reviews.Average(r => r.Rating) : 0
})
.ToList();
投影 vs Include 的关键区别¶
| 方面 | Include | Select(投影) |
|---|---|---|
| 数据传输 | 加载整个对象,可能过多 | 只取需要的字段 |
| 内存占用 | 较大 | 较小 |
| 灵活性 | 固定,加载完整对象 | 高,可自定义返回结构 |
| 性能 | 关联数据少时可以,多时差 | 通常更高效 |
| 用途 | 需要完整对象时 | 大多数查询场景 |
最佳实践¶
- 默认使用投影 - 除非明确需要完整对象,否则用
Select() - 配合 DTO - 创建查询专用的 DTO,明确表达需要什么数据
- 在数据库端计算 -
Count()、Sum()、Average()等聚合在 SQL 中执行,不是内存中 - 注意 ToList 位置 - 在
Select()之后再ToList(),让 SQL 来执行过滤和投影
ref¶
- 与 Claude Haiku 4.5 交流得到的内容