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

假設有以下兩個實體:

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  方法結合使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章