Skip to content

显式加载 Explicit Loading ​

目录 ​


什么是显式加载 ​

概念理解 ​

显式加载(Explicit Loading) 是通过手动调用 API 来加载导航属性的技术,提供对数据加载时机的精确控制。

csharp
// 三种加载方式对比

// 1. 预加载(Eager Loading): 查询时立即加载
var customer = await context.Customers
    .Include(c => c.Orders)  // 立即加载
    .FirstOrDefaultAsync(c => c.Id == 1);

// 2. 延迟加载(Lazy Loading): 访问时自动加载
var customer = await context.Customers.FindAsync(1);
var orders = customer.Orders;  // 自动触发查询(需要 virtual)

// 3. 显式加载(Explicit Loading): 手动控制加载
var customer = await context.Customers.FindAsync(1);
await context.Entry(customer)
    .Collection(c => c.Orders)
    .LoadAsync();  // ← 明确指定何时加载

核心优势 ​

✅ 完全控制: 明确知道何时执行查询
✅ 避免意外查询: 不会像延迟加载那样自动触发
✅ 灵活过滤: 可以在加载时应用过滤条件
✅ 无需 virtual: 不需要将属性标记为 virtual
✅ 条件加载: 根据业务逻辑决定是否加载

适用场景 ​

场景推荐程度原因
条件加载⭐⭐⭐⭐⭐根据业务逻辑决定是否加载
分页加载⭐⭐⭐⭐⭐可以应用 Skip/Take
过滤加载⭐⭐⭐⭐⭐可以应用 Where 条件
按需加载⭐⭐⭐⭐用户交互后加载
批量加载⭐⭐⭐循环中逐个加载
总是需要⭐⭐不如 Include 简洁

基本使用方法 ​

加载集合导航属性 ​

csharp
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Order> Orders { get; set; }
}

// 显式加载订单集合
var customer = await context.Customers.FindAsync(1);

// 检查是否已加载
bool isLoaded = context.Entry(customer)
    .Collection(c => c.Orders)
    .IsLoaded;  // false

// 显式加载
await context.Entry(customer)
    .Collection(c => c.Orders)
    .LoadAsync();

isLoaded = context.Entry(customer)
    .Collection(c => c.Orders)
    .IsLoaded;  // true

// 现在可以访问订单
foreach (var order in customer.Orders)
{
    Console.WriteLine(order.OrderDate);
}

加载引用导航属性 ​

csharp
public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public Customer Customer { get; set; }  // 引用导航属性
}

// 显式加载引用
var order = await context.Orders.FindAsync(101);

await context.Entry(order)
    .Reference(o => o.Customer)
    .LoadAsync();  // 使用 Reference 而非 Collection

// 访问客户信息
Console.WriteLine(order.Customer.Name);

检查加载状态 ​

csharp
var customer = await context.Customers.FindAsync(1);

// 检查集合是否已加载
if (!context.Entry(customer).Collection(c => c.Orders).IsLoaded)
{
    await context.Entry(customer)
        .Collection(c => c.Orders)
        .LoadAsync();
}

// 检查引用是否已加载
var order = await context.Orders.FindAsync(101);
if (!context.Entry(order).Reference(o => o.Customer).IsLoaded)
{
    await context.Entry(order)
        .Reference(o => o.Customer)
        .LoadAsync();
}

高级应用场景 ​

场景 1: 过滤加载 ​

csharp
// 只加载最近的订单
var customer = await context.Customers.FindAsync(1);

await context.Entry(customer)
    .Collection(c => c.Orders)
    .Query()  // ← 关键: 获取可查询的 IQueryable
    .Where(o => o.OrderDate >= DateTime.Today.AddDays(-30))
    .OrderByDescending(o => o.OrderDate)
    .LoadAsync();

// 现在 customer.Orders 只包含最近 30 天的订单
foreach (var order in customer.Orders)
{
    Console.WriteLine($"{order.OrderDate}: {order.TotalAmount}");
}

// SQL:
// SELECT * FROM Orders 
// WHERE CustomerId = 1 AND OrderDate >= @date
// ORDER BY OrderDate DESC

场景 2: 分页加载 ​

