《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倍