《ASP.NET Core技术内幕与项目实战》精简集-EFCore2.3:导航关系配置(一对多、一对一、多对多)

本节内容,涉及4.6(P100-P114)。主要NuGet包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

 

一、一对多关系-双向导航(主从表)

  1 //实体类Article和Comment,在数据库上是主子表或主从表关系,在EFCore中称之为一对多
  2 //Article.cs和Comment.cs
  3 public class Article
  4 {
  5     public long Id { get; set; }
  6     public string? Title { get; set; }
  7     public string? Content { get; set; }
  8     public List<Comment> Comments { get; set; } = new List<Comment>(); //导航属性,指向多端
  9 
 10 }
 11 
 12 public class Comment
 13 {
 14     public long ID { get; set; }
 15     public Article? Article { get; set; } //导航属性,指向一端
 16     public long ArticleId { get; set; } //显式设置外键属性
 17     public string? Message { get; set; }
 18 }
 19 
 20 
 21 
 22 //创建DbContext类,并配置实体和数据表的映射关系
 23 //MyDbContext.cs
 24 public class MyDbContext: DbContext
 25 {
 26     public DbSet<Article> Articles { get; set; }
 27     public DbSet<Comment> Comments { get; set; }
 28     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 29     {
 30         base.OnConfiguring(optionsBuilder);
 31         string connStr = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TryRelation;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";
 32         optionsBuilder.UseSqlServer(connStr);
 33     }
 34 
 35     protected override void OnModelCreating(ModelBuilder modelBuilder)
 36     {
 37         base.OnModelCreating(modelBuilder);
 38 
 39         //配置Article的映射关系
 40         modelBuilder.Entity<Article>(b =>
 41         {
 42             b.ToTable("T_Articles");
 43             b.Property(a => a.Title).HasMaxLength(50).IsRequired().IsUnicode();
 44             b.Property(a => a.Content).HasMaxLength(500).IsRequired().IsUnicode();
 45         });
 46 
 47         //配置Comment的映射关系及导航关系
 48         modelBuilder.Entity<Comment>(b =>
 49         {
 50             b.ToTable("T_Comments");
 51             b.Property(c=>c.Message).HasMaxLength(500).IsRequired().IsUnicode();
 52             //配置导航关系,并显式配置外键
 53             b.HasOne<Article>(c => c.Article).WithMany(a => a.Comments).HasForeignKey(c => c.ArticleId);
 54         });
 55     }
 56 }
 57 
 58 
 59 
 60 //数据库迁移,在工具-Nuget包管理器-程序包管理控制台中,先后执行:
 61 Add-Migration init
 62 Update-database
 63 
 64 
 65 
 66 //在Program中创建DbContext对象,并进行新增和查询操作
 67 //创建DbContext对象
 68 using var ctx = new MyDbContext();
 69 
 70 //一对多的数据新增方式1
 71 var a1 = new Article { Title = "文章标题1", Content = "文章内容1" };
 72 
 73 var c1 = new Comment { Message = "评论内容1" };
 74 var c2 = new Comment { Message = "评论内容2" };
 75 var c3 = new Comment { Message = "评论内容3" };
 76 
 77 a1.Comments.Add(c1);
 78 a1.Comments.Add(c2);
 79 a1.Comments.Add(c3);
 80 
 81 ctx.Articles.Add(a1);
 82 await ctx.SaveChangesAsync();
 83 
 84 
 85 //一对多的数据新增方式2
 86 var a2 = new Article { Title = "文章标题2", Content = "文章内容2" };
 87 
 88 var c1 = new Comment { Message = "评论内容1", Article = a2 };
 89 var c2 = new Comment { Message = "评论内容2", Article = a2 };
 90 var c3 = new Comment { Message = "评论内容3", Article = a2 };
 91 
 92 ctx.Comments.Add(c1);
 93 ctx.Comments.Add(c2);
 94 ctx.Comments.Add(c3);
 95 await ctx.SaveChangesAsync();
 96 
 97 
 98 //一对多的关联数据一起获取
 99 var article1 = ctx.Articles.Include(a => a.Comments).Single(a=>a.Id == 1);