csharp
public async Task<List<Order>> GetCustomerOrdersAsync(
    int customerId, 
    int pageNumber, 
    int pageSize)
{
    var customer = await context.Customers.FindAsync(customerId);
    
    if (customer == null)
        throw new NotFoundException($"Customer {customerId} not found");
    
    // 分页加载订单
    var orders = await context.Entry(customer)
        .Collection(c => c.Orders)
        .Query()
        .OrderByDescending(o => o.OrderDate)
        .Skip((pageNumber - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();
    
    return orders;
}

// 使用示例
var recentOrders = await GetCustomerOrdersAsync(
    customerId: 1, 
    pageNumber: 1, 
    pageSize: 10);

场景 3: 统计而不加载完整对象 ​

csharp
var customer = await context.Customers.FindAsync(1);

// 只获取订单数量,不加载完整订单对象
var orderCount = await context.Entry(customer)
    .Collection(c => c.Orders)
    .Query()
    .CountAsync();

// 获取订单总金额
var totalSpent = await context.Entry(customer)
    .Collection(c => c.Orders)
    .Query()
    .SumAsync(o => o.TotalAmount);

// 获取最新订单日期
var lastOrderDate = await context.Entry(customer)
    .Collection(c => c.Orders)
    .Query()
    .MaxAsync(o => o.OrderDate);

Console.WriteLine($"订单数: {orderCount}, 总消费: {totalSpent}, 最后下单: {lastOrderDate}");

// ✅ 优势: 只执行聚合查询,不传输完整对象

场景 4: 条件加载 ​

csharp
public async Task<Customer> GetCustomerWithConditionalLoadingAsync(int customerId)
{
    var customer = await context.Customers.FindAsync(customerId);
    
    if (customer == null)
        return null;
    
    // 根据客户等级决定是否加载订单
    if (customer.IsVip)
    {
        // VIP 客户: 加载所有历史订单
        await context.Entry(customer)
            .Collection(c => c.Orders)
            .LoadAsync();
    }
    else
    {
        // 普通客户: 仅加载最近 3 个月订单
        await context.Entry(customer)
            .Collection(c => c.Orders)
            .Query(q => q.Where(o => o.OrderDate >= DateTime.Today.AddMonths(-3)))
            .LoadAsync();
    }
    
    return customer;
}

场景 5: 多级显式加载 ​

csharp
var order = await context.Orders.FindAsync(101);

// 第一级: 加载客户
await context.Entry(order)
    .Reference(o => o.Customer)
    .LoadAsync();

// 第二级: 加载订单项
await context.Entry(order)
    .Collection(o => o.Items)
    .LoadAsync();

// 第三级: 加载每个订单项的产品
foreach (var item in order.Items)
{
    await context.Entry(item)
        .Reference(i => i.Product)
        .LoadAsync();
}

// ⚠️ 注意: 这会产生 N+1 查询问题!
// 更好的方式是使用 Include

场景 6: 批量显式加载 ​

csharp
// 场景: 已经有一组客户,需要为每个客户加载订单
var customers = await context.Customers
    .Where(c => c.City == "北京")
    .ToListAsync();

// ❌ 错误: N+1 查询
foreach (var customer in customers)
{
    await context.Entry(customer)
        .Collection(c => c.Orders)
        .LoadAsync();  // 每个客户一次查询
}

// ✅ 正确: 使用 Include
var customers = await context.Customers
    .Include(c => c.Orders)
    .Where(c => c.City == "北京")
    .ToListAsync();

// 或者: 分别查询然后关联
var customerIds = customers.Select(c => c.Id).ToList();
var orders = await context.Orders
    .Where(o => customerIds.Contains(o.CustomerId))
    .ToListAsync();

// 手动关联
foreach (var customer in customers)
{
    customer.Orders = orders
        .Where(o => o.CustomerId == customer.Id)
        .ToList();
}

性能优化与对比 ​

三种加载方式性能对比 ​

csharp
public class LoadingPerformanceTest
{
    private readonly AppDbContext _context;
    private readonly ILogger _logger;
    
    public async Task CompareLoadingStrategiesAsync()
    {
        var sw = Stopwatch.StartNew();
        
        // 测试 1: 预加载(Include)
        sw.Restart();
        var result1 = await _context.Customers
            .Include(c => c.Orders)
            .ToListAsync();
        sw.Stop();
        _logger.LogInformation($"预加载: {sw.ElapsedMilliseconds}ms");
        // ~150ms (单次查询,可能 JOIN)
        
        // 测试 2: 延迟加载
        sw.Restart();
        var result2 = await _context.Customers.ToListAsync();
        foreach (var customer in result2)
        {
            var orders = customer.Orders;  // 触发查询
        }
        sw.Stop();
        _logger.LogInformation($"延迟加载: {sw.ElapsedMilliseconds}ms");
        // ~500ms (N+1 查询)
        
        // 测试 3: 显式加载
        sw.Restart();
        var result3 = await _context.Customers.ToListAsync();
        foreach (var customer in result3)
        {
            await _context.Entry(customer)
                .Collection(c => c.Orders)
                .LoadAsync();
        }
        sw.Stop();
        _logger.LogInformation($"显式加载: {sw.ElapsedMilliseconds}ms");
        // ~450ms (N+1 查询,但可控)
    }
}

// 结论:
// - 总是需要数据 → Include(最快)
// - 偶尔需要数据 → 显式加载(可控)
// - 避免延迟加载(不可控)

显式加载 vs Include ​

特性Include显式加载
查询次数1次(或SplitQuery)N+1次
性能⭐⭐⭐⭐⭐⭐⭐⭐
灵活性⭐⭐⭐⭐⭐⭐⭐⭐
可控性⭐⭐⭐⭐⭐⭐⭐⭐
代码复杂度简单中等
适用场景总是需要数据条件/分页/过滤

优化技巧 ​

1. 避免循环中的显式加载 ​

csharp
// ❌ 错误: N+1 查询
var customers = await context.Customers.ToListAsync();
foreach (var customer in customers)
{
    await context.Entry(customer)
        .Collection(c => c.Orders)
        .LoadAsync();  // 💥 100个客户 = 101次查询
}

// ✅ 正确: 使用 Include
var customers = await context.Customers
    .Include(c => c.Orders)
    .ToListAsync();  // 1-2次查询

2. 利用 Query() 进行高效聚合 ​

csharp
// ❌ 低效: 加载完整对象再统计
var customer = await context.Customers.FindAsync(1);
await context.Entry(customer)
    .Collection(c => c.Orders)
    .LoadAsync();

var total = customer.Orders.Sum(o => o.TotalAmount);
// 传输了所有订单数据,只为了求和

// ✅ 高效: 数据库端聚合
var customer = await context.Customers.FindAsync(1);
var total = await context.Entry(customer)
    .Collection(c => c.Orders)
    .Query()
    .SumAsync(o => o.TotalAmount);
// 只传输一个数字

3. 组合使用 Include 和显式加载 ​

csharp
// ✅ 推荐: 主要数据 Include,次要数据显式加载
var customer = await context.Customers
    .Include(c => c.Orders)  // 总是需要的
    .FirstOrDefaultAsync(c => c.Id == 1);

// 根据需要加载更多信息
if (needInvoices)
{
    await context.Entry(customer)
        .Collection(c => c.Invoices)  // 偶尔需要
        .LoadAsync();
}

.NET 8/9/10 新特性 ​

.NET 8: 改进的显式加载性能 ​

csharp
// .NET 8 优化了显式加载的查询生成
// 对于批量显式加载,性能提升 15-25%

var customers = await context.Customers.ToListAsync();
foreach (var customer in customers)
{
    await context.Entry(customer)
        .Collection(c => c.Orders)
        .LoadAsync();  // .NET 8 内部优化
}

.NET 9: 增强的诊断信息 ​

csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .EnableDetailedErrors()
           .LogTo(Console.WriteLine, LogLevel.Information);
});

