上次老周扯了有关主、从实体的话题,本篇咱们再挖一下,主、从实体之间建立的关系,跟咱们常用的一对一、一对多这些关系之间有什么不同。

先看看咱们从学习数据库开始就特熟悉的常用关系——多对多、一对一、一对多说起。数据实体之间会建立什么样的关系,并不是规则性的,而是要看数据的功能。比如你家养的狗狗和水果(你家狗狗可能不吃水果,但老周养的动物基本是什么都吃的,因为从它们幼年起,老周就训练它们,对食物要来者不拒,就算哪天它们不想跟着老周混,出去流浪也不会饿死,适应性更强)。

假设:

1、你的数据是以狗狗为主,那么一条狗狗会吃多种水果。即狗狗对水果是一对多;

2、你的数据以水果为主,每种水果单独记录,然后在另一个表中记录水果被哪几条狗喜欢。例:雪梨,狗Y和狗B都喜欢吃。于是水果对狗狗也可以是一对多的关系。

再假设你有个幼儿园学生尿床登记表,表中记录每次尿床的时间、床号等。每一条尿床记录都有一个字段,引用自学生表,代表是哪们同学尿床了。多条尿床记录可能都是同一个人的,比如,小明一周有三次尿床。这样,尿床记录和学生之间可以是多对一关系了。

数据是为咱们人服务的,因此实体之间建立什么样的关系,得看咱们人类是怎么理解,以及这些实体的用途。

还是用上一篇水文中的学生 - 作业的例子。

  1. public class Student
  2. {
  3. // 主键:学生ID
  4. public int StuID { get; set; }
  5. // 学生姓名
  6. public string? Name { get; set; }
  7. // 年级
  8. public ushort Grade { get; set; }
  9. // 作业(导航属性)
  10. public IEnumerable<Homework> Homeworks { get; set; } = new List<Homework>();
  11. }
  12.  
  13. public class Homework
  14. {
  15. // 主键,ID
  16. public int WorkID { get; set; }
  17. // 作业描述
  18. public string? Description { get; set; }
  19. // 科目(导航属性)
  20. public Subject? Subject { get; set; }
  21. // 引用学生对象
  22. public Student? Student { get; set; }
  23. }
  24.  
  25. public class Subject
  26. {
  27. // 主键:科目ID
  28. public int SubID { get; set; }
  29. // 科目名称
  30. public string? Name { get; set; }
  31. }

这次老周加了个实体——Subject,它表示作业的科目(数学、语文等)。

导航属性是用于建立实体关系的。

1、学生类中,Homeworks 属性建立与 Homework 对象的关系:一条学生信息可以对应多条作业信息,是一对多的关系;

2、作业类中,Subject 属性建立与 Subject 对象的关系。一对一的关系。

在 DbContext 的自定义类型中,三个实体间的关系配置如下:

  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3. // 设置主键
  4. modelBuilder.Entity<Student>().HasKey(s => s.StuID);
  5. modelBuilder.Entity<Homework>().HasKey(w => w.WorkID);
  6. modelBuilder.Entity<Subject>().HasKey(u => u.SubID);
  7. // 建立模型关系
  8. modelBuilder.Entity<Student>().HasMany(s => s.Homeworks).WithOne(w => w.Student);
  9. modelBuilder.Entity<Homework>().HasOne(w => w.Subject);
  10. }

这是咱们常规的关系配置方法,从当前实体到另一实体的关系描述为 HasXXX 方法;HasXXX 方法调用后,会顺带调用一个 WithXXX 方法。WithXXX 方法是反向描述,即描述另一个实体与当前实体的关系。这样调用可以建立比较完整的相对关系。

在上述代码中,Student -> Homework 是一对多,所以,Student 实体上调用 HasMany 方法;之后是反向关系,Homework -> Student 是一对一关系,也就是说,一条 Homework 记录通过外键只引用一条学生记录。因此调用了 WithOne 方法。

