上次老周扯了有關主、從實體的話題,本篇咱們再挖一下,主、從實體之間建立的關係,跟咱們常用的一對一、一對多這些關係之間有什麼不同。 先看看咱們從學習資料庫開始就特熟悉的常用關係——多對多、一對一、一對多說起。數據實體之間會建立什麼樣的關係,並不是規則性的,而是要看數據的功能。比如你家養的狗狗和水果(你 ...
上次老周扯了有關主、從實體的話題,本篇咱們再挖一下,主、從實體之間建立的關係,跟咱們常用的一對一、一對多這些關係之間有什麼不同。
先看看咱們從學習資料庫開始就特熟悉的常用關係——多對多、一對一、一對多說起。數據實體之間會建立什麼樣的關係,並不是規則性的,而是要看數據的功能。比如你家養的狗狗和水果(你家狗狗可能不吃水果,但老周養的動物基本是什麼都吃的,因為從它們幼年起,老周就訓練它們,對食物要來者不拒,就算哪天它們不想跟著老周混,出去流浪也不會餓死,適應性更強)。
假設:
1、你的數據是以狗狗為主,那麼一條狗狗會吃多種水果。即狗狗對水果是一對多;
2、你的數據以水果為主,每種水果單獨記錄,然後在另一個表中記錄水果被哪幾條狗喜歡。例:雪梨,狗Y和狗B都喜歡吃。於是水果對狗狗也可以是一對多的關係。
再假設你有個幼兒園學生尿床登記表,表中記錄每次尿床的時間、床號等。每一條尿床記錄都有一個欄位,引用自學生表,代表是哪們同學尿床了。多條尿床記錄可能都是同一個人的,比如,小明一周有三次尿床。這樣,尿床記錄和學生之間可以是多對一關係了。
數據是為咱們人服務的,因此實體之間建立什麼樣的關係,得看咱們人類是怎麼理解,以及這些實體的用途。
還是用上一篇水文中的學生 - 作業的例子。
public class Student { // 主鍵:學生ID public int StuID { get; set; } // 學生姓名 public string? Name { get; set; } // 年級 public ushort Grade { get; set; } // 作業(導航屬性) public IEnumerable<Homework> Homeworks { get; set; } = new List<Homework>(); } public class Homework { // 主鍵,ID public int WorkID { get; set; } // 作業描述 public string? Description { get; set; } // 科目(導航屬性) public Subject? Subject { get; set; } // 引用學生對象 public Student? Student { get; set; } } public class Subject { // 主鍵:科目ID public int SubID { get; set; } // 科目名稱 public string? Name { get; set; } }
這次老周加了個實體——Subject,它表示作業的科目(數學、語文等)。
導航屬性是用於建立實體關係的。
1、學生類中,Homeworks 屬性建立與 Homework 對象的關係:一條學生信息可以對應多條作業信息,是一對多的關係;
2、作業類中,Subject 屬性建立與 Subject 對象的關係。一對一的關係。
在 DbContext 的自定義類型中,三個實體間的關係配置如下:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 設置主鍵 modelBuilder.Entity<Student>().HasKey(s => s.StuID); modelBuilder.Entity<Homework>().HasKey(w => w.WorkID); modelBuilder.Entity<Subject>().HasKey(u => u.SubID); // 建立模型關係 modelBuilder.Entity<Student>().HasMany(s => s.Homeworks).WithOne(w => w.Student); modelBuilder.Entity<Homework>().HasOne(w => w.Subject); }
這是咱們常規的關係配置方法,從當前實體到另一實體的關係描述為 HasXXX 方法;HasXXX 方法調用後,會順帶調用一個 WithXXX 方法。WithXXX 方法是反向描述,即描述另一個實體與當前實體的關係。這樣調用可以建立比較完整的相對關係。
在上述代碼中,Student -> Homework 是一對多,所以,Student 實體上調用 HasMany 方法;之後是反向關係,Homework -> Student 是一對一關係,也就是說,一條 Homework 記錄通過外鍵只引用一條學生記錄。因此調用了 WithOne 方法。
Homework -> Subject 是一對一,所以在 Homework 實體上調用 HasOne 方法。這裡,Homework 與 Subject 兩實體並沒有建立相互引用的關係,僅僅是作業中引用了科目信息,而 Subject 實體自身可以獨立,它不需要引用 Homework 的任何實例,因此沒有調用 WithXXX 方法。
由於實體之間建立的關係是相對的,即參照當前對象。所以,上面代碼也可以這樣寫:
modelBuilder.Entity<Homework>().HasOne(h => h.Student).WithMany(s => s.Homeworks);
modelBuilder.Entity<Homework>().HasOne(h => h.Subject);
要註意的是,這兩種關係配置其實是相同的,所以兩者任選一即可,不要重覆配置。
兩種關係配置的差別就在選擇誰來做“當前實體”,即以當前實體為參照而建立相對關係。第二種方法是以 Homework 實體為當前實體,一條作業信息只關聯一位學生,所以是一對一,調用 HasOne 方法;反過來,一條學生信息可包含多條作業信息,所以是一對多,即調用 WithMany 方法。
定義幾個靜態方法,用於驗證模型建得對不對。
首先,InitDatabase 方法負責運行階段創建資料庫,並插入一些測試數據。
static void InitDatabase() { using MyContext cxt = new(); // 確保數據已創建 bool v = cxt.Database.EnsureCreated(); // 如果資料庫已存在,不用初始化數據 if (!v) return; /* 初始化數據 */ // 這是科目 Subject s1 = new(){ Name = "語文"}; Subject s2 = new(){ Name = "數學"}; Subject s3 = new(){ Name = "英語"}; Subject s4 = new(){ Name = "物理"}; Subject s5 = new(){ Name = "地理"}; cxt.Subjects.AddRange(new[]{ s1, s2, s3, s4, s5 }); // 學生和作業可以一起添加 cxt.Students.Add( new Student{ Name = "小華", Grade = 4, Homeworks = new [] { new Homework { Description = "背單詞3500個", Subject = s3 }, new Homework { Description = "作文《我是誰,我在哪裡》", Subject = s1 }, new Homework { Description = "手繪廣州地鐵網路圖", Subject = s5 } } } ); cxt.Students.Add( new Student { Name = "王雙喜", Grade = 3, Homeworks = new[] { new Homework { Description = "完型填空練習", Subject = s3 } } } ); cxt.Students.Add( new Student { Name = "割麥小王子", Grade = 5, Homeworks = new[]{ new Homework { Description = "實驗:用激光給蟑螂美容", Subject = s4 }, new Homework{ Description = "翻譯文言文《醉駕通鑒》", Subject = s1 } } } ); // 保存到資料庫 cxt.SaveChanges(); }
SaveChanges 方法記得調用,調用了才會保存數據。
ShowData 方法負責在控制台列印數據。
static void ShowData() { using MyContext ctx = new(); var students = ctx.Students.Include(s => s.Homeworks) .ThenInclude(hw => hw.Subject) .AsEnumerable(); // 列印學生信息 Console.WriteLine("{0,-5}{1,-10}{2,-6}", "學號", "姓名", "年級"); Console.WriteLine("----------------------------------------------------"); foreach(var stu in students) { Console.WriteLine($"{stu.StuID,-7}{stu.Name,-10}{stu.Grade,-4}"); // 列印作業信息 foreach(Homework wk in stu.Homeworks) { Console.Write(">> {0,-4}", wk.Subject!.Name); Console.WriteLine(wk.Description); } Console.Write("\n\n"); } }
在載入數據時得小心,因為如果你只訪問 Students 集合,那麼,Homeworks 和 Subjects 集合不會載入,這會使得 Student 實體的 Homeworks 屬性變為空。為了讓訪問 Students 集合時同時載入關聯的數據,要用 Include 方法。
第一個 Include 方法載入 Homeworks 屬性引用的 Homework對象;第二個ThenInclude 方法是指在載入 Homework 後,Homework 實體的 Subject 屬性引用了 Subject 對象,所以 ThenInclude 方法是通知模型順便載入 Subjects 集合。
最後,要調用一下實際觸發查詢的方法,如 AsEnumerable 方法,這樣才會讓查詢執行,你在記憶體中才能訪問到數據。當然,像 ToArray、ToList 之類的方法也可以,這個和 LINQ 語句的情況類似。要調用到相應的方法才觸發查詢真正執行。
RemoveDatabase 方法是可選的,刪除資料庫。咱們這是演示,免得在資料庫中存太多不必要的東西。測試完代碼可以調用一下它,刪除資料庫。這裡老周照例用 SQL Server LocalDB 來演示。
static void RemoveDatabase() { using MyContext c = new(); c.Database.EnsureDeleted(); }
-------------------------------------------------------------------------------------------
用的時候,按順調用這些方法,就可以測試了。
Console.WriteLine("** 第一步:初始化資料庫。【請按任意鍵繼續】"); _ = Console.ReadKey(true); InitDatabase(); Console.WriteLine("** 第二步:顯示數據。【請按任意鍵繼續】"); _ = Console.ReadKey(true); ShowData(); //Console.WriteLine("** 第三步:刪除資料庫。【請按任意鍵繼續】"); //_ = Console.ReadKey(); //RemoveDatabase();
產生的數據表如下圖所示:
我們上面的這個模型還是有點問題的,可以看一下,生成的數據表是沒有刪除約束的。
CREATE TABLE [dbo].[Homeworks] ( [WorkID] INT IDENTITY (1, 1) NOT NULL, [Description] NVARCHAR (MAX) NULL, [SubjectSubID] INT NULL, [StudentStuID] INT NULL, CONSTRAINT [PK_Homeworks] PRIMARY KEY CLUSTERED ([WorkID] ASC), CONSTRAINT [FK_Homeworks_Students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[Students] ([StuID]), CONSTRAINT [FK_Homeworks_Subjects_SubjectSubID] FOREIGN KEY ([SubjectSubID]) REFERENCES [dbo].[Subjects] ([SubID]) );
假如現在我要刪掉一條學生記錄。
using(MyContext dbcontext = new()) { // 刪第一條記錄 var one = dbcontext.Students.FirstOrDefault(); if(one != null) { dbcontext.Students.Remove(one); dbcontext.SaveChanges(); } }
但刪除的時候會遇到錯誤。
這表明咱們要配置級聯刪除。
public class MyContext : DbContext { public DbSet<Student> Students => Set<Student>(); public DbSet<Homework> Homeworks => Set<Homework>(); public DbSet<Subject> Subjects => Set<Subject>(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"server=(localdb)\MSSQLLocalDB;Database=TestDB;Integrated Security=True"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { …… // 建立模型關係 modelBuilder.Entity<Student>() .HasMany(s => s.Homeworks) .WithOne(w => w.Student) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity<Homework>().HasOne(w => w.Subject); } }
現在再刪一次看看。
可以看到,與第一位學生有關的作業記錄也一併被刪除了。生成的數據表也與前面有一點差異。
CREATE TABLE [dbo].[Homeworks] ( [WorkID] INT IDENTITY (1, 1) NOT NULL, [Description] NVARCHAR (MAX) NULL, [SubjectSubID] INT NULL, [StudentStuID] INT NULL, CONSTRAINT [PK_Homeworks] PRIMARY KEY CLUSTERED ([WorkID] ASC), CONSTRAINT [FK_Homeworks_Students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[Students] ([StuID]) ON DELETE CASCADE, CONSTRAINT [FK_Homeworks_Subjects_SubjectSubID] FOREIGN KEY ([SubjectSubID]) REFERENCES [dbo].[Subjects] ([SubID]) );
約束裡面顯然多了 ON DELETE CASCADE 語句。
回憶一下,在上一篇水文中,咱們使用主從對象後,我們在模型中沒有明確配置級聯刪除,但生成的數據表中自動加上級聯刪除了。
這是不是說明:主從關係的實體對象里,主實體對從屬實體的控制更強烈,咱們再對比對比看。
現在,讓 Student 和 Homework 成為主從關係。
public class MyContext : DbContext { public DbSet<Student> Students => Set<Student>(); public DbSet<Homework> Homeworks => Set<Homework>(); public DbSet<Subject> Subjects => Set<Subject>(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { …… } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 設置主鍵 modelBuilder.Entity<Student>().HasKey(s => s.StuID); modelBuilder.Entity<Subject>().HasKey(u => u.SubID); // 建立模型關係 modelBuilder.Entity<Student>() .OwnsMany(s => s.Homeworks, mrb => { mrb.WithOwner(w => w.Student); mrb.HasKey(w => w.WorkID); mrb.HasOne(w => w.Subject); }); } }
上次我們也證實過,凡成為從屬的實體是無法單獨進行配置的(如主鍵等),只能在配置主從關係的時候通過 OwnsMany 方法的委托來配置。
主從關係會自動生成級聯刪除語句。
CREATE TABLE [dbo].[Homeworks] ( ……, CONSTRAINT [PK_Homeworks] PRIMARY KEY CLUSTERED ([WorkID] ASC), CONSTRAINT [FK_Homeworks_Students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[Students] ([StuID]) ON DELETE CASCADE, …… );
還有一點更關鍵的,Homework 成為 Student 的從對象後,你甚至無法直接訪問 Homeworks 集合,必須通過 Sudents 集合來訪問。
using (MyContext ctx = new MyContext()) { foreach(Homework hw in ctx.Homeworks) { Console.WriteLine($"{hw.Description}"); } }
上述代碼會拋異常。
這很明瞭,就是說你必須通過 Student 實體才能訪問 Homework。所以,正確的做法要這樣:
using (MyContext ctx = new MyContext()) { ctx.Subjects.Load(); // 這個可不會自動載入,必須Load foreach(Student stu in ctx.Students) { Console.WriteLine("【{0}】同學", stu.Name); foreach(Homework work in stu.Homeworks) { Console.WriteLine(" {0}:{1}", work.Subject?.Name, work.Description); } } }
Subjects 集合為什麼要顯式地調用 Load 方法呢?因為 Homework 與 Subject 實體並沒有建立主從關係,Subject 對象要手動載入。
這樣訪問就不出錯了。
-----------------------------------------------------------------------------------
最後,咱們來總結一下:
1、普通關係的數據未自動載入,要顯式Load,或者 Include 方法載入。主從關係會自動載入從屬數據;
2、建立主從關係後,主實體對從實體是完全控制了,不僅自動生成級聯刪除等約束,而且你還不能直接訪問從實體,只能透過主實體訪問;普通關係的實體需要手動配置約束。
========================================================
下麵是老周講故事時間。
上大學的時候,在《程式員》雜誌上看過一句很“權威”的話:程式員是世上最有尊嚴的職業,不用酒局飯局,不用看人臉色,想幹啥幹啥,自由得很。然而,“多年以後一場大雨驚醒沉睡的我,突然之間都市的霓虹都不再閃爍”。客戶說需求要這樣這樣,你改不改?改完之後客戶又說還是改回那樣那樣,你改不改?總姦,哦不,總監說要這樣這樣,你能那樣那樣嗎?客戶說:“我們希望增加XXX功能,最好可以分開YYY、KKK 來管理。這些對你們來很簡單的,動動滑鼠就好了嘛!” 你動動滑鼠試試?
再說了,哪個公司哪個單位的領導不是酒囊飯袋?IT 公司沒有嗎?哪兒都有,這世界最不缺的就是酒囊飯袋,最缺的是成吉思汗。
所以說,最TM自由、耍得最爽的就寫博客,愛寫啥寫啥,套用土傑倫的歌詞就是“你愛看就看,不愛看拉倒”。至於碼農,就如同被壓迫數千年的農民一樣,沒本質區別。所以,我們在給後輩講碼農生涯時,千萬不要給他們畫大餅,充不了飢。我們更應該教會他們程式員的最基本職業道德—— sudo rm -rf /*。