大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: ADO&EF 修改讲义

加载

EF默认不会加载entity所关联的entity:

Major major = context.Find<Major>(1);
Console.WriteLine(major.Teacher.Name);//NullReference Exception
但这是可以的:
Console.WriteLine(major.TeacherId);

@想一想@:为什么?

复习:ORM常见加载模式

显式(Explicit)加载

在已经取到一个entity之后,再利用方法:

  • Entry():获取该entity在DbContext中的追踪信息,然后调用其
  • Reference()/Collection()方法,表明要获取该entity的单个的关联(Reference)或集合(Collection)对象,最后再调用
  • Load():将其加载
    context.Entry(major)
        .Reference(m => m.Teacher)
        .Load();
    context.Entry(major)
        .Collection(m=>m.Students)
        .Load();
演示:
  • EF分两次用SQL查询分别获取entity信息
  • Load()之后EF自动赋值entity关联属性值

之前我们学习过的影子属性,可以用Entry()方法获取。断点演示:(以及ChangeTracker)

context.Entry(major).Property("TeacherId")

延迟(惰性Lazy)加载

在.NET core 2.1之后支持,需要添加引用Microsoft.EntityFrameworkCore.Proxies,并显式开启:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseLazyLoadingProxies()

所涉及的entity必须是可被继承的,且(所有相关)关联entity属性必须是可override的(virtual):

public class Major : BaseEntity
{
    public virtual Teacher Teacher { get; internal set; }
    public virtual IList<Student> Students { get; internal set; }
public class Teacher : Person
{
    public virtual IList<Major> Majors { get; set; }
public class Student : Person
{
    public virtual List<Major> Majors { get; set; }

演示:

  • 没有virtual声明报错
  • 按需加载(进行SELECT查询)

预先(Eager)加载

关联的数据会随着entity加载同时加载。这需要用到方法

Include()

包含当前entity的关联对象。

但因为Include()的调用对象必须是IQueryable对象调用,所以这里不能用Find(),而是:

Major major = context.Set<Major>().Where(m => m.Id == 1)
    .SingleOrDefault();
//等同于:Major major = context.Find<Major>(1);
然后,加上Include
Major major = context.Set<Major>().Where(m => m.Id == 1)
    .Include(m => m.Teacher)
    .SingleOrDefault();  

演示:

  • F9断点,Include()之后能看到关联entity的值
  • logger,生成的SQL语句使用JOIN,一次性获取全部数据

一个entity下可以Include()多个Property:

Major major = context.Set<Major>().Where(m => m.Id == 1)
    .Include(m => m.Teacher)
    .Include(m=>m.Students)

也可以Include()关联entity的关联entity:(Teacher中添加Bed,重新建库建表略)

Teacher fg = new Teacher { Name = "大飞哥", Age = 41, 
    Bed = new Bed { Location = "闭包间" } 
};

ThenInclude()

但是,如果Include()出来的是集合,要再把集合中每个元素的关联entity加载出来,就需要:

Major major = context.Set<Major>().Where(m => m.Id == 2)
    .Include(m => m.Students)
        .ThenInclude(s => s.Bed)

PS:早期版本的VS ,ThenInclude()的智能提示可能有问题,略过错误提示直接运行即可


关系变更

关联entity本身(基本属性)发生的更改,会在SaveChanges()时被同步到数据库:这是很自然/简单的。

麻烦的是entity和entity之间“关系”(外键值)的变更。

一对多

1个Teacher,可以有n个Major。

“n”这一边(Major)的更改只需要重新赋值:

context.Find<Major>(1).Teacher = context.Find<Teacher>(2);
数据库中简单UPDATE就可以完成。

n”这一边(Teacher):

