EF默认不会加载entity所关联的entity:
Major major = context.Find<Major>(1); Console.WriteLine(major.Teacher.Name);//NullReference Exception但这是可以的:
Console.WriteLine(major.TeacherId);
@想一想@:为什么?
复习:ORM常见加载模式
在已经取到一个entity之后,再利用方法:
context.Entry(major) .Reference(m => m.Teacher) .Load(); context.Entry(major) .Collection(m=>m.Students) .Load();
之前我们学习过的影子属性,可以用Entry()方法获取。断点演示:(以及ChangeTracker)
context.Entry(major).Property("TeacherId")
在.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; }
演示:
关联的数据会随着entity加载同时加载。这需要用到方法
包含当前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();
演示:
一个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 = "闭包间" } };
但是,如果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):
context.Find<Teacher>(2).Majors.Remove(teacher.Majors[0]);并不会真正的DELETE这个entity,只是“切断”两个entity之间的关系,将外键值改为NULL
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!
演示:
PS:以上演示都基于LazyLoad,非LazyLoad加载模式注意避免NullReference异常!
略。
按我们之前的方案,会遇到很多麻烦:
所以如果要能修改,就要改映射方案,额外引入一个unique的外键……
而且一般就不推荐1:1,一对一定了也不会更改……
@想一想@:在关联的entity之间,当删除父entity(比如Classroom)的时候,子entity(比如Student)会被如何处理?
EF默认按数据库指示操作(复习:SQL中的Cascade Delete)。
但EF建表时默认:(演示)
modelBuilder.Entity<Major>() .HasOne(m=>m.Teacher) .WithMany() .OnDelete(DeleteBehavior.Cascade) ;
演示:重新建库建表,Major表的外键上有了指示:
ON DELETE CASCADE
EF的DeleteBehavior枚举可以被归为3类。对应着当删除父entity的时候,子entity或数据行的3种处理方案:
client指EF:
所以,如果设置是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);演示:
当子entity的navigator属性(表外键)注定不能为NULL值到时候,不能设置SetNull,包括:
当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题
多快好省!前端后端,线上线下,名师精讲
更多了解 加: