一、概览
Part3-14:EF Core一对多关系配置_哔哩哔哩_bilibili
Part3-15:EF Core一对多关系数据的获取_哔哩哔哩_bilibili
Part3-16:EF Core额外的外键字段_哔哩哔哩_bilibili
Part3-18:EF Core关系配置在任何一方都可以_哔哩哔哩_bilibili
二、关系配置
一对多:HasOne(...).WithMany(...);
一对一:HasOne(...).WithOne (...);
多对多:HasMany (...).WithMany(...);
三、一对多
3.1、一端实体类代码:Article
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:new List<Comment>()
}
3.2、多端实体类代码:Comment
public class Comment
{
public long Id { get; set; }
public Article Article_P{ get; set; }
public string Message { get; set; }
}
3.3、一端实体类对应的配置类:ArticleConfig,因为这次例子主要是从多端配置外键,所以一端没什么注意的,就是一个常规的配置
class ArticleConfig : IEntityTypeConfiguration<Article>
{
public void Configure(EntityTypeBuilder<Article> builder)
{
builder.ToTable("Articles_Test");
builder.Property(a => a.Title).HasMaxLength(100).IsUnicode().IsRequired();
builder.Property(a => a.Message).IsUnicode().IsRequired();
}
}
3.4、多端实体类对应的配置类:CommentConfig
主要是这行代码用于配置外键:builder.HasOne<Article>(c => c.Article_P).WithMany(a => a.Comments).IsRequired();
- HasOne<Article>表示一端是:Article
- (c => c.Article_P)中的Article_P:是Comment(多端)中的Article(一端)类型字段;用于生成外键。
- WithMany(a => a.Comments)Comments:一端中连接多端的字段
class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("Comments_Test");
builder.Property(a => a.Message).IsUnicode().IsRequired();
builder.HasOne<Article>(c => c.Article_P).WithMany(a => a.Comments).IsRequired();
}
}
3.5、Sql Server中多端表:Comment会生成一个外键,由多端实体中连接一端外键的属性的属性名(public Article Article_P{ get; set; })+Id组成
3.6、往生成的表中插入数据
即使没有将子对象Comment的数据插入到数据库,也不影响其插入数据库,因为Comments与Articles之间存在关联,并且Comments表中的Article_PId字段自动使用其父表(Article)的Id信息。
class Program
{
static void Main(string[] args)
{
using (MyDdContext db = new MyDdContext())
{
//一端:父对象
Article a1 = new Article();
a1.Title = "杨中科被评为亚洲最酷程序员";
a1.Message = "据报道......";
//多端:子对象
Comment c1 = new Comment { Message="太牛了"};
Comment c2 = new Comment { Message = "吹吧,SX!" };
//子对象加入到父对象中
a1.Comments.Add(c1);
a1.Comments.Add(c2);
//父对象加入到数据库中
db.Articles.Add(a1);
db.SaveChanges();
} }
}
3.7、关联查询:Include
- 通过查询一端信息关联带出多端信息
Article a = db.Articles.Include(a => a.Comments).Single(b => b.Id == 1); //Include 关联查询出Comments的值
Console.WriteLine();
Console.WriteLine(a.Id+","+a.Title);
foreach (var c in a.Comments)
{
Console.WriteLine(c.Id+","+c.Message);
}
- 通过查询多端信息关联带出一端信息
Comment c = db.Comments.Include(a => a.Article_P).Single(b => b.Id == 2);
Console.WriteLine(c.Message);
Console.WriteLine(c.Article_P.Id + "," + c.Article_P.Title);
3.8、额外的外键字段
背景:如果只需要Comments信息及外键Id信息而不需要外键对应的其它信息,按照常规外键配置及3.7中得知,必须通过Include,但是这样的方式产生的SQL语句会产生Join操作,性能优化角度不符合预期,解决方案就是新增额外外键用于查询操作避免此问题;如果同一张表需要两个完全相同的”外键“列,一个是真正意义上的外键,一个只是数值和”真正意义上的外键“相同,并不作为外键使用,用作查询辅助避免Join操作。
依照本例子,在多端表(Comments)实体类中指定外键列。
并在多端表配置类(CommentConfig)中指定外键列
查询
var c = db.Comments.Single(a => a.Id == 2);
Console.WriteLine(c.Id + ","+ c.Article_PId);
3.9、避免Select *操作的方法,获取部分列的信息,而不是所有列信息:Select
例如以下例子中只获取Comments中的Id和Article_PId信息,而不要Message信息
四、关系配置在任何一方都可以:放置多端或一端均可
4.1、在三中的例子,关系配置是放在多端。也可以放置在一端,放置在一端如图更改三中例子。
4.2、推荐策略:考虑到有单向导航属性的可能,我们一般使用HasOne().WithMany(),也可以称之为:一对多关系一般将关系配置在多端。单向导航属性详见3-17讲解。
五、项目实战——WEB API的Json序列化与EF CORE一对多查询结果的爱恨情仇
1、在.NET Web API中,你从API方法返回一个对象作为响应,Web API框架将自动将该对象序列化为JSON格式,并将其作为HTTP响应的内容返回给客户端,如果是前端项目处理JSON格式的数据非常方便。这是因为Web API默认使用JSON作为数据交换格式。详见ASP.NET Core WebAPI的异步及返回值 - 简书
2、一对多查询结果中的某个字段是某个实体对象或者实体对象集(一端.includ(多端) 或 多端.includ(一端))
3、如果直接将一对多的查询结果通过API返回给客户端会出现报错:A possible object cycle was detected....
- 报错分析:因为一端有多端类型的字段,多端也有一端类型的字段,所以Json解析的时候会从一端解析出多端的数据,又从多端解析出一端,这就是所谓的循环引用
4、解决方案! - 方案一(最简单):在实体类上使用 [JsonIgnore] 特性,将循环引用的导航属性标记为不需要进行序列化。假设我的查询结果是一端为主,多端作为一端的某个字段信息,那么我就在多端中的一端类型字段中标注 [JsonIgnore]
- 方案二(未验证,有兴趣的可以去整一下):展开查询结果集,使用DTO(数据传输对象)来表示返回给前端页面的数据,而不使用实体类。创建一个新的DTO类,将需要的属性复制到DTO中,并在查询时将结果映射到DTO对象。
六、EF CORE 一对多:多端外键设置为可空 null
1、按照常规配置方案,多端的外键在SQL SERVER中是not null。常规配置方案如下:默认生成外键是:SalePlanInfoId
-
多端
-
一端
2、如何让默认生成外键:SalePlanInfoId在数据库是null
-
Data Annotation:数据库中的SalePlanInfoId(子表中的主键id列)默认情况是自动生成,在实体类中不体现,实体类中只体现一端实体类类型参数:本例中是public SalePlanInfo SalePlanInfo { get; set; }。此时需要对SalePlanInfoIdId显性配置成可空,加个问号即可
-
FluentAPI:对SalePlanInfoId配置.IsRequired(false)
FluentAPI和Data Annotation区别可以参考:Part3-4&5 EF CORE-FluentAPI&Data Annotation(杨中科)
其实根据FluentAPI和Data Annotation特性,Data Annotation和FluentAPI配置一个即可,但是根据我实际验证要两个都配置才生效,是我的姿势不对?有了解的大佬可以帮帮提提讲解。
关于一对多多端中外键列配置,也可以测试以下这里面的方法(我没试过,好像行不通?看着挺靠谱):实体框架核心中的一对多关系
七、EF CORE 一对多:批量导入数据性能优化
备注:以下代码演示中,SalePlanInfo为一端,SalePlan为多端,SalePlans是一端实体类中的多端类型属性,SalePlanInfoId是多端实体类中的一端主键id属性。
1、常规方案:一对多导入数据,是通过AddRange(),一端对象绑定多端数据集,再SaveChanges()。
- 但是性能非常差,EF CORE因为架构的原因官方一直没有优化,除非重构EF CORE。
salePlanInfo.SalePlans.AddRange(salePlans);
db.salePlanInfos.Add(salePlanInfo);
int success = db.SaveChanges();
2、优化后的方案
先导入一端数据、再获取导入的一端数据在数据库中生成的自增id
再通过for循环list<多端对象>,批量更新多端对象中的主键id值
通过杨中科老师提供的批量导入功能完成批量导入:BulkInsert
Zack.EFCore.Batch/README_CN.md at main · yangzhongke/Zack.EFCore.Batch (github.com)关于EF CORE批量增删改的性能优化可以参考以下文章
Part3-36:EFCore如何批量删除、新增、修改、插入优化后的代码演示
dbcontext.salePlanInfos.Add(salePlanInfo);
db.SaveChanges(); // 保存一端数据以生成主键
var salePlanInfoId = salePlanInfo.Id; // 获取一端刚保存的主键id
for (int i = 0; i < salePlans.Count(); i++)
{
salePlans[i].SalePlanInfoId = salePlanInfoId; // 手动设置外键
}
db.BulkInsert(salePlans); // 批量导入多端数据
3、优化后的方案注意事项,更新salePlans中的SalePlanInfoId,那么salePlans类型一定要使用List<salePlans>,不能使用IEnumerable<salePlans>,原因可以参考:C# List<T>、IEnumerable<T>主要区别 - 简书