Homework -> Subject 是一对一,所以在 Homework 实体上调用 HasOne 方法。这里,Homework 与 Subject 两实体并没有建立相互引用的关系,仅仅是作业中引用了科目信息,而 Subject 实体自身可以独立,它不需要引用 Homework 的任何实例,因此没有调用 WithXXX 方法。

由于实体之间建立的关系是相对的,即参照当前对象。所以,上面代码也可以这样写:

  1. modelBuilder.Entity<Homework>().HasOne(h => h.Student).WithMany(s => s.Homeworks);
  2. modelBuilder.Entity<Homework>().HasOne(h => h.Subject);

要注意的是,这两种关系配置其实是相同的,所以两者任选一即可,不要重复配置。

两种关系配置的差别就在选择谁来做“当前实体”,即以当前实体为参照而建立相对关系。第二种方法是以 Homework 实体为当前实体,一条作业信息只关联一位学生,所以是一对一,调用 HasOne 方法;反过来,一条学生信息可包含多条作业信息,所以是一对多,即调用 WithMany 方法。

定义几个静态方法,用于验证模型建得对不对。

首先,InitDatabase 方法负责运行阶段创建数据库,并插入一些测试数据。

  1. static void InitDatabase()
  2. {
  3. using MyContext cxt = new();
  4. // 确保数据已创建
  5. bool v = cxt.Database.EnsureCreated();
  6. // 如果数据库已存在,不用初始化数据
  7. if (!v)
  8. return;
  9. /* 初始化数据 */
  10. // 这是科目
  11. Subject s1 = new(){ Name = "语文"};
  12. Subject s2 = new(){ Name = "数学"};
  13. Subject s3 = new(){ Name = "英语"};
  14. Subject s4 = new(){ Name = "物理"};
  15. Subject s5 = new(){ Name = "地理"};
  16. cxt.Subjects.AddRange(new[]{
  17. s1, s2, s3, s4, s5
  18. });
  19. // 学生和作业可以一起添加
  20. cxt.Students.Add(
  21. new Student{
  22. Name = "小华",
  23. Grade = 4,
  24. Homeworks = new []
  25. {
  26. new Homework
  27. {
  28. Description = "背单词3500个",
  29. Subject = s3
  30. },
  31. new Homework
  32. {
  33. Description = "作文《我是谁,我在哪里》",
  34. Subject = s1
  35. },
  36. new Homework
  37. {
  38. Description = "手绘广州地铁网络图",
  39. Subject = s5
  40. }
  41. }
  42. }
  43. );
  44. cxt.Students.Add(
  45. new Student
  46. {
  47. Name = "王双喜",
  48. Grade = 3,
  49. Homeworks = new[] {
  50. new Homework
  51. {
  52. Description = "完型填空练习",
  53. Subject = s3
  54. }
  55. }
  56. }
  57. );
  58. cxt.Students.Add(
  59. new Student
  60. {
  61. Name = "割麦小王子",
  62. Grade = 5,
  63. Homeworks = new[]{
  64. new Homework
  65. {
  66. Description = "实验:用激光给蟑螂美容",
  67. Subject = s4
  68. },
  69. new Homework{
  70. Description = "翻译文言文《醉驾通鉴》",
  71. Subject = s1
  72. }
  73. }
  74. }
  75. );
  76. // 保存到数据库
  77. cxt.SaveChanges();
  78. }

SaveChanges 方法记得调用,调用了才会保存数据。

ShowData 方法负责在控制台打印数据。

  1. static void ShowData()
  2. {
  3. using MyContext ctx = new();
  4. var students = ctx.Students.Include(s => s.Homeworks)
  5. .ThenInclude(hw => hw.Subject)
  6. .AsEnumerable();
  7. // 打印学生信息
  8. Console.WriteLine("{0,-5}{1,-10}{2,-6}", "学号", "姓名", "年级");
  9. Console.WriteLine("----------------------------------------------------");
  10. foreach(var stu in students)
  11. {
  12. Console.WriteLine($"{stu.StuID,-7}{stu.Name,-10}{stu.Grade,-4}");
  13. // 打印作业信息
  14. foreach(Homework wk in stu.Homeworks)
  15. {
  16. Console.Write(">> {0,-4}", wk.Subject!.Name);
  17. Console.WriteLine(wk.Description);
  18. }
  19. Console.Write("\n\n");
  20. }
  21. }

