EF Core中如何正確地設定兩張表之間的關聯關係
資料庫
假設現在我們在SQL Server資料庫中有下面兩張表:
Person表,代表的是一個人:
CREATE TABLE [dbo].[Person]( [ID] [int] IDENTITY(1,1) NOT NULL, [PersonCode] [nvarchar](20) NULL, [Name] [nvarchar](50) NULL, [Age] [int] NULL, CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX =OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY], CONSTRAINT [IX_Person] UNIQUE NONCLUSTERED ( [PersonCode] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS =ON) ON [PRIMARY] ) ON [PRIMARY] GO
其主鍵是ID,而且主鍵是自增列。Person表還有個PersonCode列是唯一鍵,然後Name和Age列用來描述一個人的名字和年齡。
Book表,代表的是一本書:
CREATE TABLE [dbo].[Book]( [ID] [int] IDENTITY(1,1) NOT NULL, [BookCode] [nvarchar](20) NULL, [PersonCode] [nvarchar](20) NULL, [BookName] [nvarchar](50) NULL, [ISBN] [nvarchar](20) NULL, CONSTRAINT [PK_Book] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY], CONSTRAINT [IX_Book] UNIQUE NONCLUSTERED ( [BookCode] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO
其主鍵是ID,而且主鍵也是自增列。Book表的BookCode列是唯一鍵,Book表的PersonCode列引用Person表的PersonCode列值,所以Book表的PersonCode列實際上是外來鍵,但是我們並沒有在資料庫中設定兩張表之間的外來鍵關係,我們將稍後在EF Core中的實體之間設定外來鍵關係,來演示就算在資料庫中沒有設定外來鍵,EF Core也可以設定實體之間的外來鍵關係。 所以Person表和Book表實際上是一對多關係,通過兩張表的PersonCode列,一個Person對應多個Book,表示一個人可以擁有多本書。
實體
新建一個.NET Core控制檯專案,現在我們在EF Core中建立Person表和Book表的實體:
Person實體,對應資料庫的Person表,其屬性Book是一個ICollection<Book>型別的Book實體集合,表示一個Person實體包含多個Book實體:
public partial class Person { public int Id { get; set; } public string PersonCode { get; set; } public string Name { get; set; } public int? Age { get; set; } //通過Person實體的Book屬性,可以找到多個Book實體,說明Person表是一對多關係中的主表 public virtual ICollection<Book> Book { get; set; } }
Book實體,對應資料庫的Book表,其屬性Person是一個Person實體,表示一個Book實體只能找到一個Person實體:
public partial class Book { public int Id { get; set; } public string BookCode { get; set; } public string PersonCode { get; set; } public string BookName { get; set; } public string Isbn { get; set; } //通過Book實體的Person屬性,可以找到一個Person實體,說明Book表是一對多關係中的從表 public virtual Person Person { get; set; } }
然後是繼承DbContext的TestDBContext類,其中最重要的地方是OnModelCreating方法中設定Person實體和Book實體一對多關係的Fluent API,每一行都寫明瞭註釋:
public partial class TestDBContext : DbContext { public TestDBContext() { } public TestDBContext(DbContextOptions<TestDBContext> options) : base(options) { } public virtual DbSet<Book> Book { get; set; } public virtual DbSet<Person> Person { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer("Server=localhost;User Id=sa;Password=Dtt!123456;Database=TestDB"); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>(entity => { entity.HasKey(e => e.BookCode);//設定Book實體的BookCode屬性為EF Core實體的Key屬性 entity.HasIndex(e => e.BookCode) .HasName("IX_Book") .IsUnique(); entity.Property(e => e.Id).ValueGeneratedOnAdd();//設定Book實體的Id屬性為插入資料到資料庫Book表時自動生成,因為Book表的ID列為自增列 entity.Property(e => e.Id).HasColumnName("ID"); entity.Property(e => e.BookCode).HasMaxLength(20); entity.Property(e => e.BookName).HasMaxLength(50); entity.Property(e => e.Isbn) .HasColumnName("ISBN") .HasMaxLength(20); entity.Property(e => e.PersonCode).HasMaxLength(20); }); modelBuilder.Entity<Person>(entity => { entity.HasKey(e => e.PersonCode);//設定Person實體的PersonCode屬性為EF Core實體的Key屬性 entity.HasIndex(e => e.PersonCode) .HasName("IX_Person") .IsUnique(); entity.Property(e => e.Id).ValueGeneratedOnAdd();//設定Person實體的Id屬性為插入資料到資料庫Person表時自動生成,因為Person表的ID列為自增列 entity.Property(e => e.Id).HasColumnName("ID"); entity.Property(e => e.Name).HasMaxLength(50); entity.Property(e => e.PersonCode).HasMaxLength(20); //設定Person實體和Book實體之間的一對多關係,儘管我們並沒有在資料庫中建立Person表和Book表之間的一對多外來鍵關係,但是我們可以用EF Core的Fluent API在實體層面設定外來鍵關係 entity.HasMany(p => p.Book)//設定Person實體通過屬性Book可以找到多個Book實體,表示Person表是一對多關係中的主表 .WithOne(b => b.Person)//設定Book實體通過屬性Person可以找到一個Person實體,表示Book表是一對多關係中的從表 .HasPrincipalKey(p => p.PersonCode)//設定Person表的PersonCode列為一對多關係中的主表鍵 .HasForeignKey(b => b.PersonCode)//設定Book表的PersonCode列為一對多關係中的從表外來鍵 .OnDelete(DeleteBehavior.ClientSetNull);//設定一對多關係的級聯刪除效果為DeleteBehavior.ClientSetNull }); } }
示例程式碼
現在我們來設想下面一個場景:
假設資料庫中的Person表有一行資料如下:
資料庫中的Book表有三行資料如下:
可以看到Book表三行資料的PersonCode列都為NULL,那麼我們怎麼在EF Core中更改Book表三行資料的PersonCode列為Person表的PersonCode列值呢?也就是說將Book表三行資料的PersonCode列都改為Person表的值P001,從而表示James這個人擁有三本書。
本例的示例程式碼都寫在了.NET Core控制檯專案的Program類中,這裡先將程式碼全部貼出來:
class Program { /// <summary> /// 初始化Person表和Book表的資料,沒有設定Book表的外來鍵列PersonCode的值 /// </summary> static void InitData() { //初始化資料庫資料 using (var dbContext = new TestDBContext()) { var james = new Person() { PersonCode = "P001", Name = "James", Age = 30 }; dbContext.Person.Add(james); var chineseBook = new Book() { BookCode = "B001", Isbn = "001", BookName = "Chinese" };//沒有設定Book表中外來鍵列PersonCode的值 var japaneseBook = new Book() { BookCode = "B002", Isbn = "001", BookName = "Japanese" };//沒有設定Book表中外來鍵列PersonCode的值 var englishBook = new Book() { BookCode = "B003", Isbn = "001", BookName = "English" };//沒有設定Book表中外來鍵列PersonCode的值 //插入三條資料到Book表 dbContext.Book.Add(chineseBook); dbContext.Book.Add(japaneseBook); dbContext.Book.Add(englishBook); dbContext.SaveChanges(); } } /// <summary> /// 刪除Person表和Book表的所有資料 /// </summary> static void DeleteAllData() { using (var dbContext = new TestDBContext()) { dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[Book]"); dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[Person]"); } } /// <summary> /// 不正確地設定Person表和Book表的關聯關係,這種方法會讓EF Core錯誤地生成INSERT語句,而不是UPDATE語句 /// </summary> static void SetRelationshipIncorrectly() { using (var dbContext = new TestDBContext()) { var james = dbContext.Person.First(e => e.Name == "James");//首先通過DbContext從資料庫中查詢出要建立關聯關係的Person表實體 var chineseBook = new Book() { BookCode = "B001" };//只構造Book實體的Key屬性即可,根據BookCode值"B001"來構造Chinese Book var japaneseBook = new Book() { BookCode = "B002" };//只構造Book實體的Key屬性即可,根據BookCode值"B002"來構造Japanese Book var englishBook = new Book() { BookCode = "B003" };//只構造Book實體的Key屬性即可,根據BookCode值"B003"來構造English Book Console.WriteLine($"Before adding, chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//可以看到由於此時Book實體chineseBook沒有被DbContext跟蹤,所以狀態是Detached Console.WriteLine($"Before adding, japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//可以看到由於此時Book實體japaneseBook沒有被DbContext跟蹤,所以狀態是Detached Console.WriteLine($"Before adding, englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//可以看到由於此時Book實體englishBook沒有被DbContext跟蹤,所以狀態是Detached Console.WriteLine(); james.Book = new List<Book>();//由於我們在上面呼叫dbContext.Person.First(e => e.Name == "James")時,沒有用EF Core中Eager Loading的Include方法來載入Book實體集合,所以這裡要用List類來構造一個Book實體集合,否則james.Book為null james.Book.Add(chineseBook);//新增chineseBook到Person類的Book實體集合 Console.WriteLine("chineseBook was added into Person.Book collection"); james.Book.Add(japaneseBook);//新增japaneseBook到Person類的Book實體集合 Console.WriteLine("japaneseBook was added into Person.Book collection"); james.Book.Add(englishBook);//新增englishBook到Person類的Book實體集合 Console.WriteLine("englishBook was added into Person.Book collection"); Console.WriteLine(); Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//呼叫DbContext.Entry()方法後,DbContext發現一個原本狀態是Detached的Book實體chineseBook被加入到Person.Book集合中了,所以此時chineseBook的實體狀態變為了Added Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//呼叫DbContext.Entry()方法後,DbContext發現一個原本狀態是Detached的Book實體japaneseBook被加入到Person.Book集合中了,所以此時japaneseBook的實體狀態變為了Added Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//呼叫DbContext.Entry()方法後,DbContext發現一個原本狀態是Detached的Book實體englishBook被加入到Person.Book集合中了,所以此時englishBook的實體狀態變為了Added dbContext.SaveChanges();//由於此時chineseBook、japaneseBook和englishBook的EntityState都是Added,所以此時DbContext.SaveChanges方法呼叫後,EF Core生成的是INSERT語句,將chineseBook、japaneseBook和englishBook插入資料庫表Book,導致插入了重複值到唯一鍵列BookCode,所以資料庫報錯 } } /// <summary> /// 正確地設定Person表和Book表的關聯關係,這種方法會讓EF Core正確地生成UPDATE語句,在資料庫中設定Book表的PersonCode列資料 /// </summary> static void SetRelationshipCorrectly() { using (var dbContext = new TestDBContext()) { var james = dbContext.Person.First(e => e.Name == "James");//首先通過DbContext從資料庫中查詢出要建立關聯關係的Person表實體 var chineseBook = new Book() { BookCode = "B001" };//只構造Book實體的Key屬性即可,根據BookCode值"B001"來構造Chinese Book var japaneseBook = new Book() { BookCode = "B002" };//只構造Book實體的Key屬性即可,根據BookCode值"B002"來構造Japanese Book var englishBook = new Book() { BookCode = "B003" };//只構造Book實體的Key屬性即可,根據BookCode值"B003"來構造English Book dbContext.Attach(chineseBook);//將chineseBook關聯到DbContext,開始跟蹤 dbContext.Attach(japaneseBook);//將japaneseBook關聯到DbContext,開始跟蹤 dbContext.Attach(englishBook);//將englishBook關聯到DbContext,開始跟蹤 Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//由於上面chineseBook被Attach到DbContext開始跟蹤了,所以此時chineseBook的實體狀態是Unchanged Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//由於上面japaneseBook被Attach到DbContext開始跟蹤了,所以此時japaneseBook的實體狀態是Unchanged Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//由於上面englishBook被Attach到DbContext開始跟蹤了,所以此時englishBook的實體狀態是Unchanged Console.WriteLine(); james.Book = new List<Book>();//由於我們在上面呼叫dbContext.Person.First(e => e.Name == "James")時,沒有用EF Core中Eager Loading的Include方法來載入Book實體集合,所以這裡要用List類來構造一個Book實體集合,否則james.Book為null james.Book.Add(chineseBook);//新增chineseBook到Person類的Book實體集合 Console.WriteLine("chineseBook was added into Person.Book collection"); james.Book.Add(japaneseBook);//新增japaneseBook到Person類的Book實體集合 Console.WriteLine("japaneseBook was added into Person.Book collection"); james.Book.Add(englishBook);//新增englishBook到Person類的Book實體集合 Console.WriteLine("englishBook was added into Person.Book collection"); Console.WriteLine(); Console.WriteLine($"Berfore querying DbContext.Entry(chineseBook), chineseBook.PersonCode is :{chineseBook.PersonCode ?? "null"}");//此時由於我們還沒有呼叫DbContext.Entry()方法,所以DbContext還無法察覺到chineseBook已經被新增到Person類的Book實體集合了,所以chineseBook.PersonCode為null Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//呼叫DbContext.Entry()方法後,DbContext發現一個原本狀態是Unchanged的Book實體chineseBook被加入到Person.Book集合中了,所以此時chineseBook的實體狀態變為了Modified Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook.PersonCode is :{chineseBook.PersonCode}");//由於上面我們呼叫DbContext.Entry(chineseBook)使得DbContext得知了chineseBook被加入到Person.Book集合中了,所以DbContext還將Book實體的外來鍵屬性PersonCode也進行了賦值,為P001 Console.WriteLine(); Console.WriteLine($"Berfore querying DbContext.Entry(japaneseBook),japaneseBook.PersonCode is :{japaneseBook.PersonCode ?? "null"}");//很有意思的是我們上面在chineseBook上呼叫DbContext.Entry()方法後,japaneseBook的PersonCode屬性也不為null了,變為了P001,說明呼叫一次DbContext.Entry()方法後,會引發DbContext重新檢查所有被跟蹤實體的狀態 Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//在上面為chineseBook呼叫DbContext.Entry()方法時,DbContext同時發現了原本狀態是Unchanged的Book實體japaneseBook,也被加入到了Person.Book集合中,所以japaneseBook的實體狀態也變為了Modified Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook.PersonCode is :{japaneseBook.PersonCode}");//在上面為chineseBook呼叫DbContext.Entry()方法時,DbContext得知了japaneseBook也被加入到了Person.Book集合中,所以DbContext將japaneseBook的PersonCode屬性也賦值為P001了 Console.WriteLine(); Console.WriteLine($"Berfore querying DbContext.Entry(englishBook),englishBook.PersonCode is :{englishBook.PersonCode ?? "null"}");//在上面為chineseBook呼叫DbContext.Entry()方法時,DbContext得知了englishBook也被加入到了Person.Book集合中,所以DbContext將englishBook的PersonCode屬性也賦值為P001了 Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//在上面為chineseBook呼叫DbContext.Entry()方法時,DbContext同時發現了原本狀態是Unchanged的Book實體englishBook,也被加入到了Person.Book集合中,所以englishBook的實體狀態也變為了Modified Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook.PersonCode is :{englishBook.PersonCode}");//在上面為chineseBook呼叫DbContext.Entry()方法時,DbContext得知了englishBook也被加入到了Person.Book集合中,所以DbContext將englishBook的PersonCode屬性也賦值為P001了 dbContext.SaveChanges();//由於此時chineseBook、japaneseBook和englishBook的EntityState都是Modified,所以此時DbContext.SaveChanges方法呼叫後,EF Core生成的是UPDATE語句,通過更新資料庫Book表的PersonCode列,將Chinese、Japanese和English三行Book資料同Person表的資料成功關聯了起來 } } static void Main(string[] args) { DeleteAllData();//呼叫DeleteAllData方法刪除Person表和Book表的所有資料,防止有髒資料 InitData();//初始化Person表和Book表的資料 SetRelationshipCorrectly();//正確地的設定Person表和Book表的關聯關係 SetRelationshipIncorrectly();//不正確地的設定Person表和Book表的關聯關係,該方法會丟擲異常錯誤 Console.WriteLine("Press any key to quit..."); Console.ReadKey(); } }
示例程式碼中的DeleteAllData方法,是清表語句,用來刪除Person表和Book表的所有資料,防止有髒資料。
InitData方法用來初始化Person表和Book表的資料,Person表插入了一行資料,Book表插入了三行資料且PersonCode列都為NULL,呼叫InitData方法後資料庫Person表和Book表的資料就和上面示例程式碼前的兩個截圖相同了。
測試SetRelationshipIncorrectly方法
先將示例程式碼的Main方法改為如下:
static void Main(string[] args) { DeleteAllData();//呼叫DeleteAllData方法刪除Person表和Book表的所有資料,防止有髒資料 InitData();//初始化Person表和Book表的資料 //SetRelationshipCorrectly();//正確地的設定Person表和Book表的關聯關係 SetRelationshipIncorrectly();//不正確地的設定Person表和Book表的關聯關係,該方法會丟擲異常錯誤 Console.WriteLine("Press any key to quit..."); Console.ReadKey(); }
SetRelationshipIncorrectly方法用來演示怎麼錯誤地設定Person表和Book表的關聯關係,可以看到由於我們在其中新建的三個Book實體
var chineseBook = new Book() { BookCode = "B001" };//只構造Book實體的Key屬性即可,根據BookCode值"B001"來構造Chinese Book var japaneseBook = new Book() { BookCode = "B002" };//只構造Book實體的Key屬性即可,根據BookCode值"B002"來構造Japanese Book var englishBook = new Book() { BookCode = "B003" };//只構造Book實體的Key屬性即可,根據BookCode值"B003"來構造English Book
最終在呼叫DbContext.SaveChanges方法時其實體狀態都是Added,所以呼叫DbContext.SaveChanges方法時,EF Core在資料庫中生成的是INSERT語句,嘗試將這三個實體資料插入資料庫Book表,由於呼叫InitData方法後,資料庫Book表中已經有相同PersonCode列值的資料了,Book表的PersonCode列又是唯一鍵,所以DbContext.SaveChanges方法丟擲異常。
我們可以從EF Core的後臺日誌中,檢視到呼叫DbContext.SaveChanges方法時生成的是INSERT語句:
=============================== EF Core log started =============================== SaveChanges starting for 'TestDBContext'. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== DetectChanges starting for 'TestDBContext'. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== DetectChanges completed for 'TestDBContext'. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Opening connection to database 'TestDB' on server 'localhost'. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Opened connection to database 'TestDB' on server 'localhost'. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Beginning transaction with isolation level 'ReadCommitted'. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Executing update commands individually as the number of batchable commands (3) is smaller than the minimum batch size (4). =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Executing DbCommand [Parameters=[@p0='?' (Size = 20), @p1='?' (Size = 50), @p2='?' (Size = 20), @p3='?' (Size = 20)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; INSERT INTO [Book] ([BookCode], [BookName], [ISBN], [PersonCode]) VALUES (@p0, @p1, @p2, @p3); SELECT [ID] FROM [Book] WHERE @@ROWCOUNT = 1 AND [BookCode] = @p0; =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Failed executing DbCommand (8ms) [Parameters=[@p0='?' (Size = 20), @p1='?' (Size = 50), @p2='?' (Size = 20), @p3='?' (Size = 20)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; INSERT INTO [Book] ([BookCode], [BookName], [ISBN], [PersonCode]) VALUES (@p0, @p1, @p2, @p3); SELECT [ID] FROM [Book] WHERE @@ROWCOUNT = 1 AND [BookCode] = @p0; =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Disposing transaction. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Closing connection to database 'TestDB' on server 'localhost'. =============================== EF Core log finished =============================== =============================== EF Core log started =============================== Closed connection to database 'TestDB' on server 'localhost'. =============================== EF Core log finished ===============================
在SetRelationshipIncorrectly方法中我們還輸出了chineseBook、japaneseBook和englishBook三個Book實體的EntityState,可以看到將chineseBook、japaneseBook和englishBook三個Book實體新增到Person類的Book實體集合後,EntityState發生了相應的變化。