EntityFramework之孩子删除(四)(你以为你真的懂了?)
前言
从表面去看待事物视线总有点被层层薄雾笼罩的感觉,当你静下心来思考并让指尖飞梭于键盘之上,终将会拨开浓雾见青天。这是我切身体验。
在EF关系配置中,我暂且将主体对象称作为父亲,而依赖对象称作为孩子,父亲与孩子关联的关系可能是必须的也可能是可选的,如果是必须的那么意味着孩子不能因没有父亲而独立存在,又如果父亲被删除了(即父亲与孩子的关系被隔离),那么孩子将变成留守儿童(即孤儿),所以当处在这种情况下时,那么孩子应该需要自动被删除。
话题
必须关系和可选关系
我们接下来就父亲与孩子的关联关系来进行删除的话题。
我们建立三个类,一个类是Student(学生类),一个类是Grade(成绩类),最后一个类是Flower(小红花类)。我们假设有如下场景:一个学生对应多门成绩,但一门成绩就属于一个学生,同时可能学生团队合作表现好,一朵小红花对应多个学生,但是这个小红花肯定只会被一个学生拿走也就只对应一个学生,也有可能没得到小红花。鉴于此,类建立如下:
public class Student /*学生类*/ { public int Id { get; set; } public string Name { get; set; } public int? FlowerId { get; set; } public virtual Flower Flower { get; set; } public virtual ICollection<Grade> Grades { get; set; } } public class Grade /*成绩类*/ { public int Id { get; set; } public int Fraction { get; set; } /*学生成绩*/ public int StudentId { get; set; } public virtual Student Student { get; set; } } public class Flower /*小红花类*/ { public int Id { get; set; } public string Remark { get; set; } /*小红花描述*/ public virtual ICollection<Student> Students { get; set; } }
通过上述描述,我们对应的映射如下:
学生映射:
public class StudentMap : EntityTypeConfiguration<Student> { public StudentMap() { ToTable("Student"); HasKey(key => key.Id); HasOptional(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId); } }
成绩映射:
public class GradeMap: EntityTypeConfiguration<Grade> { public GradeMap() { ToTable("Grade"); HasKey(p => p.Id); HasRequired(p => p.Student).WithMany(p => p.Grades).HasForeignKey(p => p.StudentId); } }
对于EF上下文建立,不再描述,不明白的话可以参见我前两篇文章。
对于我们上面的可选字段FlowerId生成数据库中也是可选的,如下:
我们插入数据如图:
现在我们进行如下操作:删除学生姓名为bob的
using (var ctx = new EntityDbContext()) { ctx.Set<Student>().Remove(ctx.Set<Student>().Single(p => p.Name == "bob")); }
删除后结果如下:
那么问题来了,为什么我删除学生名为bob的而相关成绩也删除了呢?
答案是在学生和成绩之间建立了一个级联删除,所以会自动进行删除,级联删除也就是当父亲被删除时,其孩子也会被删除,EF Code First为什么会这样做呢?因为学生和成绩之间的关系是必须(Required)的。
EF Code First不仅在实体在进行了配置而且在数据库中进行了配置,因为那是至关重要的,如果级联删除存在于实体中,那么在数据库中也应该必须存在,如果这两者不能同步那么在数据库中会出现约束错误。
接下来我们通过Flower(小花)来简介删除学生姓名为bob的,因为其对应的Remark是so bad(坏学生):
ctx.Set<Flower>().Remove(ctx.Set<Flower>().Include(p => p.Students).Single(p => p.Remark == "so bad"));
结果如下:
那么问题来了,为什么没有删除学生bob呢?
答案就是外键属性FlowerId和导航属性Flower被设置成了空,所以学生bob不会被删除,因为EF Code First不会为可选的关系设置级联删除。
【注意】在此种情况下, 如果你加载学生集合列表到内存中,那么EF Code First会在保存之前将外键属性设置为空。
如果此时你想在可选关系上强制执行删除那就在映射中进行如下操作:
HasOptional(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId).WillCascadeOnDelete(true);
接下来如果我进行学生与小花之间的映射进行如下修改:
HasRequired(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId);
此时再来进行上述删除: ctx.Set<Flower>().Remove(ctx.Set<Flower>().Include(p => p.Students).Single(p => p.Remark == "so bad")); 此时结果如下:
那么问题来了,为什么这样就能进行相应学生的删除了呢?
答案就是当你用上必须的关系(即Required)之后即使你设置外键属性可为空,但是当映射到数据库之后,它会将其映射为非空的外键字段(可以理解为关系映射比POCO实体手动设置优先级高)!不信看如下图:
小结
(1)当关系为可选(Optional)时此时外键属性和导航属性为空,不会进行级联删除,但是可以用 WillCascadeOnDelete 进行强制删除。
(2)当关系为必须(Required)时此时会内置进行级联删除即使外键属性为可空的类型,也就是说无需多此一举加上WillCascadeOnDelete来进行级联删除。
你是不是觉得关于删除就这么简单呢?那你就大错特错了,请继续看下文。
隔离关系
依然以上述为例,我们现在想象有这样一场景,bob的成绩太差每次都没及格,并且虽给了小红花但是评语写着so bad,这样放学回家如何向爸妈交代呢,至少将成绩考好点吧,于是它要求老师删除他不良的成绩并给其100分的好成绩。在此场景下,我们代码如下:
using (var ctx = new EntityDbContext()) { var stu = ctx.Set<Student>().Single(p => p.Name == "bob"); stu.Grades.Remove(stu.Grades.OrderBy(p => p.Id).First(p => p.Student.Name == "bob")); stu.Grades.Add(new Grade() { Fraction = 100 }); }
但结果是老师也是有心无力啊,出错了,如下:
因为成绩从导航属性集合中移出后,它变成孤立对象(外键为NULL),提交时,是因为外键约束而失败,异常提示,也显示外键不能为空!
所以此时我们能想到的办法就是直接将孩子进行删除或者通过重写SaveChanges找到并删除。
于是在保存之前我添加如下代码:
ctx.Set<Grade>().Local.Where(p => p.Student == null).ToList().ForEach(r => ctx.Set<Grade>().Remove(r));
重写SaveChanges
public override int SaveChanges() { ctx.Set<Grade>() .Local .Where(p => p.Student == null).ToList() .ForEach(r => ctx.Set<Grade>().Remove(r)); return base.SaveChanges(); }
最后通过,数据成功进行添加,如图:
上述代码有如下四点意思
(1)使用DbSet.Local来访问当前通过上下文追踪的没有运行任何数据库查询并且未被删除的成绩实体
(2)过滤列表中每一个没有引用学生实体的数据
(3)通过一个过滤列表的副本,来避免枚举时修改一个Collection
(4)标记每个孤儿(成绩)为已删除
小结
(1)默认情况下,EF Code First认为空的外键属性其关系是可选的,而对于非空的外键属性其关系是必须的。必须关系同时配置了级联删除,以至于如果父亲被删除则其所有的孩子也将被删除。
(2)必须和可选的关系自然能通过Fluent API来进行改变或者Data Anotaions和级联删除能够用Fluent API来进行配置
(3)如果父亲已经被隔离,那么通过级联删除不会删除孩子。
EF 那些琐事儿
上述异常信息被EF团队称作为“概念上可空消息”,因为当一个关系被隔离,则其关系中的外键将被设置为空。然而,如果属性为非空,那么EF在概念上将其设置为空,但是实际上没这么做,所以“概念上可空消息”没有被保存到数据库中而是在异常中。