设置导航属性不意味着一定要使用 Include 的方式加载数据。

适合使用 Include 的场景

1. 小规模关联数据

2. 一对一关系

3. 确定知道需要关联数据

4. 一级或二级深度关系

5. 过滤或限制条件

不适合使用 Include 的场景

1. 大规模一对多关系

2. 可选的辅助信息

3. 多对多关系的完整加载

4. 深层嵌套关系

5. 只需要标量值或统计信息

6. 循环引用的关系

实践建议

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

替代方案

经验法则

总的来说,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