02020407 EF Core基础07-一对多实体类&关系配置&插入数据&查询数据、设置额外的外键字段
1. EF Core一对多关系配置(视频3-14)
1.1 实体间关系
1、所谓“关系数据库”
2、复习:数据库表之间的关系:一对一、一对多、多对多。
3、EF Core不仅支持单实体操作,更支持多实体的关系操作。
4、三部曲:实体类中关系属性;FluentAPI关系配置;使用关系操作。
1.2 一对多(one to many):实体类
- 文章实体类Article、评论实体类Comment。一篇文章对应多条评论。
- 一篇文章有多个评论,一个评论只对应唯一的文章。
public class Article
{public long Id { get; set; }public string Title { get; set; }public string Message { get; set; }public List<Comment> Comments { get; set; } = new List<Comment>(); // 建一个空的list,而不用默认的null。
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
class Comment // 评论
{public long Id { get; set; }public Article TheArticle { get; set; }public string Message { get; set; }
}
1.3 EF Core中实体之间的关系配置
EF Core中实体之间关系的配置的套路:
HasXXX(…).WithXXX(…); // 有xxx.带着xxx。
XXX可选值One、Many。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
一对多:HasOne(…).WithMany(…);
一对一:HasOne(…).WithOne (…);
多对多:HasMany (…).WithMany(…);
1.4 一对多:关系配置
class ArticleConfig : IEntityTypeConfiguration<Article> // 一段
{public void Configure(EntityTypeBuilder<Article> builder){builder.ToTable("T_Articles");builder.Property(a => a.Content).IsRequired().IsUnicode();builder.Property(a => a.Title).IsRequired().IsUnicode().HasMaxLength(255);}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
class CommentConfig : IEntityTypeConfiguration<Comment> // 多端
{public void Configure(EntityTypeBuilder<Comment> builder){builder.ToTable("T_Comments");builder.HasOne<Article>(c=>c.Article).WithMany(a => a.Comments).IsRequired();builder.Property(c=>c.Message).IsRequired().IsUnicode();}
}说明:一对多的关系配置,即可以配置到一端,也可以配置到多端。老师一般配置到一端,这段代码配置到了多端。
2. 一对多示例
2.1 创建实体类和配置类
// OneToMany.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>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
</Project>
// Article.cs
using System.Collections.Generic;namespace OneToMany
{class Article // 文章{public long Id { get; set; }public string Title { get; set; }public string Message { get; set; }public List<Comment> Comments { get; set; } = new List<Comment>(); // 建一个空的list,而不用默认的null。}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// ArticleConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace OneToMany
{class ArticleConfig : IEntityTypeConfiguration<Article>{public void Configure(EntityTypeBuilder<Article> builder){builder.ToTable("T_Articles");builder.Property(a => a.Title).HasMaxLength(100).IsUnicode().IsRequired();builder.Property(a => a.Message).IsUnicode().IsRequired();}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Comment.cs
namespace OneToMany
{class Comment // 评论{public long Id { get; set; }public Article TheArticle { get; set; }public string Message { get; set; }}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// CommentConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace OneToMany
{class CommentConfig : IEntityTypeConfiguration<Comment>{public void Configure(EntityTypeBuilder<Comment> builder){builder.ToTable("T_Comments");builder.Property(c => c.Message).HasMaxLength(100).IsUnicode().IsRequired(); // 在Comment表建一个指向Article的外键builder.HasOne<Article>(a => a.TheArticle).WithMany(c => c.Comments).IsRequired();}}
}—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
using System;namespace OneToMany
{class MyDbContext : DbContext{public DbSet<Article> Articles { get; set; }public DbSet<Comment> Comments { 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);}}
}
2.2 完成数据库迁移
1、迁移生成数据库表。
2、编写代码测试数据插入。
3、不需要显式为Comment对象的Article属性赋值(当前赋值也不会出错),也不需要显式地把新创建的Comment类型的对象添加到DbContext中。EF Core会“顺竿爬”。
- 完成数据库迁移,查看外键信息:SSMS → 表 → T_Comments → 设计 → TheArticleId → 右键 → 关系

- 说明:
- 有些公司可能因为性能或者迁移的问题,不允许建立外键。关于这一块以后会讲解。
- 我们并没有在Article实体类中建立TheArticleId这个属性,但是数据库中自动生成了TheArticleId这个外键。
2.3 插入数据
// Program.cs
using System;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){// 创建文章Article article01 = new Article { Title = "杨中科被评为亚洲最酷程序员" , Message = "据报道:" };// 创建评论Comment comment01 = new Comment { Message = "太牛了!!!"};Comment comment02 = new Comment { Message = "吹吧!!!" };// Article的Comments属性是List类型。article01.Comments.Add(comment01);article01.Comments.Add(comment02);// 更新数据库ctx.Add(article01); // @1 将article01插入数据库,注意此时不用将comment01和comment02插入数据库。// ctx.Add(comment01); // 不写// ctx.Add(comment02); // 不写ctx.SaveChanges();}Console.WriteLine("数据插入成功!!!");}}
}控制台输出:
数据插入成功!!!说明:
1. 在@1处,comment01和comment02可以不用手动加入数据库,EF Core会顺着关系自动将它们两个插入到数据库。
2. 在@1处也可以手动将comment01和comment02插入数据库,但是没有必要。
2.4 查看数据库信息
// T_Articles表
Id Title Message
1 杨中科被评为亚洲最酷程序员 据报道:
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// T_Comments表
Id TheArticleId Message
1 1 太牛了!!!
2 1 吹吧!!!
3. 一对多关系数据的获取(视频3-15)
3.1 获取关系数据
Article a = ctx.Articles.Include(a=>a.Comments).Single(a=>a.Id==1);
Console.WriteLine(a.Title);
foreach(Comment c in a.Comments)
{Console.WriteLine(c.Id+":"+c.Message);
}
Include定义在Microsoft.EntityFrameworkCore命名空间中。
查看一下生成的SQL语句
3.2 获取数据示例:【正向查 → 通过Article(一)查Comments(多)】
- 在2.4基础上做如下演示
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){Article a01 = ctx.Articles.Single(a => a.Id == 1); // 确认有且只有Id为1的文章用SingleConsole.WriteLine(a01.Id);Console.WriteLine(a01.Title);foreach (Comment c in a01.Comments) // 遍历所有Comment{Console.WriteLine(c.Id + "," + c.Message);}}Console.WriteLine("数据获取成功!!!");}}
}控制台输出:
杨中科被评为亚洲最酷程序员
dbug: 2025/9/21 23:31:58.540 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)'MyDbContext' disposed.
数据获取成功!!!说明:此时没有输出Comment的内容。// 查看SQL语句
SELECT TOP(2) [t].[Id], [t].[Message], [t].[Title]FROM [T_Articles] AS [t]WHERE [t].[Id] = CAST(1 AS bigint)说明:SQL语句并没有LEFT JOIN语句,那么此时只是在T_Articles表里面查询数据了,并没有在T_Comments表里面查询。因此没有输出Comment的内容。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 在需要查询的关联字段上加上Include方法
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){Article a01 = ctx.Articles.Include(a => a.Comments).Single(a => a.Id == 1); // Include方法将关联的Comments的值也给查询出来Console.WriteLine(a01.Id);Console.WriteLine(a01.Title);foreach (Comment c in a01.Comments) // 遍历所有Comment{Console.WriteLine(c.Id + "," + c.Message);}}Console.WriteLine("数据获取成功!!!");}}
}控制台输出:
杨中科被评为亚洲最酷程序员
1,太牛了!!!
2,吹吧!!!
dbug: 2025/9/21 23:39:48.366 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)'MyDbContext' disposed.
数据获取成功!!!// 查看SQL语句SELECT [t0].[Id], [t0].[Message], [t0].[Title], [t1].[Id], [t1].[Message], [t1].[TheArticleId]FROM (SELECT TOP(2) [t].[Id], [t].[Message], [t].[Title]FROM [T_Articles] AS [t]WHERE [t].[Id] = CAST(1 AS bigint)) AS [t0]LEFT JOIN [T_Comments] AS [t1] ON [t0].[Id] = [t1].[TheArticleId]ORDER BY [t0].[Id], [t1].[Id]说明:此时有JOIN操作。
3.3 获取数据示例:【反向查 → 通过Comments(多)查Article(一)】
// Program.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){Comment cmt = ctx.Comments.Single(c => c.Id == 2);Console.WriteLine(cmt.Message);Console.WriteLine(cmt.TheArticle.Id + "," + cmt.TheArticle.Title);}Console.WriteLine("数据获取成功!!!");}}
}抛出异常:System.NullReferenceException:“Object reference not set to an instance of an object.”// 查看SQL语句
SELECT TOP(2) [t].[Id], [t].[Message], [t].[TheArticleId]FROM [T_Comments] AS [t]WHERE [t].[Id] = CAST(2 AS bigint)说明:此时没有INNER JOIN查询T_Articles表。因此此时TheArticle的值为null,进而抛出异常。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){Comment cmt = ctx.Comments.Include(c => c.TheArticle).Single(c => c.Id == 2); // Include方法Console.WriteLine(cmt.Message);Console.WriteLine(cmt.TheArticle.Id + "," + cmt.TheArticle.Title);}Console.WriteLine("数据获取成功!!!");}}
}控制台输出:
吹吧!!!
1,杨中科被评为亚洲最酷程序员
dbug: 2025/9/21 23:48:35.193 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)'MyDbContext' disposed.
数据获取成功!!!// 查看SQL语句SELECT TOP(2) [t].[Id], [t].[Message], [t].[TheArticleId], [t0].[Id], [t0].[Message], [t0].[Title]FROM [T_Comments] AS [t]INNER JOIN [T_Articles] AS [t0] ON [t].[TheArticleId] = [t0].[Id]WHERE [t].[Id] = CAST(2 AS bigint)
4. 通过EF Core设置额外的外键字段(视频3-16)
4.1 数据库优化原则
1. 数据库中尽量不要select * from t,这样会获取表中所有的值。
2. 推荐用select id, name from t,获取针对性的数据,这样性能更高。
4.2 直接rticle数据表第一条数据
// 形式1:获取当前列所有数据
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var a01 = ctx.Articles.First();Console.WriteLine(a01.Title + "," + a01.Id);}Console.WriteLine("数据获取成功!!!");}}
}控制台输出:
杨中科被评为亚洲最酷程序员
dbug: 2025/9/22 20:48:58.930 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)'MyDbContext' disposed.
数据获取成功!!!// 查看SQL语句
SELECT TOP(1) [t].[Id], [t].[Message], [t].[Title]FROM [T_Articles] AS [t]说明:我只想获取Title和Id,不想获取Message,但是EF Core生成的SQL语句也获取Message了。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 形式2:获取当前列所有数据
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var a02 = ctx.Articles.Select(a => a).First(); // 通过匿名类型来获取。Console.WriteLine(a02.Title + "," + a02.Id);}Console.WriteLine("数据获取成功!!!");}}
}控制台输出:
杨中科被评为亚洲最酷程序员,1
dbug: 2025/9/22 21:01:01.888 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)'MyDbContext' disposed.
数据获取成功!!!// 查看SQL语句SELECT TOP(1) [t].[Id], [t].[Message], [t].[Title]FROM [T_Articles] AS [t]
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:形式1和形式2获取形式等价
4.3 通过匿名类型获取Article表第一条数据
- 优化SQL语句形式1:通过匿名类型来查询
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var a02 = ctx.Articles.Select(a => new { a.Id, a.Title }).First(); // 通过匿名类型来获取。Console.WriteLine(a02.Title + "," + a02.Id);}Console.WriteLine("数据获取成功!!!");}}
}控制台输出:
杨中科被评为亚洲最酷程序员,1
dbug: 2025/9/22 20:55:50.718 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)'MyDbContext' disposed.
数据获取成功!!!// 查看SQL语句
SELECT TOP(1) [t].[Id], [t].[Title]FROM [T_Articles] AS [t]说明:
1. 通过匿名类型获取,此时没有查询Message信息了。只取出来了Title和Id。
2. 通过匿名类型获取指定的数据,是数据库优化的一种形式。
4.4 通过额外外键获取数据
- 适用于只获取外键Id,不获取外键形式。为了避免性能损失(不JOIN另外一个表),可以增加一个额外的外键属性。
说明:
1、EF Core会在数据表中建外键列。
2、如果需要获取外键列的值,就需要做关联查询,效率低。在4.2中已经试了。
3、需要一种不需要Join直接获取外键列的值的方式。解决方法:
1、在实体类中显式声明一个外键属性。
2、关系配置中通过HasForeignKey(c=>c.ArticleId)指定这个属性为外键。
3、除非必要,否则不用声明,因为会引入重复。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// step1:单独给Comment.cs实体类增加一个属性TheArticleId。
namespace OneToMany
{class Comment // 评论{public long Id { get; set; }public Article TheArticle { get; set; }public string Message { get; set; }public long TheArticleId { get; set; } // 实体类增加一个外键属性(同数据库中自动生成的TheArticleId外键相同)}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// step2:在CommentConfig.cs配置类中指定TheArticleId属性为外键列。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace OneToMany
{class CommentConfig : IEntityTypeConfiguration<Comment>{public void Configure(EntityTypeBuilder<Comment> builder){builder.ToTable("T_Comments");builder.Property(c => c.Message).HasMaxLength(100).IsUnicode().IsRequired();builder.HasOne<Article>(a => a.TheArticle).WithMany(c => c.Comments).HasForeignKey(c => c.TheArticleId).IsRequired(); // 声明外键列}}
}
说明:
1. 这里必须声明外键列,否则会在数据库中生成TheArticleId和TheArticleId1两个列。
2. 注意,这里并没有修改数据库,因此不用再程序包管理器中重新使用migration和update-database迁移数据库。当然,执行这两个语句也可以,数据库不会有变化。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var cmt = ctx.Comments.Single(c => c.Id == 1);Console.WriteLine(cmt.Id + "," + cmt.TheArticleId);}Console.WriteLine("数据获取成功!!!");}}
}控制台输出:
1,1
dbug: 2025/9/22 21:20:43.001 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)'MyDbContext' disposed.
数据获取成功!!!// 查看SQL语句
SELECT TOP(2) [t].[Id], [t].[Message], [t].[TheArticleId]FROM [T_Comments] AS [t]WHERE [t].[Id] = CAST(1 AS bigint)说明:
1. 此时只查询了Comment这个表,没有JOIN查询Article这个表。只查询单表,这样效率更高。
2. 这种形式知道即可,非必要不要使用。因为会额外增加实体类的属性和增加配置,增加了不确定性。
5. EF Core性能怎么样?
- 使用EF Core查询比绝大部分程序员写出来的SQL性能都要高。
- EF Core有少部分SQL语句性能可能不尽如人意,但是也影响不大。
- 特殊的一些SQL语句可能影响性能瓶颈,咱们在要特殊优化。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※