02020409 EF Core基础09-一对一、多对多、EF Core基于关系的复杂查询
1. 一对一(视频3-20)
1.1 一对一关系
采购申请单 ↔ 采购订单
订单 ↔ 快递单
- 一对一:对方都是对方的唯一。
- 一个订单单对应一个快递单,一个快递单也对应一个订单。
1.2 新建一对一的实体类
class Order // 订单
{public long Id { get; set; }public string Name { get; set; }public string Address { get; set; }public Delivery Delivery { get; set;} // Delivery类型的属性,对应哪个快递单。
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
class Delivery // 快递单
{public long Id { get; set; }public string CompanyName { get; set; }public String Number { get; set; }public Order Order { get; set; } // Order类型的属性,对应哪个订单。public long OrderId { get; set; }
}说明:
1. 必须显式的在其中一个实体类中声明一个外键属性。显式声明的外键属性既可以在订单中声明,也可以在快递单中声明,注意只需要建一个就行。
2. 在前面讲到的一对多是否声明外键属性不强制要求显式声明,因为一对多情况下:多的一端是一定有一个外键属性来对应一的一端,会自动生成外键而不需要显式声明。
1.3 一对一关系配置
1、builder.HasOne<Delivery>(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(d=>d.OrderId);
2. 一对一项目示例
2.1 创建项目
- 创建OneToOne控制台项目
// OneToOne.csproj直接添加依赖包
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net5.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="microsoft.entityframeworkcore.sqlserver" Version="5.0.4" /><PackageReference Include="microsoft.entityframeworkcore.tools" Version="5.0.4"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets></PackageReference></ItemGroup></Project>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Order.cs
namespace OneToOne
{class Order{public long Id { get; set; }public string Name { get; set; }public string Address { get; set; }public Delivery Delivery { get; set; } // Delivery类型的属性,对应哪个快递单。}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// OrderConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace OneToOne
{class OrderConfig : IEntityTypeConfiguration<Order>{public void Configure(EntityTypeBuilder<Order> builder){builder.ToTable("T_Orders");builder.HasOne<Delivery>(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(o => o.OrderId);}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Delivery.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace OneToOne
{class Delivery{public long Id { get; set; }public string CompanyName { get; set; }public String Number { get; set; }public Order Order { get; set; } // Order类型的属性,对应哪个订单。public long OrderId { get; set; }}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// DeliveryConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace OneToOne
{class DeliveryConfig : IEntityTypeConfiguration<Delivery>{public void Configure(EntityTypeBuilder<Delivery> builder){builder.ToTable("T_Deliveries");}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
using System;namespace OneToOne{class MyDbContext : DbContext{public DbSet<Order> Orders { get; set; }public DbSet<Delivery> Deliveries { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True;MultipleActiveResultSets=true";optionsBuilder.UseSqlServer(connStr);// optionsBuilder.LogTo(Console.WriteLine);}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);}}
}
3.2 迁移数据库
PM> add-migration init
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
PM> update-database
Build started...
Build succeeded.
Applying migration '20250923121957_init'.
Done.
PM>
- 通过SSMS查看表
![]() T_Deliveries表和外键关系 | ![]() T_Orders表 |
3.3 插入数据
// 插入数据形式1:通过关联的形式存储数据,顺杆爬。
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;namespace OneToOne
{class Program{static void Main(string[] args){using(MyDbContext ctx = new MyDbContext()){Order od01 = new Order { Name = "书" };Delivery dl01 = new Delivery { CompanyName = "科科快递", Number = "keke001", Order = od01}; // @1ctx.Add(dl01); // 此时存od01是不可以的,因为od01没有关联两个对象。而dl01中Order = od01关联了两个对象。ctx.SaveChanges(); }Console.WriteLine("数据插入成功!!!");}}
}
控制台输出:
数据插入成功!!!说明:在@1处,Order = od01使得两个对象产生关系,保存时会自动给外键赋值。我们不需要显式的外键赋值。// 查看数据库
T_Orders表
1 书 NULLT_Deliveries表
Id CompanyName Number OrderId
1 科科快递 keke001 1
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 插入数据形式2:不考虑顺杆爬的问题,直接将所有的数据都存起来。
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;namespace OneToOne
{class Program{static void Main(string[] args){using(MyDbContext ctx = new MyDbContext()){Order od01 = new Order { Name = "书" };Delivery dl01 = new Delivery { CompanyName = "科科快递", Number = "keke001", Order = od01};// 直接存储,不考虑顺杆爬的问题。ctx.Orders.Add(od01); // 直接存,不考虑顺杆爬ctx.Deliveries.Add(dl01); // 直接存,不考虑顺杆爬ctx.SaveChanges(); }Console.WriteLine("数据插入成功!!!");}}
}// 查看数据库
T_Orders表
1 书 NULLT_Deliveries表
Id CompanyName Number OrderId
1 科科快递 keke001 1
3.4 查询数据
using System;
using System.Linq;namespace OneToOne
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){IQueryable<Order> orders = ctx.Orders.Where(o => o.Delivery.CompanyName == "科科快递"); // 查询科科快递对应的所有订单foreach (var item in orders){Console.WriteLine(item.Name);}}Console.WriteLine("数据查询成功!!!");}}
}控制台输出:
书
数据查询成功!!!
3. 多对多(视频3-21)
3.1 多对多关系
1、多对多:老师—学生。
2、EF Core 5.0开始,才正式支持多对多
3、需要中间表,举例数据。中间表由EF Core自动生成,我们只需要配置一下中间表的名字即可。说明:一个老师可以有多个学生,一个学生可以有多个老师。
3.2 新建多对多的实体类
class Student
{public long Id { get; set; }public string Name { get; set; }public List<Teacher> Teachers { get; set; } = new List<Teacher>();
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
class Teacher
{public long Id { get; set; }public string Name { get; set; }public List<Student> Students { get; set; } = new List<Student>();
}
3.3 多对多关系配置
builder.HasMany<Teacher>(s => s.Teachers).WithMany(t=>t.Students).UsingEntity(j=>j.ToTable("T_Students_Teachers"));说明:UsingEntity()配置中间表,参数为中间表的名称。
4. 多对多项目示例
4.1 创建项目
- 创建ManyToMany控制台项目
// ManyToMany.csproj
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net5.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="microsoft.entityframeworkcore.sqlserver" Version="5.0.4" /><PackageReference Include="microsoft.entityframeworkcore.tools" Version="5.0.4"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets></PackageReference></ItemGroup></Project>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Student.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ManyToMany
{class Student{public long Id { get; set; }public string Name { get; set; }public List<Teacher> Teachers { get; set; } = new List<Teacher>();}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// StudentConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ManyToMany
{class StudentConfig : IEntityTypeConfiguration<Student>{public void Configure(EntityTypeBuilder<Student> builder){builder.ToTable("T_Students");builder.HasMany<Teacher>(s => s.Teachers).WithMany(t => t.Students).UsingEntity(j => j.ToTable("T_Students_Teachers"));}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Teacher.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ManyToMany
{class Teacher{public long Id { get; set; }public string Name { get; set; }public List<Student> Students { get; set; } = new List<Student>();}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// TeacherConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ManyToMany
{class TeacherConfig : IEntityTypeConfiguration<Teacher>{public void Configure(EntityTypeBuilder<Teacher> builder){builder.ToTable("T_Teachers");}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
using System;namespace ManyToMany{class MyDbContext : DbContext{public DbSet<Student> Students { get; set; }public DbSet<Teacher> Teachers { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True;MultipleActiveResultSets=true";optionsBuilder.UseSqlServer(connStr);// optionsBuilder.LogTo(Console.WriteLine);}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);}}
}
4.2 迁移数据库
PM> add-migration init
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
PM> update-database
Build started...
Build succeeded.
Applying migration '20250923134033_init'.
Done.
- 查看数据表

4.3 插入数据
// Program.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;namespace ManyToMany
{class Program{static void Main(string[] args){using(MyDbContext ctx = new MyDbContext()){Student st01 = new Student { Name = "张三" };Student st02 = new Student { Name = "李四" };Student st03 = new Student { Name = "王五" };Teacher th01 = new Teacher { Name = "Tom" };Teacher th02 = new Teacher { Name = "Jerry" };Teacher th03 = new Teacher { Name = "Zack" };st01.Teachers.Add(th01);st01.Teachers.Add(th02);st02.Teachers.Add(th02);st02.Teachers.Add(th03);st03.Teachers.Add(th01);st03.Teachers.Add(th02);st03.Teachers.Add(th03);// 所有的学生和老师都插入,避免漏项。虽然麻烦,但是事少。不推荐顺杆爬ctx.Teachers.Add(th01);ctx.Teachers.Add(th02);ctx.Teachers.Add(th03);ctx.Students.Add(st01);ctx.Students.Add(st02);ctx.Students.Add(st03);ctx.SaveChanges();}Console.WriteLine("数据插入成功!!!");}}
}控制台输出:
数据插入成功!!!// T_Students表
Id Name
1 张三
2 李四
3 王五// T_Teachers表
Id Name
1 Tom
2 Jerry
3 Zack// T_Students_Teachers表
StudentsId TeachersId
1 1
3 1
1 2
2 2
3 2
2 3
3 3
4.4 查询数据
// Program.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;namespace ManyToMany
{class Program{static void Main(string[] args){using(MyDbContext ctx = new MyDbContext()){var teachers = ctx.Teachers.Include(t => t.Students);foreach (var th in teachers){Console.WriteLine(th.Name);foreach (var st in th.Students){Console.WriteLine("\t" + st.Name);}}}Console.WriteLine("数据查询成功!!!");}}
}控制台输出:
Tom张三王五
Jerry张三李四王五
Zack李四王五
数据查询成功!!!
5. EF Core基于关系的复杂查询(视频3-22)
5.1 一对多的复杂查询
- 基于02020407章2.1小结项目继续演示
// 通过SSMS在T_Articles表中添加如下数据
Id Title Message
1 杨中科入选中科院 大新闻
2 微软发布.NET 10.0 中新闻
3 微软发射微星 小新闻
4 中国发射小行星探测器 劲爆新闻// 通过SSMS在T_Comments表中添加如下数据
Id TheArticleId Message
1 1 太牛了
2 2 微软不过如此
3 3 微软真牛
4 4 OMG
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 查询评论中含有“微软”的所有的文章
形式1:ctx.Articles.Where(a=>a.Comments.Any(c=>c.Message.Contains("微软")));
形式2:ctx.Comments.Where(c => c.Message.Contains("微软")).Select(c => c.Article).Distinct();查看两种形式生成的SQL语句。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 查询形式1:Program.cs
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){// 从Article入手var items = ctx.Articles.Where(a => a.Comments.Any(c => c.Message.Contains("微软"))); // Article中Comments属性中Message属性包含“微软”的文章。foreach (var item in items){Console.WriteLine(item.Title);}}Console.ReadLine();}}
}控制台输出:
微软发布.NET 10.0
微软发射微星// 查看SQL语句
SELECT [t].[Id], [t].[Message], [t].[Title]FROM [T_Articles] AS [t]WHERE EXISTS (SELECT 1FROM [T_Comments] AS [t0]WHERE ([t].[Id] = [t0].[TheArticleId]) AND ([t0].[Message] LIKE N'%微软%'))
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 查询形式2:Program.cs
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){IQueryable<Article> items = ctx.Comments.Where(c => c.Message.Contains("微软")).Select(c => c.TheArticle); // 评论的Messages属性包含微软,然后Select(投影)到Article上。foreach (var item in items){Console.WriteLine(item.Title);}}Console.ReadLine();}}
}控制台输出:
微软发布.NET 10.0
微软发射微星// 查看SQL语句SELECT [t0].[Id], [t0].[Message], [t0].[Title]FROM [T_Comments] AS [t]INNER JOIN [T_Articles] AS [t0] ON [t].[TheArticleId] = [t0].[Id]WHERE [t].[Message] LIKE N'%微软%'
5.2 一对多的复杂查询:排重
- 基于本节5.1继续演示
// 通过SSMS在T_Articles表中添加如下数据
Id Title Message
1 杨中科入选中科院 大新闻
2 微软发布.NET 10.0 中新闻
3 微软发射微星 小新闻
4 中国发射小行星探测器 劲爆新闻// 通过SSMS在T_Comments表中添加如下数据
Id TheArticleId Message
1 1 科科太牛了
2 2 微软不过如此
3 3 微软真牛
4 3 微软真水
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Program.cs
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){IQueryable<Article> items = ctx.Comments.Where(c => c.Message.Contains("微软")).Select(c => c.TheArticle); // 评论的Messages属性包含微软,然后Select(投影)到Article上。foreach (var item in items){Console.WriteLine(item.Title);}}Console.ReadLine();}}
}控制台输出:
微软发布.NET 10.0
微软发射微星
微软发射微星// 查看SQL语句SELECT [t0].[Id], [t0].[Message], [t0].[Title]FROM [T_Comments] AS [t]INNER JOIN [T_Articles] AS [t0] ON [t].[TheArticleId] = [t0].[Id]WHERE [t].[Message] LIKE N'%微软%'说明:此时微软发射微星重复了2次。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Program.cs
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){IQueryable<Article> items = ctx.Comments.Where(c => c.Message.Contains("微软")).Select(c => c.TheArticle).Distinct(); // Distinct方法排重。foreach (var item in items){Console.WriteLine(item.Title);}}Console.ReadLine();}}
}控制台输出:
微软发布.NET 10.0
微软发射微星// 查看SQL语句
SELECT DISTINCT [t0].[Id], [t0].[Message], [t0].[Title]FROM [T_Comments] AS [t]INNER JOIN [T_Articles] AS [t0] ON [t].[TheArticleId] = [t0].[Id]WHERE [t].[Message] LIKE N'%微软%'说明:排重之后,相同的文章只输出一次。
5.3 一对多两种复杂查询方式对比
- 在5.1和5.2小节中,两种查询在应对不同的需求时效率是不同的,按需选择。
- 如果用5.1中有性能瓶颈,那么可以尝试用5.2中的形式,反之亦然。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※