100 Console.WriteLine(article1.Title);
101 foreach (var item in article1.Comments)
102 {
103     Console.WriteLine($"{item.ID}:{item.Message}");
104 }
105 
106 
107 //如果未设置外键属性ArticleId,获取Comment的ArticleId,需要使用Include
108 foreach (var item in ctx.Comments.Include(c=>c.Article))
109 {
110     Console.WriteLine($"{item.ID}:{item.Message};{item.Article!.Id}");
111 }
112 //因本案例配置了外键属性,所以可以不使用Include
113 foreach (var item in ctx.Comments)
114 {
115     Console.WriteLine($"{item.ID}:{item.Message};{item.ArticleId}");
116 }

代码解读:

8行:在一端,设置指向多端的导航属性

15行:在多端,设置指向一端的导航属性

16行:显式的创建了外键属性。可以省略,如省略,数据库T_Comments表中,一样会创建外键字段ArticleId,只是实体Comment没有ArticleId属性。如果要获得,需要使用Include查询,见下例。建议显式添加。

53行:配置一对多的导航关系,在一端和多端均可以设置,建议在多端设置,如上例所示。HasOne<T>().WithMany(),Has的主语为当前实体(即Comment),泛型T可以省略。With的主语为导航过去实体。

53行:显式配置实体的外键属性,除了在多端添加外键属性外,还需要在导航关系配置中,显示的设置外键

99行,108行:查询主表时,要关系从表;或查询从表时,要关系主表,使用Include

 

 

二、一对多关系-单向导航

 1 //实体类,一端为User-用户,多端为Leave-离职申请
 2 //User.cs和Leave.cs
 3 public class User
 4 {
 5     public long Id { get; set; }
 6     public string? Name { get; set; }
 7 }
 8 
 9 public class Leave
10 {
11     public long Id { get; set; }
12     public User Requester { get; set; } //申请人
13     public User? Approver { get; set; } //审批人
14     public DateTime From { get; set; }
15     public DateTime To { get; set; }
16 }
17 
18 
19 //DbContext类,配置映射和一对多的单向导航关系
20 public class MyDbContext: DbContext
21 {
22     public DbSet<User> Users { get; set; }
23     public DbSet<Leave> Leaves { get; set; }
24 
25     protected override void OnModelCreating(ModelBuilder modelBuilder)
26     {
27         //配置User的映射关系
28         modelBuilder.Entity<User>(b =>
29         {
30             b.ToTable("T_Users");
31             b.Property(u => u.Name).IsRequired().HasMaxLength(100).IsUnicode();
32         });
33 
34         //配置Leave的映射关系及一对多单向导航
35         modelBuilder.Entity<Leave>(b =>
36         {
37             b.ToTable("T_Leaves");
38             b.HasOne<User>(l => l.Requester).WithMany(); //设置申请人的单向导航
39             b.HasOne<User>(l => l.Approver).WithMany(); //设置审批人的单向导航
40         });
41      }
42 }
43 
44 
45 //新增数据和查询
46 //Program.cs
47 var u1 = new User { Name = "张三" };
48 var l1 = new Leave { Requester = u1, From = new DateTime(2022, 10, 28), To = new DateTime(2022, 10, 30) };
49 
50 ctx.Users.Add(u1);
51 ctx.Leaves.Add(l1);
52 await ctx.SaveChangesAsync();
53 
54 var u = await ctx.Users.SingleAsync(u => u.Name == "张三");
55 foreach (var item in ctx.Leaves.Where(l => l.Requester == u))
56 {
57     Console.WriteLine(item.From.ToString());
58 }

代码解读:

38-39行:一对多关系配置,建议都在多端设置。单向导航与双向的差异,主要在WithMany端,因为一端不会指向多端,所以WithMany端留空

50-51行:一对多单向导航新增数据时,一端和多端,均需要Add,而双向导航,只要在一端或多端Add即可

54-55行:单向导航时,一端和多端的关联查询,不能使用Include

 

三、一对一关系,双向导航

 1 //实体类,订单和快递单,一对一关系,在业务流上有上下前后关系
 2 //Order.cs和Delivery.cs
 3 public class Order
 4 {
 5     public long Id { get; set; }
 6     public string? Name { get; set; } //品名
 7     public string? Address { get; set; } //收货地址
 8     public Delivery? Delivery { get; set; } //快递单
 9 }
10 
11 public class Delivery
12 {
13     public long Id { get; set; }
14     public string? Company { get; set; } //快递公司
15     public string? Number { get; set; } //快递单号
16     public Order Order { get; set; } //订单
17     public long OrderId { get; set; } //指向订单的外键
18 }
19 
20 
21 //DbContext类设置映射关系和一对一导航关系
22 public class MyDbContext: DbContext
23 {
24     public DbSet<Order> Orders { get; set; }
25     public DbSet<Delivery> Deliverys { get; set; }
26 
27     ......
28 
29     protected override void OnModelCreating(ModelBuilder modelBuilder)
30     {
31         //配置Leave的映射关系及一对多单向导航
32         modelBuilder.Entity<Leave>(b =>
33         {
34             b.ToTable("T_Leaves");
35             b.HasOne<User>(l => l.Requester).WithMany(); //设置申请人的单向导航
36             b.HasOne<User>(l => l.Approver).WithMany(); //设置审批人的单向导航
37         });
38 
39         //配置Order的映射关系
40         modelBuilder.Entity<Order>(b =>
41         {
42             b.ToTable("T_Orders");
43             b.Property(o => o.Name).IsUnicode();
44             b.Property(o => o.Address).IsUnicode();
45         });
46 
47         //配置Delivery的映射关系和一对一(双向)导航
48         modelBuilder.Entity<Delivery>(b =>
49         {
50             b.ToTable("T_Delivery");
51             b.Property(d => d.Company).HasMaxLength(10).IsUnicode();
52             b.Property(d => d.Number).HasMaxLength(50);
53             b.HasOne<Order>(d => d.Order).WithOne(l => l.Delivery).HasForeignKey<Delivery>(d=>d.OrderId);
54         });
55     }
56 }    
57 
58 
59 //一对一导航关系,新增和查询
60 //Program.cs
61 var o1 = new Order { Name = "笔记本电脑", Address = "某某市" };
62 var d1 = new Delivery { Company = "蜗牛快递", Number = "SN847534", Order = o1};
63 
64 ctx.Deliverys.Add(d1);
65 await ctx.SaveChangesAsync();
66 
67 var o = await ctx.Orders.Include(o=>o.Delivery).FirstAsync(o=>o.Name.Contains("电脑"));
68 Console.WriteLine($"品名:{o.Name},单号:{o.Delivery.Number}");

代码解读:

52行:设置一对一导航关系,可以设置在任何一端,建议设置在下游端。HasForeignKey和外键属性必须设置,注意HasForeignKey的泛型和参数类型。

64行:新增时,在任何一端Add均可,建议在下游端Add。

67行:关联查询,需要使用Include。

 

四、多对多关系,双向导航

 1 //实体类,老师和学生,多对多关系
 2 //Teacher.cs和Student.cs
 3 public class Teacher
 4 {
 5     public long Id { get; set; }
 6     public string? Name { get; set; }
 7     public List<Student> Students { get; set; } = new List<Student>(); //导航属性,多对多端
 8 }
 9 