// .NET 9 输出更详细的加载信息:
// info: Explicitly loading collection 'Orders' for entity 'Customer' with key values: [1]
// info: Executed DbCommand (5ms) [SELECT ... FROM Orders WHERE CustomerId = @p0]

.NET 10: 智能加载建议(路线图) ​

预计特性:

  • 检测 N+1 查询模式并警告
  • 基于访问模式自动建议 Include
  • 显式加载性能分析工具
  • 批量加载优化 API

最佳实践与陷阱 ​

最佳实践 ​

1. 优先使用 Include ​

csharp
// ✅ 推荐: 如果确定需要关联数据,使用 Include
var orders = await context.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
    .ToListAsync();

// ⚠️ 仅在以下情况使用显式加载:
// - 条件加载
// - 分页加载
// - 过滤加载
// - 用户交互后加载

2. 使用 Query() 进行高效查询 ​

csharp
// ✅ 推荐: 利用 Query() 进行聚合和过滤
var orderCount = await context.Entry(customer)
    .Collection(c => c.Orders)
    .Query()
    .CountAsync();

var recentOrders = await context.Entry(customer)
    .Collection(c => c.Orders)
    .Query()
    .Where(o => o.OrderDate >= DateTime.Today.AddDays(-30))
    .ToListAsync();

3. 检查加载状态避免重复加载 ​

