【EF Core】主從實體關係與常見實體關係的區別

来源:https://www.cnblogs.com/tcjiaan/archive/2023/07/15/17550152.html
-Advertisement-
Play Games

上次老周扯了有關主、從實體的話題,本篇咱們再挖一下,主、從實體之間建立的關係,跟咱們常用的一對一、一對多這些關係之間有什麼不同。 先看看咱們從學習資料庫開始就特熟悉的常用關係——多對多、一對一、一對多說起。數據實體之間會建立什麼樣的關係,並不是規則性的,而是要看數據的功能。比如你家養的狗狗和水果(你 ...


上次老周扯了有關主、從實體的話題,本篇咱們再挖一下,主、從實體之間建立的關係,跟咱們常用的一對一、一對多這些關係之間有什麼不同。

先看看咱們從學習資料庫開始就特熟悉的常用關係——多對多、一對一、一對多說起。數據實體之間會建立什麼樣的關係,並不是規則性的,而是要看數據的功能。比如你家養的狗狗和水果(你家狗狗可能不吃水果,但老周養的動物基本是什麼都吃的,因為從它們幼年起,老周就訓練它們,對食物要來者不拒,就算哪天它們不想跟著老周混,出去流浪也不會餓死,適應性更強)。

假設:

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 /*。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • > 出於各種限制,很多公司依然停留在Java8,部分小伙伴轉向了Kotlin。Kotlin作為靜態編譯語言,提供大量語法糖,而且編譯後的位元組碼跟Java一致。 > > 當時,Java8於2014年發佈,Kotlin於2016年,很多宣稱的語法糖都是對比的Java8。不禁要問,相對今天的Java17, ...
  • """ # 一、axios是什麼 Axios 是一個基於 promise 網路請求庫,作用於node.js 和瀏覽器中。 它是 isomorphic 的(即同一套代碼可以運行在瀏覽器和node.js中)。在服務端它使用原生 node.js http 模塊, 而在客戶端 (瀏覽端) 則使用 XMLHt ...
  • 中大型項目中,我們都會把項目結構劃分多個模塊。它清晰的定義,便於項目結果維護,同時在日常代碼變更時,各個模塊的隔離也一定程度上保證了變更質量…… ...
  • python的開發工具有很多款,很多都是非常好用的,其中vscode作為其中一款Python的開發工具,是非常輕量級的,今天我們來介紹一下vs code的下載與安裝。 # vscode的下載與安裝 首先需要到vscode的官網,這個谷歌或者百度一下就可以搜到,然後根據你的系統下載你對應的版本,我這裡 ...
  • 第三方鏡像是在Docker Hub或其他容器註冊表上提供的預構建Docker容器鏡像。這些鏡像由個人或組織創建和維護,可以作為您容器化應用程式的起點。 ### 查找第三方鏡像 [**Docker Hub**](https://hub.docker.com/) 是最大和最受歡迎的容器鏡像註冊表,包含官 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《Java擴展Nginx》系列的第 ...
  • # Scala基礎篇 ## 數據類型 下表中列出的數據類型都是對象,可以直接對它們調用方法。 | 數據類型 | 描述 | | | | | Byte | 8位有符號補碼整數。數值區間為 -128 到 127 | | Short | 16位有符號補碼整數。數值區間為 -32768 到 32767 | | ...
  • 在Revit自帶的導出功能中,我們可以知道,Revit可以導出如下格式文件: 他們分別對應的API在Document類下麵,主要包含以下方法 1 Export(String, String, MassGBXMLExportOptions) 從體量模型文檔中導出gbXML文件。 2 Export(St ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...