【EF Core】實體的主、從關係

2023-07-02 18:00:32

假設有以下兩個實體:

public class Student
{
    public int StuID { get; set; }
    public string? Name { get; set; }
    public IEnumerable<Homework>? Homeworks { get; set; }
}

public class Homework
{
    public string? Class { get; set; }
    public string? Subject { get; set; }
}

Homework 類表示家庭作業,它並不是獨立使用的,而是與學生類(Student)有依賴關係。一位學生有多個家庭作業記錄,即 Homework 物件用於記錄每位同學的作業的。按照這樣的前提,Student 是主物件,Homework 是從物件。

Student 物件有個 Homeworks 屬性,用於參照 Homework 物件,也就是所謂的「導航屬性」。這個「導航」,估計意思就是你通過這個屬性可以找到被參照的另一個實體物件,所以稱之為導航,就是從 Navigation 的翻譯。

隨後,咱們要從 DbContext 類派生出自定義的資料庫上下文。

public class MyDbContext : DbContext
{
    // 對映的資料表,名稱預設與屬性名稱一樣
    // 即 Students + Works
    public DbSet<Student> Students => Set<Student>();
    public DbSet<Homework> Works => Set<Homework>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // 設定連線字串
        optionsBuilder.UseSqlServer(Helper.Conn_STRING);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 設定主鍵
        modelBuilder.Entity<Student>().HasKey(s => s.StuID);
        // 建立主從關係
        modelBuilder.Entity<Student>().OwnsMany(s => s.Homeworks);
    }
}

連線字串是老周事先設定好的,連的是 SQL Server。

public class Helper
{
    public const string Conn_STRING = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=stuDB;Integrated Security=True";
}

用的是 LocalDB,這玩意兒方便。

其實這是一個控制檯應用程式,並新增了 Nuget 包。

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  </ItemGroup>

好,回到咱們的程式碼中,MyDbContext 重寫了兩個方法:

1、重寫 OnConfiguring 方法,做一些與該 Context 有關的設定,通常是設定連線字串;也可能設定一下紀錄檔輸出。上面程式碼中使用的是擴充套件方法 UseSqlServer。這就是參照 Microsoft.EntityFrameworkCore.SqlServer Nuget 包的作用。

2、重寫 OnModelCreating 方法。這個是設定實體類相關的模型屬性,以及與資料表的對映,或設定實體之間的關係。上述程式碼中,老周做了兩件事:A、為 Student 實體設定主鍵,作為主鍵的屬性是 StuID;B、建立 Student 和 Homework 物件的主從關係,呼叫 OwnsMany 方法的意思是:一條 Student 記錄對應 N 條 Homework 記錄。因為 Student 類的 Homeworks 屬性是集合。

注意:咱們此處是先建了實體類,執行後才建立資料庫的,所以不需要生成遷移程式碼。

在 Main 方法中,咱們要做兩件事:A、根據上面的建模建立資料庫;B、往資料庫中存一點資料。

static void Main(string[] args)
{
    using (var ctx = new MyDbContext())
    {
        //ctx.Database.EnsureDeleted();
        bool res = ctx.Database.EnsureCreated();
        if (res)
        {
            Console.WriteLine("已建立資料庫");
        }
    }

    using(MyDbContext ctx = new())
    {
        // 加點料
        ctx.Students.Add(new Student
        {
            Name = "小張",
            Homeworks = new List<Homework>
            {
                new Homework{ Class = "數學", Subject = "3000道口算題"},
                new Homework{ Class = "英語", Subject = "背9999個單詞"}
            }
        });

        ctx.Students.Add(new Student
        {
            Name = "小雪",
            Homeworks = new Homework[]
            {
                new Homework{ Class = "歷史", Subject = "臨一幅《清明上河圖》"},
                new Homework{ Class = "語文", Subject = "作文題:《百鬼日行》"}
            }
        });

        // 儲存
        int x = ctx.SaveChanges();
        Console.WriteLine("共儲存了{0}條記錄", x);
    }
}

EnsureCreated 方法會自動建立資料庫。如果不存在資料庫且建立成功,返回 true,否則是 false。資料庫的名稱在連線字串中設定過。

Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=stuDB;Integrated Security=True

接下來,我們執行一下。稍等幾秒鐘,看到控制檯輸出下面文字就算成功了。

