通過上面兩篇日記,我相信大家已經知道了Entity Framework Code First如何根據類之間的依賴關系推斷並建立數據庫中表之間的一對多和多對多關系。這次日記我們將詳細Entity Framework Code First是如何建立數據庫中的一對一關系。
在介紹一對多關系和多對多關系時,大家應該已經注意到了只要存在依賴關系的兩個類的定義中包含對方的實例或實例的集合,Entity Framework Code First會自動推斷出與之對應的數據庫關系。這個方式對一對一關系也同樣適用嗎?先讓我們來作一個實驗。
假設我們的訂單系統現在需要存儲每個客戶的銀行賬號信息。顯然,在我們的訂單系統中,銀行賬號並不是我們關注的重點,我只需要保存賬號的號碼,開戶行以及賬號名稱,由此可見銀行賬號在我們這里只是一個值對象。
我們需要定義銀行賬號類:
public class BankAccount { public string AccountNumber { get; set; } public DateTime CreatedDate { get; set; } public string BankName { get; set; } public string AccountName { get; set; } }
我們還需要在客戶類當中包含一個銀行賬號類的實例。
public class Customer { public string IDCardNumber { get; set; } public string CustomerName { get; set; } public string Gender { get; set; } public Address Address { get; set; } public string PhoneNumber { get; set; } public BankAccount Account { get; set; } }
我們寫一個單元測試程序,看看Entity Framework Code First會不是自動地根據兩個類的依賴關系創建數據庫中的一對一關系。
[TestMethod] public void CanAddCustomerWithBankAccount() { OrderSystemContext unitOfWork = new OrderSystemContext(); CustomerRepository repository = new CustomerRepository(unitOfWork); Customer newCustomer = new Customer() { IDCardNumber = "120104198106072518", CustomerName = "Alex", Gender = "M", PhoneNumber = "test" }; Address customerAddress = new Address { Country = "China", Province = "Tianjin", City = "Tianjin", StreetAddress = "Crown Plaza", ZipCode = "300308" }; BankAccount account = new BankAccount { AccountNumber = "2012001001", BankName = "ICBC", AccountName = "Alex", CreatedDate = DateTime.Parse("2012-1-21") }; newCustomer.Address = customerAddress; newCustomer.Account = account; repository.AddNewCustomer(newCustomer); unitOfWork.CommitChanges(); }
我們運行一下我們的單元測試程序,程序會拋出異常:
Test method EntityFramework.CodeFirst.Demo1.UnitTest.CustomerRepositoryUnitTest.CanAddCustomerWithBankAccount threw exception: System.Data.DataException: An exception occurred while initializing the database. See the InnerException for details. ---> System.Data.Entity.Infrastructure.DbUpdateException: Null value for non-nullable member. Member: 'Account'. ---> System.Data.UpdateException: Null value for non-nullable member. Member: 'Account'.
這是為什么呢?因為Entity Framework Code First無法根據類之間的依賴關系推斷並建立一對一關系,它根本搞不清楚在這兩個存在依賴關系的類中,哪個是主表,哪個是子表,外鍵應該建立在哪個表中。一對多關系中非常容易分清主表和子表,哪個類中包含另一個的實例集合,它就是主表。多對多關系是通過連接表建立的,不需要分清主表和子表。但是到一對一關系時,這就是個問題了。
要想讓Entity Framework Code First根據類之間的依賴關系推斷並建立一對一關系,你必須幫助它,告訴他哪個是主表,哪個是子表。
假設一個銀行賬號必須有對應的客戶,但是客戶可以沒有銀行賬號,並且由於銀行賬號是個值對象,沒有必要讓它包含客戶類的實例。
因為銀行賬號類的定義中並不包含客戶類的實例,所以我們需要在客戶類的配置方法中設定這個一對一關系。
public class CustomerEntityConfiguration:EntityTypeConfiguration<Customer> { public CustomerEntityConfiguration() { HasKey(c => c.IDCardNumber).Property(c => c.IDCardNumber).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); this.Property(c => c.IDCardNumber).HasMaxLength(20); this.Property(c => c.CustomerName).IsRequired().HasMaxLength(50); this.Property(c => c.Gender).IsRequired().HasMaxLength(1); this.Property(c => c.PhoneNumber).HasMaxLength(20); this.HasOptional(c => c.Account).WithOptionalDependent(); } }
在客戶類中的HasOptional意味着客戶類可以有也可以沒有銀行賬號。當我們通過HasOptional指定Customer與BankAccount類的關系時,Entity Framework Code First要求我們指定它們之間的依賴關系。這時,IntelliSense會讓你在兩個方法之間進行選擇:WithOptionalDependent和WithOptionalPrincipal。如果你選擇WithOptionalDependent則代表Customer表中有一個外鍵指向BankAccount表的主鍵,如果你選擇WithOptionalPrincipal則相反,BankAccount擁有指向Customer表的外鍵。
執行一下我們的單元測試,這次就不會報錯了。然后我們打開SQL Server,我們發現Entity Framework Code First映射到正確的一對一關系。
大家可以看到Customer表中的外鍵是可以為空的,這是由於我們使用了HasOptional。如果我們需要一對一關系中的外鍵不能為空,我們就需要使用HasRequired.
HasRequired(c => c.Account).WithRequiredDependent();
當我們使用HasRequired時候,IntelliSense會讓你在WithRequiredDependent和WithRequiredPrinciple之間選擇, 這里的dependent和principle也是用於決定主鍵在哪個表的。
但是有一點非常奇怪,我使用的Entity Framework版本是4.1,當我執行我的測試程序時,我發現一個非常奇怪的事情。Code First並不是按照HasOptional的那樣建立一個外鍵,而是把Customer的主鍵同時變為指向BankAccount表的外鍵:
這肯定不是我們希望的結果,因為Code First會把AccountNumber當成IDCardNumber存進去。我覺得這可能是EF4.1中的一個bug。
但是我們可以通過我們前面學過的指定外鍵名稱的方法來定義正確的外鍵。
HasRequired(c => c.Account).WithRequiredDependent().Map(c => c.MapKey("AccountId"));
我們再次執行我們的測試程序得到的就是正確的結果了:
由於我在前面兩篇日記中已經介紹了級聯刪除以及更改外鍵名稱的方法,我在這篇日記中就不再重復了。我們下一篇的日記將探討如何將類之間的繼承關系映射到數據庫中去。