Appearance
显式加载 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次查询总结
核心要点
- 显式加载: 手动控制导航属性加载时机
- API:
Entry().Collection().LoadAsync()或Reference().LoadAsync() - 优势: 完全控制、支持过滤/分页/条件加载
- 劣势: N+1 查询风险,性能不如 Include
- 适用: 条件加载、分页加载、按需加载
决策流程
需要加载导航属性?
├─ 总是需要 → 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();下一步
- 📖 阅读 投影查询 Select DTO
- 🔧 学习 图形更新
- 🚀 了解 原始 SQL 查询