已建立資料庫
共儲存了6條記錄

然後,連上去看看有沒有資料庫。

看看,這表的名稱是不是和 MyDbContext 的兩個屬性一樣? 

public class MyDbContext : DbContext
{
    public DbSet<Student> Students => Set<Student>();
    public DbSet<Homework> Works => Set<Homework>();
    ……

你要是不喜歡用這倆名字,也可以發動傳統技能(指老 EF),用 Table 特性給它們另取高名。

[Table("tb_students", Schema = "dbo")]
public class Student
{
   ……
}

[Table("tb_homeworks", Schema = "dbo")]
public class Homework
{
    ……
}

刪除資料庫,再執行一次程式,然後再登入資料庫看看,表名變了嗎?

那有夥伴們會問:有沒有現代技能?有的,使用 ToTable 方法定義對映的資料表名稱。

先去掉 Student、Homework 類上的 Table 特性,然後直接在重寫 OnModelCreating 方法時設定。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>().ToTable("dt_students").HasKey(s => s.StuID);
    modelBuilder.Entity<Homework>().ToTable("dt_works");
    // 建立主從關係
    modelBuilder.Entity<Student>().OwnsMany(s => s.Homeworks);
}

但是這樣寫會報錯的。因為 Homework 實體是 Student 的從屬物件,單獨呼叫 ToTable 方法在設定的時候會將其設定為獨立物件,而非從屬物件。

所以,正確的做法是在兩個實體建立了從屬性關係後再呼叫 ToTable 方法(Student 物件是主物件,它可以單獨呼叫)。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>().HasKey(s => s.StuID);
    modelBuilder.Entity<Student>()
        .ToTable("tb_students")
        .OwnsMany(s => s.Homeworks)
        .ToTable("tb_works");
}

因為 Homework 是 Student 的從屬,tb_works 表中要存在一個外來鍵——參照 Student.StuID,這樣兩個表才能建立主從關係。如果單獨呼叫 Entity<Homework>.ToTable 對映表的話,那麼表中不會新增參照 StuID 的外來鍵列。就是預設被設定為非主從模式。沒有了外來鍵,tb_works 表中存的資料就無法知道是哪位學生的作業了。

這樣建立資料庫後,tb_works 表中就存在名為 StudentStuID 的列,它就是參照 Student.StuID 的外來鍵。

CREATE TABLE [dbo].[tb_works] (
    [StudentStuID] INT            NOT NULL,
    [Id]           INT            IDENTITY (1, 1) NOT NULL,
    [Class]        NVARCHAR (MAX) NULL,
    [Subject]      NVARCHAR (MAX) NULL,
    CONSTRAINT [PK_tb_works] PRIMARY KEY CLUSTERED ([StudentStuID] ASC, [Id] ASC),
    CONSTRAINT [FK_tb_works_tb_students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[tb_students] ([StuID]) ON DELETE CASCADE
);

當然,這個外來鍵名字是根據實體類名(Student)和它的主鍵屬性名(StuID)生成的,如果你想自己搞個名字,也是可以的。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>().HasKey(s => s.StuID);
    modelBuilder.Entity<Student>()
        .ToTable("tb_students")
        .OwnsMany(s => s.Homeworks, tb =>
        {
            tb.ToTable("tb_works");
            tb.WithOwner().HasForeignKey("student_id");
        });
}

這樣 tb_works 表中就有了名為 student_id 的外來鍵。

CREATE TABLE [dbo].[tb_works] (
    [student_id] INT            NOT NULL,
    [Id]         INT            IDENTITY (1, 1) NOT NULL,
    [Class]      NVARCHAR (MAX) NULL,
    [Subject]    NVARCHAR (MAX) NULL,
    CONSTRAINT [PK_tb_works] PRIMARY KEY CLUSTERED ([student_id] ASC, [Id] ASC),
    CONSTRAINT [FK_tb_works_tb_students_student_id] FOREIGN KEY ([student_id]) REFERENCES [dbo].[tb_students] ([StuID]) ON DELETE CASCADE
);

OwnsXXX 方法是指:俺是主表,我要「關照」一下從表;

WithOwner 方法是指:俺是從表,我要設定一下和主表之間建立聯絡的引數(如上面給外來鍵另起個名字)。