  1. 删除一个集合元素,
    context.Find<Teacher>(2).Majors.Remove(teacher.Majors[0]);
    会真正的DELETE这个entity,只是“切断”两个entity之间的关系,将外键值改为NULL
  2. 添加一个已有的entity到集合,
    teacher.Majors.Add(
        //不是:new Major()
        //而是其他Teacher的Major
        context.Find<Teacher>(1).Majors[0]
        );
    会直接UPDATE被添加进来entity的外键

多对多

首先,单边操作行不行?是可行的!(不然,累死,^_^)

所以删除是比较简单的:

Student student1 = context.Find<Student>(1);
major.Students.Remove(student1);

演示:DELETE关系表相关行

但另一种常见的操作:全部更新某Major的所有学生(演示:文章编辑页更新关键字)

再重新赋值之前

major.Students = new List<Student>
{
    student1, student2, student5

不要忘了调用:

major.Students.Clear();

否则EF无法生成“原有关系应该被删除”的ChangeTracker!

演示:

  • 没有Clear(),就只会无脑INSERT,结果报duplicate key异常
  • 调用Clear()之后,ChangeTracker生成了额外的Dicttionary……

PS:以上演示都基于LazyLoad,非LazyLoad加载模式注意避免NullReference异常!

一对一

略。

按我们之前的方案,会遇到很多麻烦:

  1. 主键不能被修改
  2. 主键不能冲突

所以如果要能修改,就要改映射方案,额外引入一个unique的外键……

而且一般就不推荐1:1,一对一定了也不会更改……


DeleteBehavior

@想一想@:在关联的entity之间,当删除父entity(比如Classroom)的时候,子entity(比如Student)会被如何处理?

EF默认按数据库指示操作(复习:SQL中的Cascade Delete)。

但EF建表时默认:(演示

  • 一对一和多对多:CASCADE DELETE,删除一个父entity,在Logger中没有有删除/更新子entity的SQL语句,但子entity在数据库中被自动删除
  • 一对多:不予指示,删除一个父entity,触动外键约束,抛出异常

自定义:OnDelete()方法

在OnModelCreating()中可调用OnDelete()方法,传递DeleteBehavior枚举值,指定EF core如何操作。比如:
modelBuilder.Entity<Major>()
    .HasOne(m=>m.Teacher)
    .WithMany()
    .OnDelete(DeleteBehavior.Cascade)
    ;

演示:重新建库建表,Major表的外键上有了指示:

ON DELETE CASCADE

EF的DeleteBehavior枚举可以被归为3类。对应着当删除父entity的时候,子entity或数据行的3种处理方案:

  1. Cascade/ClientCascade:子entity被同时删除
  2. SetNull/ClientSetNull:其外键被设置为null (如果外键不能被设置为null,会报异常)
  3. NoAction:不发生改变

Client和非Client

client指EF

  • 有client前缀:EF会生成处理子entity的Delete语句,交数据库执行,不依赖于数据库的设定
  • 没有client,EF建外键约束时就指定casacade方式,此后需依此执行,EF不会生成处理子entity的Delete语句

所以,如果设置是ClientX,子entity必选要已经加入到当前DbContext:

modelBuilder.Entity<Student>()
    .HasOne(s => s.SleepIn)
    .WithOne(b => b.Student)
    .HasPrincipalKey<Student>(s => s.Id)
    .OnDelete(DeleteBehavior.ClientCascade)
    ;

Student atai = context.Find<Student>(1);
//不能省略,必须通过延迟加载讲atai.SleepIn加入当前context
Console.WriteLine(atai.SleepIn.Location);
context.Remove(atai);
演示:
  • Major表的外键上没有ON DELETE的设置
  • log中生成了删除Bed和Student的SQL语句

限制

当子entity的navigator属性(表外键)注定不能为NULL值到时候,不能设置SetNull,包括:

  • 一对一:因为外键同时也是主键,此时的一对一是无法设置SetNull的
  • 一对多:设置了[Required]
还有多对多,SET NULL没有意义所以根本就不能点出OnDelete()方法。

标记删除

当entity之间的关系复杂之后,级联删除会导致复杂的链式反应,比如:

删除A就会删除B,删除B就会删除C和D,但是D又被E和F依赖,可能根本就删不掉……

所以我们通常都不进行物理删除,而是用flag标记删除(在entity中添加一个是否已删除的Flag列)。

比如,让entity实现以下接口,一旦删除,就将HasDeleted赋值:

public interface IDeletable
{
    //记录删除时间,或许比单纯的bool更有用
    DateTime? HasDeleted { get; set; }
    //甚至还要记录被谁删除等……

最后的最后,确实要删除(通常是因为数据太大影响性能等),再由DBA批量删除。这样做的好处很多:

  • 减少用户等待时间
  • 可以回溯撤销
  • 便于控制意外情况发生


作业

ORM概述的第5/6/7/8题

学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ADO&EF 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码