在加载数据时得小心,因为如果你只访问 Students 集合,那么,Homeworks 和 Subjects 集合不会加载,这会使得 Student 实体的 Homeworks 属性变为空。为了让访问 Students 集合时同时加载关联的数据,要用 Include 方法。

第一个 Include 方法加载 Homeworks 属性引用的 Homework对象;第二个ThenInclude 方法是指在加载 Homework 后,Homework 实体的 Subject 属性引用了 Subject 对象,所以 ThenInclude 方法是通知模型顺便加载 Subjects 集合。

最后,要调用一下实际触发查询的方法,如 AsEnumerable 方法,这样才会让查询执行,你在内存中才能访问到数据。当然,像 ToArray、ToList 之类的方法也可以,这个和 LINQ 语句的情况类似。要调用到相应的方法才触发查询真正执行。

RemoveDatabase 方法是可选的,删除数据库。咱们这是演示,免得在数据库中存太多不必要的东西。测试完代码可以调用一下它,删除数据库。这里老周照例用 SQL Server LocalDB 来演示。

  1. static void RemoveDatabase()
  2. {
  3. using MyContext c = new();
  4. c.Database.EnsureDeleted();
  5. }

-------------------------------------------------------------------------------------------

用的时候,按顺调用这些方法,就可以测试了。

  1. Console.WriteLine("** 第一步:初始化数据库。【请按任意键继续】");
  2. _ = Console.ReadKey(true);
  3. InitDatabase();
  4.  
  5. Console.WriteLine("** 第二步:显示数据。【请按任意键继续】");
  6. _ = Console.ReadKey(true);
  7. ShowData();
  8.  
  9. //Console.WriteLine("** 第三步:删除数据库。【请按任意键继续】");
  10. //_ = Console.ReadKey();
  11. //RemoveDatabase();

产生的数据表如下图所示:

我们上面的这个模型还是有点问题的,可以看一下,生成的数据表是没有删除约束的。

  1. CREATE TABLE [dbo].[Homeworks] (
  2. [WorkID] INT IDENTITY (1, 1) NOT NULL,
  3. [Description] NVARCHAR (MAX) NULL,
  4. [SubjectSubID] INT NULL,
  5. [StudentStuID] INT NULL,
  6. CONSTRAINT [PK_Homeworks] PRIMARY KEY CLUSTERED ([WorkID] ASC),
  7. CONSTRAINT [FK_Homeworks_Students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[Students] ([StuID]),
  8. CONSTRAINT [FK_Homeworks_Subjects_SubjectSubID] FOREIGN KEY ([SubjectSubID]) REFERENCES [dbo].[Subjects] ([SubID])
  9. );

假如现在我要删掉一条学生记录。

  1. using(MyContext dbcontext = new())
  2. {
  3. // 删第一条记录
  4. var one = dbcontext.Students.FirstOrDefault();
  5. if(one != null)
  6. {
  7. dbcontext.Students.Remove(one);
  8. dbcontext.SaveChanges();
  9. }
  10. }

但删除的时候会遇到错误。

这表明咱们要配置级联删除。

  1. public class MyContext : DbContext
  2. {
  3. public DbSet<Student> Students => Set<Student>();
  4. public DbSet<Homework> Homeworks => Set<Homework>();
  5. public DbSet<Subject> Subjects => Set<Subject>();
  6.  
  7. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  8. {
  9. optionsBuilder.UseSqlServer(@"server=(localdb)\MSSQLLocalDB;Database=TestDB;Integrated Security=True");
  10. }
  11.  
  12. protected override void OnModelCreating(ModelBuilder modelBuilder)
  13. {
  14. ……
  15. // 建立模型关系
  16. modelBuilder.Entity<Student>()
  17. .HasMany(s => s.Homeworks)
  18. .WithOne(w => w.Student)
  19. .OnDelete(DeleteBehavior.Cascade);
  20. modelBuilder.Entity<Homework>().HasOne(w => w.Subject);
  21. }
  22. }

现在再删一次看看。

可以看到,与第一位学生有关的作业记录也一并被删除了。生成的数据表也与前面有一点差异。

  1. CREATE TABLE [dbo].[Homeworks] (
  2. [WorkID] INT IDENTITY (1, 1) NOT NULL,
  3. [Description] NVARCHAR (MAX) NULL,
  4. [SubjectSubID] INT NULL,
  5. [StudentStuID] INT NULL,
  6. CONSTRAINT [PK_Homeworks] PRIMARY KEY CLUSTERED ([WorkID] ASC),
  7. CONSTRAINT [FK_Homeworks_Students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[Students] ([StuID]) ON DELETE CASCADE,
  8. CONSTRAINT [FK_Homeworks_Subjects_SubjectSubID] FOREIGN KEY ([SubjectSubID]) REFERENCES [dbo].[Subjects] ([SubID])
  9. );

约束里面显然多了 ON DELETE CASCADE 语句。

回忆一下,在上一篇水文中,咱们使用主从对象后,我们在模型中没有明确配置级联删除,但生成的数据表中自动加上级联删除了。

这是不是说明:主从关系的实体对象里,主实体对从属实体的控制更强烈,咱们再对比对比看。

现在,让 Student 和 Homework 成为主从关系。

  1. public class MyContext : DbContext
  2. {
  3. public DbSet<Student> Students => Set<Student>();
  4. public DbSet<Homework> Homeworks => Set<Homework>();
  5. public DbSet<Subject> Subjects => Set<Subject>();
  6.  
  7. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  8. {
  9. ……
  10. }
  11.  
  12. protected override void OnModelCreating(ModelBuilder modelBuilder)
  13. {
  14. // 设置主键
  15. modelBuilder.Entity<Student>().HasKey(s => s.StuID);
  16. modelBuilder.Entity<Subject>().HasKey(u => u.SubID);
  17. // 建立模型关系
  18. modelBuilder.Entity<Student>()
  19. .OwnsMany(s => s.Homeworks, mrb =>
  20. {
  21. mrb.WithOwner(w => w.Student);
  22. mrb.HasKey(w => w.WorkID);
  23. mrb.HasOne(w => w.Subject);
  24. });
  25.  
  26. }
  27. }

上次我们也证实过,凡成为从属的实体是无法单独进行配置的(如主键等),只能在配置主从关系的时候通过 OwnsMany 方法的委托来配置。

主从关系会自动生成级联删除语句。

  1. CREATE TABLE [dbo].[Homeworks] (
  2. ……,
  3. CONSTRAINT [PK_Homeworks] PRIMARY KEY CLUSTERED ([WorkID] ASC),
  4. CONSTRAINT [FK_Homeworks_Students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[Students] ([StuID]) ON DELETE CASCADE,
  5. ……
  6. );

还有一点更关键的,Homework 成为 Student 的从对象后,你甚至无法直接访问 Homeworks 集合,必须通过 Sudents 集合来访问。

  1. using (MyContext ctx = new MyContext())
  2. {
  3. foreach(Homework hw in ctx.Homeworks)
  4. {
  5. Console.WriteLine($"{hw.Description}");
  6. }
  7. }

上述代码会抛异常。

这很明了,就是说你必须通过 Student 实体才能访问 Homework。所以,正确的做法要这样:

  1. using (MyContext ctx = new MyContext())
  2. {
  3. ctx.Subjects.Load(); // 这个可不会自动加载,必须Load
  4. foreach(Student stu in ctx.Students)
  5. {
  6. Console.WriteLine("【{0}】同学", stu.Name);
  7. foreach(Homework work in stu.Homeworks)
  8. {
  9. Console.WriteLine(" {0}:{1}", work.Subject?.Name, work.Description);
  10. }
  11. }
  12. }

Subjects 集合为什么要显式地调用 Load 方法呢?因为 Homework 与 Subject 实体并没有建立主从关系,Subject 对象要手动加载。

这样访问就不出错了。

-----------------------------------------------------------------------------------

最后,咱们来总结一下:

1、普通关系的数据未自动加载,要显式Load,或者 Include 方法加载。主从关系会自动加载从属数据;

2、建立主从关系后,主实体对从实体是完全控制了,不仅自动生成级联删除等约束,而且你还不能直接访问从实体,只能透过主实体访问;普通关系的实体需要手动配置约束。

========================================================

下面是老周讲故事时间。

上大学的时候,在《程序员》杂志上看过一句很“权威”的话:程序员是世上最有尊严的职业,不用酒局饭局,不用看人脸色,想干啥干啥,自由得很。然而,“多年以后一场大雨惊醒沉睡的我,突然之间都市的霓虹都不再闪烁”。客户说需求要这样这样,你改不改?改完之后客户又说还是改回那样那样,你改不改?总奸,哦不,总监说要这样这样,你能那样那样吗?客户说:“我们希望增加XXX功能,最好可以分开YYY、KKK 来管理。这些对你们来很简单的,动动鼠标就好了嘛!” 你动动鼠标试试?

再说了,哪个公司哪个单位的领导不是酒囊饭袋?IT 公司没有吗?哪儿都有,这世界最不缺的就是酒囊饭袋,最缺的是成吉思汗。

所以说,最TM自由、耍得最爽的就写博客,爱写啥写啥,套用土杰伦的歌词就是“你爱看就看,不爱看拉倒”。至于码农,就如同被压迫数千年的农民一样,没本质区别。所以,我们在给后辈讲码农生涯时,千万不要给他们画大饼,充不了饥。我们更应该教会他们程序员的最基本职业道德—— sudo rm -rf /*。

【EF Core】主从实体关系与常见实体关系的区别的更多相关文章

  1. EF Core中Key属性相同的实体只能被跟踪(track)一次

    在EF Core的DbContext中,我们可以通过DbContext或DbSet的Attach方法,来让DbContext上下文来跟踪(track)一个实体对象,假设现在我们有User实体对象,其U ...

  2. EF Core中怎么实现自动更新实体的属性值到数据库

    我们在开发系统的时候,经常会遇到这种需求数据库表中的行被更新时需要自动更新某些列. 数据库 比如下面的Person表有一列UpdateTime,这列数据要求在行被更新后自动更新为系统的当前时间. Pe ...

  3. EF Core反向导航属性解决多对一关系

    多对一是一种很常见的关系,例如:一个班级有一个学生集合属性,同时,班级有班长.语文课代表.数学课代表等单个学生属性,如果定义2个实体类,班级SchoolClass和学生Student,那么,班级Sch ...

  4. EF Core 2.0 已经支持自动生成父子关系表的实体

    现在我们在SQL Server数据库中有Person表如下: CREATE TABLE [dbo].[Person]( ,) NOT NULL, ) NULL, ) NULL, ) NULL, [Cr ...

  5. asp.net core系列 28 EF模型配置(字段,构造函数,拥有实体类型)

    一. 支持字段 EF允许读取或写入字段而不是一个属性.在使用实体类时,用面向对象的封装来限制或增强应用程序代码对数据访问的语义时,这可能很有用.无法使用数据注释配置.除了约定,还可以使用Fluent ...

  6. EF Core 中多次从数据库查询实体数据,DbContext跟踪实体的情况

    使用EF Core时,如果多次从数据库中查询一个表的同一行数据,DbContext中跟踪(track)的实体到底有几个呢?我们下面就分情况讨论下. 数据库 首先我们的数据库中有一个Person表,其建 ...

  7. EF Core 快速上手——EF Core的三种主要关系类型

    系列文章 EF Core 快速上手--EF Core 入门 本节导航 三种数据库关系类型建模 Migration方式创建和习修改数据库 定义和创建应用DbContext 将复杂查询拆分为子查询   本 ...

  8. 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获

    项目开发中的一些注意事项以及技巧总结   1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...

  9. EF Core 的关联查询

    0 前言 本文会列举出 EF Core 关联查询的方法: 在第一.二.三节中,介绍的是 EF Core 的基本能力,在实体中配置好关系,即可使用,且其使用方式,与编程思维吻合,是本文推荐的方式. 第四 ...

  10. [翻译 EF Core in Action 2.4] 加载相关数据

    Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...

随机推荐

  1. Java学习笔记08

    1. static关键字 ​ static可以用来修饰的成员变量和成员方法,被static修饰的成员是属于类的是放在静态区中,没有static修饰的成员变量和方法则是属于对象的. 1.1 静态变量 ​ ...

  2. Springboot整合Flowable6.x导出bpmn20

    项目源码仓库 BPMN2.0(Business Process Model and Notation)是一套业务流程模型与符号建模标准,以XML为载体,以符号可视化业务,支持精准的执行语义来描述元素的 ...

  3. MyQR--生成个性二维码

    1.二维码定义: 二维码(2-Dimensional Bar Code),是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的.它是指在一维条码的基础上扩展出另一 ...

  4. ROS机器人校正

    vROS机器人IMU自动校正 连接小车 注意:必须在同一区域网 ssh clbrobort@clbrobort 激活树莓派主板 roslaunch clbrobot bringup.launch 自动 ...

  5. Jquery实现复选框的选中和取消

    复选框的选中与取消 我在网上看了好多关于这个问题的解答,好多都是一两个按钮的触发事件,有的甚至没有任何效果,经过自己的调试发现这个方法好用一点: 首先我在页面上添加了这样一个复选框 我的复选框是动态加 ...

  6. [人脸活体检测] 论文:Face De-Spoofing: Anti-Spoofing via Noise Modeling

    Face De-Spoofing: Anti-Spoofing via Noise Modeling 论文简介 将非活体人脸图看成是加了噪声后失真的x,用残差的思路检测该噪声从而完成分类. 文章引用量 ...

  7. 如何实现Spring中服务关闭时对象销毁执行代码

    spring提供了两种方式用于实现对象销毁时去执行操作 1.实现DisposableBean接口的destroy 2.在bean类的方法上增加@PreDestroy方法,那么这个方法会在Disposa ...

  8. 从浏览器输入域名开始分析DNS解析过程

    摘要:DNS(Domain Name System)是域名系统的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,用于 TCP/IP 网络. 本文分享自华为云社区<DNS那些事--从浏 ...

  9. 2022-07-27:小红拿到了一个长度为N的数组arr,她准备只进行一次修改, 可以将数组中任意一个数arr[i],修改为不大于P的正数(修改后的数必须和原数不同), 并使得所有数之和为X的倍数。

    2022-07-27:小红拿到了一个长度为N的数组arr,她准备只进行一次修改, 可以将数组中任意一个数arr[i],修改为不大于P的正数(修改后的数必须和原数不同), 并使得所有数之和为X的倍数. ...

  10. 2021-04-27:如果一个字符相邻的位置没有相同字符,那么这个位置的字符出现不能被消掉。比如:“ab“,其中a和b都不能被消掉 。如果一个字符相邻的位置有相同字符,就可以一起消掉。比如:“abbb

    2021-04-27:如果一个字符相邻的位置没有相同字符,那么这个位置的字符出现不能被消掉.比如:"ab",其中a和b都不能被消掉 .如果一个字符相邻的位置有相同字符,就可以一起消 ...