10 public class Student
11 {
12     public long Id { get; set; }
13     public string? Name { get; set; }
14     public List<Teacher> Teachers { get; set; } = new List<Teacher>(); //导航属性,多对多端
15 }
16 
17 
18 //DbContext配置映射和多对多导航关系
19 //MyDbContext.cs
20 public class MyDbContext: DbContext
21 {
22     public DbSet<Student> Students { get; set; }
23     public DbSet<Teacher> Teachers { get; set; }
24     ......
25     protected override void OnModelCreating(ModelBuilder modelBuilder)
26     {
27         base.OnModelCreating(modelBuilder);
28 
29         //配置Teacher的映射关系
30         modelBuilder.Entity<Teacher>(b =>
31         {
32             b.ToTable("T_Teachers");
33             b.Property(t => t.Name).HasMaxLength(20).IsUnicode();
34         });
35 
36         //配置Teacher的映射关系和多对多导航关系
37         modelBuilder.Entity<Student>(b =>
38         {
39             b.ToTable("T_Students");
40             b.Property(s => s.Name).HasMaxLength(20).IsUnicode();
41             b.HasMany<Teacher>(s=>s.Teachers).WithMany(t=>t.Students)
42              .UsingEntity(j=>j.ToTable("T_Teachers_Students"));
43         });
44     }
45 }
46 
47 
48 //多对多关系进行关联新增
49 var t1 = new Teacher { Name = "MissFan" };
50 var t2 = new Teacher { Name = "MissLiu" };
51 var s1 = new Student { Name = "ZS" };
52 var s2 = new Student { Name = "LS" };
53 
54 t1.Students.Add(s1);
55 t1.Students.Add(s2);
56 t2.Students.Add(s1);
57 t2.Students.Add(s2);
58 
59 ctx.Students.AddRange(s1, s2);
60 ctx.Teachers.AddRange(t1, t2);
61 await ctx.SaveChangesAsync();
62 
63 
64 //多对多关系进行关联查询
65 foreach (var item in ctx.Teachers.Include(t=>t.Students))
66 {
67     Console.WriteLine($"老师:{item.Name}");
68     foreach (var student in item.Students)
69     {
70         Console.WriteLine($"------学生:{student.Name}");
71     }
72 }

代码解读:

7,14行:两个多端,均配置List<T>导航属性

41-42行:在任何一个多端均可以配置多对多导航关系。多对多,需要一张中间表建立两端关系,UsingEntity方法设置中间表的表名,如果不使用UsingEntity方法,则会自动创建一个中间表

59-60行:在两端均Add,使用了AddRange方法,仅是Add的语法糖,更容易写代码而已,编译为SQL时,还是一条条新增。

65行:查询使用Include,可见无论是一对多、一对一、或多对多,只要是双向导航,关联查询时均使用Include。

 

五、最后总结:

1、一对多,有双向导航和单向导航;一对一和多对多,均是双向导航

2、一对多、一对一和多对多的双向导航,两端的实体均要配置导航属性;一对多的单向导航,仅在多端设置导航属性

3、一对多配置导航映射关系时,在多端设置,HasOne<>(...).WithMany(...)。其中单向导航的WithMany方法参数为空

4、一对一配置导航映射关系,任何一端均可,建议在下游端

5、多对多配置导航映射关系,任何一端均可

6、关联新增时,有双向导航关系的,会顺竿爬,Add一端即可;单向导航关系的,两端都要Add。当然,无论单向或双向,两端都Add,均可以

7、关联查询时,有双向导航关系的,使用Include;单向导航关系的,不能使用Include

 

特别说明:
1、本系列内容主要基于杨中科老师的书籍《ASP.NET Core技术内幕与项目实战》及配套的B站视频视频教程,同时会增加极少部分的小知识点
2、本系列教程主要目的是提炼知识点,追求快准狠,以求快速复习,如果说书籍学习的效率是视频的2倍,那么“简读系列”应该做到再快3-5倍

 

posted @ 2022-10-29 01:06  functionMC  阅读(533)  评论(0编辑  收藏  举报