csharp
// ✅ 推荐: 检查是否已加载
if (!context.Entry(customer).Collection(c => c.Orders).IsLoaded)
{
    await context.Entry(customer)
        .Collection(c => c.Orders)
        .LoadAsync();
}

4. 封装复用 ​

csharp
// ✅ 推荐: 封装常用加载逻辑
public static class EntityLoadingExtensions
{
    public static async Task LoadRecentOrdersAsync(
        this DbContext context, 
        Customer customer, 
        int days = 30)
    {
        await context.Entry(customer)
            .Collection(c => c.Orders)
            .Query(q => q.Where(o => o.OrderDate >= DateTime.Today.AddDays(-days)))
            .LoadAsync();
    }
}

// 使用
await context.LoadRecentOrdersAsync(customer, 60);

常见陷阱 ​

陷阱 1: 忘记异步版本 ​

csharp
// ❌ 错误: 在异步上下中使用同步方法
public async Task<Customer> GetCustomerAsync(int id)
{
    var customer = await context.Customers.FindAsync(id);
    context.Entry(customer)
        .Collection(c => c.Orders)
        .Load();  // 💥 阻塞线程!
    return customer;
}

// ✅ 正确: 使用异步版本
public async Task<Customer> GetCustomerAsync(int id)
{
    var customer = await context.Customers.FindAsync(id);
    await context.Entry(customer)
        .Collection(c => c.Orders)
        .LoadAsync();  // ✅ 异步
    return customer;
}

陷阱 2: 在循环中显式加载 ​

csharp
// ❌ 错误: N+1 查询
var customers = await context.Customers.ToListAsync();
foreach (var customer in customers)
{
    await context.Entry(customer)
        .Collection(c => c.Orders)
        .LoadAsync();  // 💥 每次循环都查询
}

// ✅ 正确: 使用 Include
var customers = await context.Customers
    .Include(c => c.Orders)
    .ToListAsync();

陷阱 3: 访问未加载的导航属性 ​

csharp
// ❌ 错误: 未加载就访问
var customer = await context.Customers.FindAsync(1);
var count = customer.Orders.Count;  // 💥 NullReferenceException 或空集合

// ✅ 正确: 先加载再访问
var customer = await context.Customers.FindAsync(1);
await context.Entry(customer)
    .Collection(c => c.Orders)
    .LoadAsync();
var count = customer.Orders.Count;  // OK

陷阱 4: 过度使用显式加载 ​

csharp
// ❌ 错误: 所有关联都用显式加载
var customer = await context.Customers.FindAsync(1);
await context.Entry(customer).Collection(c => c.Orders).LoadAsync();
await context.Entry(customer).Collection(c => c.Invoices).LoadAsync();
await context.Entry(customer).Reference(c => c.Address).LoadAsync();
// 💥 4次查询!

// ✅ 正确: 主要数据 Include
var customer = await context.Customers
    .Include(c => c.Orders)
    .Include(c => c.Invoices)
    .Include(c => c.Address)
    .FirstOrDefaultAsync(c => c.Id == 1);
// 1-2次查询

总结 ​

核心要点 ​

  1. 显式加载: 手动控制导航属性加载时机
  2. API: Entry().Collection().LoadAsync() 或 Reference().LoadAsync()
  3. 优势: 完全控制、支持过滤/分页/条件加载
  4. 劣势: N+1 查询风险,性能不如 Include
  5. 适用: 条件加载、分页加载、按需加载

决策流程 ​

需要加载导航属性?
├─ 总是需要 → Include(预加载)
├─ 偶尔需要 → 显式加载
│   ├─ 需要过滤 → Query().Where()
│   ├─ 需要分页 → Query().Skip().Take()
│   └─ 条件加载 → if (condition) LoadAsync()
└─ 不确定 → Include(更安全)

代码模板 ​

csharp
// 模板 1: 基本显式加载
await context.Entry(entity)
    .Collection(e => e.NavigationProperty)
    .LoadAsync();

// 模板 2: 过滤加载
await context.Entry(entity)
    .Collection(e => e.NavigationProperty)
    .Query()
    .Where(x => x.Condition)
    .LoadAsync();

// 模板 3: 分页加载
var items = await context.Entry(entity)
    .Collection(e => e.NavigationProperty)
    .Query()
    .Skip(skip)
    .Take(take)
    .ToListAsync();

// 模板 4: 聚合查询
var count = await context.Entry(entity)
    .Collection(e => e.NavigationProperty)
    .Query()
    .CountAsync();

下一步 ​

基于 MIT 许可发布