假設有以下兩個實體:
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 方法結合使用。