VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 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():允许对关联数据进行过滤、排序,再加载,适合按需获取部分数据。
基础知识拓展

  1. 关联加载方式对比
加载方式 实现方式 优点 缺点 适用场景
延迟加载 代理+virtual属性,访问时自动加载 代码简洁,按需加载 易引发N+1,性能不可控 单个实体的关联查询
立即加载 Include/ThenInclude一次性加载 避免N+1,性能可控 可能加载冗余数据 批量查询关联数据
显式加载 Entry+Reference/Collection手动加载 完全按需,灵活过滤 代码繁琐 部分实体的关联数据,复杂过滤
  1. 最佳实践
    禁用延迟加载:若批量查询多,建议注释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


相关教程