那麼,我想把兩個表的列全自定義命名,可以嗎?當然可以的。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>().HasKey(s => s.StuID);
    modelBuilder.Entity<Student>()
        .ToTable("tb_students", tb =>
        {
            tb.Property(s => s.StuID).HasColumnName("sID");
            tb.Property(s => s.Name).HasColumnName("stu_name");
        })
        .OwnsMany(s => s.Homeworks, tb =>
        {
            tb.ToTable("tb_works");
            tb.WithOwner().HasForeignKey("student_id");
            tb.Property(w => w.Class).HasColumnName("wk_class");
            tb.Property(w => w.Subject).HasColumnName("wk_sub");
        });
}

兩個表的欄位名都變了。

CREATE TABLE [dbo].[tb_students] (
    [sID]      INT            IDENTITY (1, 1) NOT NULL,
    [stu_name] NVARCHAR (MAX) NULL,
    CONSTRAINT [PK_tb_students] PRIMARY KEY CLUSTERED ([sID] ASC)
);

CREATE TABLE [dbo].[tb_works] (
    [student_id] INT            NOT NULL,
    [Id]         INT            IDENTITY (1, 1) NOT NULL,
    [wk_class]   NVARCHAR (MAX) NULL,
    [wk_sub]     NVARCHAR (MAX) NULL,
    CONSTRAINT [PK_tb_works] PRIMARY KEY CLUSTERED ([student_id] ASC, [Id] ASC),
    CONSTRAINT [FK_tb_works_tb_students_student_id] FOREIGN KEY ([student_id]) REFERENCES [dbo].[tb_students] ([sID]) ON DELETE CASCADE
);

注意:Homework 類中沒有定義 Id 屬性(主鍵),它是自動生成的。

 

有大夥伴會想,在 OnModelCreating 方法中建模我頭有點暈,我能不能在定義實體類的時候,直接通過特性批註來實現主從關係呢?那肯定可以的了。

[Table("tb_students")]
[PrimaryKey(nameof(StuID))]
public class Student
{
    [Column("sID")]
    public int StuID { get; set; }

    [Column("st_name")]
    public string? Name { get; set; }

    // 這是導航屬性,不需要對映到資料表
    public IEnumerable<Homework>? Homeworks { get; set; }
}

[Owned]
[Table("tb_homeworks")]
[PrimaryKey(nameof(wID))]
public class Homework
{
    [Column("wk_id")]
    public int wID { get; set; }

    [Column("wk_class")]
    public string? Class { get; set; }

    [Column("wk_sub")]
    public string? Subject { get; set; }

    [ForeignKey("student_id")]  //設定外來鍵名稱
    public Student? StudentObj { get; set; }
}

PrimaryKey 特性設定實體類中哪些屬性為主鍵,使用屬性成員的名稱,而不是資料表欄位名稱。

在 Homework 類上用到 Owned 特性,表示其他物件如果參照了 Homework,就會自動建立主從關係—— Homework 為從屬物件。

ForeignKey 特性指定外來鍵的名稱。雖然 StudentObj 屬性的型別是 Student 類,但在建立資料表時,只參照了 Student 類的 StuID 屬性。

此時,可以清空 OnModelCreating 方法中的程式碼了。

生成的資料表結構與上文差不多。

CREATE TABLE [dbo].[tb_students] (
    [sID]     INT            IDENTITY (1, 1) NOT NULL,
    [st_name] NVARCHAR (MAX) NULL,
    CONSTRAINT [PK_tb_students] PRIMARY KEY CLUSTERED ([sID] ASC)
);

CREATE TABLE [dbo].[tb_homeworks] (
    [wk_id]      INT            IDENTITY (1, 1) NOT NULL,
    [wk_class]   NVARCHAR (MAX) NULL,
    [wk_sub]     NVARCHAR (MAX) NULL,
    [student_id] INT            NULL,
    CONSTRAINT [PK_tb_homeworks] PRIMARY KEY CLUSTERED ([wk_id] ASC),
    CONSTRAINT [FK_tb_homeworks_tb_students_student_id] FOREIGN KEY ([student_id]) REFERENCES [dbo].[tb_students] ([sID])
);

當然了,最好的做法是將特性批註與 OnModelCreating  方法結合使用。