EF Core一对多

步骤

1、实体类中关系属性
2、FluentAPI关系配置
3、使用关系操作

演示

目的:数据库保存文章与评论,一篇文章对应多条评论
1、实体类中关系属性

1
2
3
4
5
6
7
8
9
10
11
12
public class Article {
public int Id { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public List<Comment> Comments = new List<Comment> ();//实体类中关系属性
}
public class Comment {
public int Id { get; set; }
public string Message { get; set; }
public Article TheArticle { get; set; }//实体类中关系属性

}

2、FluentAPI关系配置

1
2
3
4
5
6
7
8
9
10
11
internal class CommentConfig : IEntityTypeConfiguration<Comment> {
public void Configure(EntityTypeBuilder<Comment> builder) {
builder.ToTable("T_Comments");
builder.HasOne<Article>(e => e.TheArticle).WithMany(e => e.Comments).IsRequired();//FluentAPI关系配置
}
}
public class ArticleConfig : IEntityTypeConfiguration<Article> {
public void Configure(EntityTypeBuilder<Article> builder) {
builder.ToTable("T_Articles");
}
}

3、使用关系操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
internal class Program {
static async Task Main(string[] args) {
using(var db = new MyDbContext()) {
Article article = new Article();
article.Title = "震惊!母猪为何深夜惨叫";
article.Message = "近日,家在石家庄村里的张老汉发现。。。。。。";
Comment com1 = new Comment();
Comment com2 = new Comment();
com1.Message = "标题党真讨厌";
com2.Message = "到底是人性的扭曲还是。。。。。。";
article.Comments.Add(com1);
article.Comments.Add(com2);
db.Articles.Add(article);
db.SaveChanges();
//有了HasXXX().WithXXX(),就相当于创建了外检关联,EF Core 会自动做好一些配置,不需要以下的写法了
//Article article = new Article();
//article.Title = "震惊!母猪为何深夜惨叫";
//article.Message = "近日,家在石家庄村里的张老汉发现。。。。。。";
//Comment com1 = new Comment();
//Comment com2 = new Comment();
//com1.Message = "标题党真讨厌";
//com2.Message = "到底是人性的扭曲还是。。。。。。";
//com1.TheArticle = article;
//com2.TheArticle= article;
//article.Comments.Add(com1);
//article.Comments.Add(com2);
//db.Articles.Add(article);
//db.Comments.Add(com1);
//db.Comments.Add(com2);
//db.SaveChanges();
}
}
}

不需要显式为Comment对象的Article属性赋值(赋值也不会错),也不需要显示地把新创建的Comment类型的对象添加到DbContext中。EF Core会“顺杆爬”。

一对多关系数据的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
internal class Program {
static void Main(string[] args) {
using(var db = new MyDbContext()) {
Article article1 = db.Articles.Single(e => e.Id == 1);
foreach (Comment cmt in article1.Comments) {
Console.WriteLine(cmt.Message);//这句话没有输出评论,因为生成的Sql语句为以下内容
//SELECT `t`.`Id`, `t`.`Message`, `t`.`Title` FROM `T_Articles` AS `t` WHERE `t`.`Id` = 1 LIMIT 2
//没有去Comments表中查数据
}
//解决方法如下,添加Include,表示与XXX表进行连接
Article article2 = db.Articles.Include(e => e.Comments).Single(e => e.Id == 1);
foreach (Comment cmt in article1.Comments) {
Console.WriteLine(cmt.Message);
}
Comment cmt1 = db.Comments.Single(e => e.Id == 2);
Console.WriteLine(cmt1.Message);
Console.WriteLine(cmt1.TheArticle.Id + "," + cmt1.TheArticle.Title);//错误,与上面同理,没有查询Article表
Comment cmt2 = db.Comments.Include(e => e.TheArticle).Single(e => e.Id == 2);
Console.WriteLine(cmt2.TheArticle.Id + "," + cmt2.TheArticle.Title);
}
}
}

如果不使用Include则不会去关联的表中进行查询。

额外的外键字段

EF Core会在数据表中建外键列,如果需要获取外键列的值,就需要做关联查询,效率低,因而需要一种不需要Join直接获取外键列的值的方法。
比如:
我就想获取评论对应文章的Id,但是我用了Include就会创建一个Join连接的Sql语句,这样没有必要。
解决方法:
1、在实体类中显示声明一个外键属性
2、关系配置中通过HasForeignKey(c=>c.ArticleId)显式指定该属性为外键。
3、除非必要,否则不用声明,因为会引入重复

1
2
3
4
5
6
7
public class Comment {
public int Id { get; set; }
public string Message { get; set; }
public Article TheArticle { get; set; }//实体类中关系属性
public int TheArticleId { get; set; }//显式指定外键

}
1
2
3
4
5
6
internal class CommentConfig : IEntityTypeConfiguration<Comment> {
public void Configure(EntityTypeBuilder<Comment> builder) {
builder.ToTable("T_Comments");
builder.HasOne<Article>(e => e.TheArticle).WithMany(e => e.Comments).HasForeignKey(e => e.TheArticleId).IsRequired();//FluentAPI关系配置,显式指定外键字段
}
}

Select函数会在必要的情况下自动进行连接查询,所以有了Select就可以不用Include函数了。

单向导航属性

双向导航属性的不足

如上文例子,每一条评论通过TheArticleId可以连接到T_Articles表,每一篇文章可以通过Comments连接到T_Commnets表,这样的导航称为单向导航。
如果有一个请假系统,有一个T_UserInfos表记录用户姓名、Id信息,有一个T_Leaves表记录用户请假信息比如:请假人、审批人1、审批人2、审批人3……我们就需要在T_UserInfos表中建立多个字段用来管理审批人123,这样很麻烦,为此我们需要单向导航属性

配置方法

不设置反向的属性,在配置的时候WithMany()不设置参数即可
实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Leave {
public long Id { get; set; }
public DateTime Time { get; set; }
public User Requester { get; set; }//申请人
public User Approver1 { get; set; }//审批人1
public User Approver2 { get; set; }//审批人2
public User Approver3 { get; set; }//审批人3
public string Remarks { get; set; }
}
public class User {
public long Id { get; set; }
public string Name { get; set; }
}

配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserConfig : IEntityTypeConfiguration<User> {
public void Configure(EntityTypeBuilder<User> builder) {
builder.ToTable("T_Users");
}
}
public class LeaveConfig : IEntityTypeConfiguration<Leave> {
public void Configure(EntityTypeBuilder<Leave> builder) {
builder.ToTable("T_Leaves");
builder.HasOne<User>(l=>l.Requester).WithMany().IsRequired();
builder.HasOne<User>(l=>l.Approver1).WithMany();
builder.HasOne<User>(l => l.Approver2).WithMany();
builder.HasOne<User>(l => l.Approver3).WithMany();
}
}

Program.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
    static void Main(string[] args) {
using(var ctx = new MyDbContext()) {
var zs = new User { Name = "张三" };
var ls = new User { Name = "李四" };
var ww = new User { Name = "王五" };
ctx.Users.Add(zs);
ctx.Users.Add(ls);
ctx.Users.Add(ww);
ctx.Leaves.Add(new Leave { Requester=zs,Approver1 = ls,Approver2=ww,Remarks="看牙",Time = DateTime.Now,Approver3=ww});
ctx.SaveChanges();
}
}
}

如何选择

对于主从结构的“一对多”表关系,一般声明为双向导航属性。
对于其他的“一对多”表关系:如果表属于被很多表引用的基础表,则用单向导航属性,否则可以自由决定是否用双向导航属性。

自引用组织结构树

基本概念

如果有一个这样的结构,我们该如何处理呢?
秋华集团全球总部
秋华集团亚太总部
秋华集团(中国)
秋华集团(新加坡)
秋华集团美洲总部
秋华集团(美国)
秋华集团(加拿大)
以上结构我们可以建立自引用组织结构树,具体操作如下
实体类:

1
2
3
4
5
6
public class OrgUnit {
public long Id { get; set; }
public string Name { get; set; }
public OrgUnit? Parent { get; set; }
public List<OrgUnit> Children { get; set; } = new List<OrgUnit>();
}

配置类:

1
2
3
4
5
6
7
public class OrgUnitConfig : IEntityTypeConfiguration<OrgUnit> {
public void Configure(EntityTypeBuilder<OrgUnit> builder) {
builder.ToTable("T_OrgUnits");
builder.Property(o=>o.Name).IsUnicode().IsRequired().HasMaxLength(50);
builder.HasOne<OrgUnit>(o => o.Parent).WithMany(o => o.Children);//根节点没有Parent所以这个关系不能为不可空
}
}

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void Main(string[] args) {
var orgUnit1 = new OrgUnit { Name = "秋华集团全球总部" };
var orgUnit11 = new OrgUnit { Name = "秋华集团亚太总部", Parent = orgUnit1 };
var orgUnit12 = new OrgUnit { Name = "秋华集团美洲总部", Parent = orgUnit1 };
var orgUnit111 = new OrgUnit { Name = "秋华集团(中国)", Parent = orgUnit11 };
var orgUnit112 = new OrgUnit { Name = "秋华集团(新加坡)", Parent = orgUnit11 };
var orgUnit121 = new OrgUnit { Name = "秋华集团(美国)", Parent = orgUnit12 };
var orgUnit122 = new OrgUnit { Name = "秋华集团(加拿大)", Parent = orgUnit12 };
using (var ctx = new MyDbContext()) {
ctx.OrgUnits.Add(orgUnit1);
ctx.OrgUnits.Add(orgUnit11);
ctx.OrgUnits.Add(orgUnit12);
ctx.OrgUnits.Add(orgUnit111);
ctx.OrgUnits.Add(orgUnit112);
ctx.OrgUnits.Add(orgUnit121);
ctx.OrgUnits.Add(orgUnit122);
ctx.SaveChanges();
}
}

注意,这里如果只说明某个节点的父节点是XXX,并且ctx.OrgUnits.Add(根节点)是无法做到顺杆爬的,因为无法通过父节点看到自己的子节点,所以采用以上这种方式。

递归缩进打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Interview {
internal class Program {
static void Main(string[] args) {
using (var ctx = new MyDbContext()) {
var root = ctx.OrgUnits.Single(o => o.Parent == null);
PrintChildren(0, ctx, root);
}
}
/// <summary>
/// 缩进打印parent所有的子节点
/// </summary>
/// <param name="indentLevel"></param>
/// <param name="dbContext"></param>
/// <param name="parent"></param>
static void PrintChildren(int indentLevel,MyDbContext ctx,OrgUnit parent) {
var children = ctx.OrgUnits.Where(o=>o.Parent==parent).ToList();//找以我为根节点的节点
foreach (var child in children) {
Console.WriteLine(new String('\t', indentLevel) + child.Name);
PrintChildren(indentLevel + 1, ctx, child);
}
}
}
}