Skip to content

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
  • 导致数据爆炸

实践建议

  1. 这个关联数据会被使用吗?(不是"可能")
  2. 关联数据的规模有多大?
  3. 网络带宽会成为瓶颈吗?
  4. 内存占用会很大吗?
  5. 这个查询会被频繁执行吗?

替代方案

  • 投影(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(投影)
数据传输 加载整个对象,可能过多 只取需要的字段
内存占用 较大 较小
灵活性 固定,加载完整对象 高,可自定义返回结构
性能 关联数据少时可以,多时差 通常更高效
用途 需要完整对象时 大多数查询场景

最佳实践

  1. 默认使用投影 - 除非明确需要完整对象,否则用 Select()
  2. 配合 DTO - 创建查询专用的 DTO,明确表达需要什么数据
  3. 在数据库端计算 - Count()Sum()Average() 等聚合在 SQL 中执行,不是内存中
  4. 注意 ToList 位置 - 在 Select() 之后再 ToList(),让 SQL 来执行过滤和投影

ref

  • 与 Claude Haiku 4.5 交流得到的内容