Entity Framework Code First 学习日记(8)-一对一关系
通过上面两篇日记,我相信大家已经知道了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"));
我们再次执行我们的测试程序得到的就是正确的结果了:
由于我在前面两篇日记中已经介绍了级联删除以及更改外键名称的方法,我在这篇日记中就不再重复了。我们下一篇的日记将探讨如何将类之间的继承关系映射到数据库中去。