前言
從表面去看待事物視線總有點被層層薄霧籠罩的感覺,當你靜下心來思考並讓指尖飛梭於鍵盤之上,終將會撥開濃霧見青天。這是我切身體驗。
在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在概念上將其設置為空,但是實際上沒這么做,所以“概念上可空消息”沒有被保存到數據庫中而是在異常中。