02020409 EF Core基础09-一对一、多对多、EF Core基于关系的复杂查询

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查看表
Typora-Logo
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}; // @1

                ctx.Add(dl01); // 此时存od01是不可以的,因为od01没有关联两个对象。而dl01中Order = od01关联了两个对象。
                ctx.SaveChanges(); 
            }

            Console.WriteLine("数据插入成功!!!");
        }
    }
}
控制台输出:
数据插入成功!!!

说明:在@1处,Order = od01使得两个对象产生关系,保存时会自动给外键赋值。我们不需要显式的外键赋值。

// 查看数据库
T_Orders表
1	书	NULL

T_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	书	NULL

T_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 1
          FROM [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※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-10-06 11:11  qinway  阅读(16)  评论(0)    收藏  举报