-
C#EF Core关联查询——订单与用户数据联查
第一部分:C#基础入门
EF Core关联查询——订单与用户数据联查
实例介绍
你有没有在电商系统开发中踩过这样的坑?查询订单列表时,为了显示用户姓名,循环遍历每个订单去查用户信息——结果数据库被疯狂轰炸:1次查所有订单,再加上N次查用户(N是订单数),性能直接崩溃。这就是EF Core中经典的N+1查询问题。
本节将通过订单与用户的联查场景,带你掌握三种关联查询vb.net教程C#教程python教程SQL教程access 2010教程方式(延迟加载、立即加载、显式加载)的正确姿势,避开N+1等性能陷阱,让你的查询既高效又优雅。
需求分析
本节需解决3类核心场景,同时规避错误写法的危害:
1.延迟加载:按需加载关联数据,但易引发N+1问题;
2.立即加载:一次性加载主实体与关联数据,彻底解决N+1;
3.显式加载:手动触发关联数据加载,适合按需加载场景;
4.错误写法危害:N+1查询导致数据库压力陡增、未启用延迟加载引发异常、滥用延迟加载导致性能不可控。
目标:掌握EF Core关联查询的三种方式,根据场景选择最优方案,彻底解决N+1问题。
代码实现
前置条件:.NET 6+、EF Core 7+;需安装Microsoft.EntityFrameworkCore、Microsoft.EntityFrameworkCore.SqlServer、Microsoft.EntityFrameworkCore.Proxies(延迟加载用)。
准备工作:实体类与DbContext配置
实体类(User与Order为一对多关系):
csharp
// 用户实体
public class User
{
public int Id { get; set; }
public string Name { get; set; } = null!; // EF Core7+非空标注
public string Phone { get; set; } = null!;
// 导航属性:一个用户有多个订单(virtual启用延迟加载)
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
}
// 订单实体
public class Order
{
public int Id { get; set; }
public string OrderNo { get; set; } = null!;
public decimal TotalAmount { get; set; }
public int UserId { get; set; } // 外键
// 导航属性:一个订单属于一个用户(virtual启用延迟加载)
public virtual User User { get; set; } = null!;
}
DbContext配置:
csharp
public class ECommerceDbContext : DbContext
{
public DbSet<User> Users { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 替换为你的连接字符串
optionsBuilder.UseSqlServer("Server=.;Database=ECommerceDB;Trusted_Connection=True;")
.UseLazyLoadingProxies(); // 启用延迟加载代理
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置一对多关系(EF Core可自动推断,可选)
modelBuilder.Entity<Order>()
.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId);
}
}
场景1:延迟加载的错误与正确写法
错误写法1:未启用延迟加载却访问关联属性
csharp
// ❌错误:未启用延迟加载时,访问Order.User会引发异常
public async Task GetOrdersWrong1()
{
using var dbContext = new ECommerceDbContext();
var orders = await dbContext.Orders.ToListAsync();
foreach (var order in orders)
{
// 异常:Navigation property 'User' cannot be loaded (entity not tracked)
Console.WriteLine($"订单{order.OrderNo}的用户:{order.User.Name}");
}
}
**错误写法2**:启用延迟加载后滥用导致N+1
```csharp
// ❌错误:循环访问关联属性,触发1+N次查询(1次订单+N次用户)
public async Task GetOrdersWrong2()
{
using var dbContext = new ECommerceDbContext();
var orders = await dbContext.Orders.ToListAsync(); // 1次查询
foreach (var order in orders)
{
Console.WriteLine($"订单{order.OrderNo}的用户:{order.User.Name}");
// 每个订单触发1次User查询,N个订单就是N次
}
}
正确写法:单个实体用延迟加载,批量用立即加载
csharp
// ✅正确:单个订单查询用延迟加载(总查询次数可控)
public async Task GetOrderWithUserCorrect1(int orderId)
{
using var dbContext = new ECommerceDbContext();
var order = await dbContext.Orders.FindAsync(orderId); // 1次查询
if (order != null)
{
Console.WriteLine($"订单{order.OrderNo}的用户:{order.User.Name}");
// 仅触发1次User查询,总次数2次(可接受)
}
}
// ✅正确:批量订单用立即加载避免N+1
public async Task GetOrdersWithUserCorrect2()
{
using var dbContext = new ECommerceDbContext();
var orders = await dbContext.Orders
.Include(o => o.User) // 1次查询加载所有订单+用户
.ToListAsync();
foreach (var order in orders)
{
Console.WriteLine($"订单{order.OrderNo}的用户:{order.User.Name}");
// 无额外查询
}
}
场景2:立即加载的错误与正确写法
错误写法:循环手动查询关联数据(N+1)
csharp
// ❌错误:循环查用户,导致1+N次查询
public async Task GetOrdersWithUserWrong()
{
using var dbContext = new ECommerceDbContext();
var orders = await dbContext.Orders.ToListAsync(); // 1次
foreach (var order in orders)
{
var user = await dbContext.Users.FindAsync(order.UserId); // N次
Console.WriteLine($"订单{order.OrderNo}的用户:{user.Name}");
}
}
正确写法:用Include一次性加载
csharp
// ✅正确:立即加载所有订单+用户(1次查询)
public async Task GetOrdersWithUserCorrect()
{
using var dbContext = new ECommerceDbContext();
var orders = await dbContext.Orders
.Include(o => o.User) // LEFT JOIN User表
.Where(o => o.TotalAmount > 100) // 过滤条件
.OrderByDescending(o => o.Id) // 排序
.ToListAsync();
foreach (var order in orders)
{
Console.WriteLine($"订单{order.OrderNo}:用户{order.User.Name},金额{order.TotalAmount}");
}
}
// ✅进阶:多级关联加载(Order→OrderItems→Product)
public async Task GetOrdersWithItemsAndProductCorrect()
{
using var dbContext = new ECommerceDbContext();
var orders = await dbContext.Orders
.Include(o => o.User)
.Include(o => o.OrderItems) // 加载OrderItems
.ThenInclude(oi => oi.Product) // 加载Product
.ToListAsync();
}
场景3:显式加载的错误与正确写法
错误写法:未手动加载就访问关联属性
csharp
// ❌错误:未显式加载User,触发延迟加载或异常
public async Task GetOrderWithUserWrong()
{
using var dbContext = new ECommerceDbContext();
var order = await dbContext.Orders.FindAsync(1);
Console.WriteLine($"订单{order.OrderNo}的用户:{order.User.Name}");
// 若启用延迟加载则触发额外查询,否则异常
}
正确写法:手动加载关联数据
csharp
// ✅正确:显式加载单个关联属性(User)
public async Task GetOrderWithUserCorrect(int orderId)
{
using var dbContext = new ECommerceDbContext();
var order = await dbContext.Orders.FindAsync(orderId); // 1次查询
if (order != null)
{
// 显式加载User
await dbContext.Entry(order)
.Reference(o => o.User) // Reference用于单个导航属性
.LoadAsync(); // 1次查询
Console.WriteLine($"订单{order.OrderNo}的用户:{order.User.Name}");
}
}
// ✅正确:显式加载带过滤的集合(User的高金额订单)
public async Task GetUserWithHighAmountOrdersCorrect(int userId)
{
using var dbContext = new ECommerceDbContext();
var user = await dbContext.Users.FindAsync(userId); // 1次查询
if (user != null)
{
// 显式加载过滤后的Orders
await dbContext.Entry(user)
.Collection(u => u.Orders) // Collection用于集合
.Query() // 过滤条件
.Where(o => o.TotalAmount > 500)
.LoadAsync(); // 1次查询
Console.WriteLine($"用户{user.Name}的高金额订单数:{user.Orders.Count}");
}
}
逐行讲解
场景1:延迟加载
UseLazyLoadingProxies():启用延迟加载代理,EF Core生成实体的代理类,实现访问virtual导航属性时自动加载。
错误写法2:批量查询时滥用延迟加载会导致N+1,因为每个订单都触发一次User查询。
正确写法2:用Include一次性加载所有关联数据,避免N+1。
场景2:立即加载
Include(o => o.User):生成LEFT JOIN SQL语句,将User表与Order表合并查询,一次性获取所有数据。
ThenInclude:用于多级关联,比如Order→OrderItems→Product,需链式调用。
错误写法:循环手动查用户是性能杀手,尤其是订单数多时。
场景3:显式加载
dbContext.Entry(order):获取实体的跟踪信息(EntityEntry),用于操作关联数据。
Reference(o => o.User):针对单个导航属性(如Order.User),Collection针对集合(如User.Orders)。
Query():允许对关联数据进行过滤、排序,再加载,适合按需获取部分数据。
基础知识拓展
- 关联加载方式对比
| 加载方式 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 延迟加载 | 代理+virtual属性,访问时自动加载 | 代码简洁,按需加载 | 易引发N+1,性能不可控 | 单个实体的关联查询 |
| 立即加载 | Include/ThenInclude一次性加载 | 避免N+1,性能可控 | 可能加载冗余数据 | 批量查询关联数据 |
| 显式加载 | Entry+Reference/Collection手动加载 | 完全按需,灵活过滤 | 代码繁琐 | 部分实体的关联数据,复杂过滤 |
-
最佳实践
禁用延迟加载:若批量查询多,建议注释UseLazyLoadingProxies,强制显式处理关联加载,避免隐性N+1。
优先用立即加载:批量查询时,Include是解决N+1的首选。
显式加载带过滤:用Query()减少不必要的数据加载。
监控SQL:用EF Core的LogTo查看生成的SQL,检查是否有N+1。
AsNoTracking:查询仅用于展示时,用AsNoTracking()关闭跟踪,提升性能。
总结
本节通过订单与用户的联查场景,详细讲解了EF Core的三种关联加载方式。核心要点:
1.延迟加载适合单个实体,批量查询需避免;
2.立即加载是解决N+1的关键,用Include一次性加载;
3.显式加载适合按需加载,尤其是带过滤的关联数据;
4.始终监控SQL,避免隐性N+1。
掌握这些技巧,你就能在电商、CRM等系统中高效处理关联数据查询,提升应用性能。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49454.html










