CodeFirst与EntityFramework【续】

3、    实现一对一的关系。
在介绍一对多关系和多对多关系时,大家应该已经注意到了只要存在依赖关系的两个类的定义中包含对方的实例或实例的集合,Entity Framework Code First会自动推断出与之对应的数据库关系。这个方式对一对一关系也同样适用吗?先让我们来作一个实验。
假设我们的订单系统现在需要存储每个客户的银行账号信息。显然,在我们的订单系统中,银行账号并不是我们关注的重点,我只需要保存账号的号码,开户行以及账号名称,由此可见银行账号在我们这里只是一个值对象(Value Object)。

我们需要定义银行账号类:
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根据类之间的依赖关系推断并建立一对一关系,你必须帮助它,告诉他哪个是主表,哪个是子表。

这里做个备注:关于值对象(Value Object):
什么情况下用值对象?
i、定义的Model类中字段都是基本类型;
ii、所谓的值对象就是一些没有生命周期,也没有业务逻辑上唯一标识符的类(也就是说无关紧要、可有可无的Model类)。
iii、哪些类是Entity,哪些类是Value Object不是固定的,取决于具体的业务逻辑。


假设一个银行账号必须有对应的客户,但是客户可以没有银行账号,并且由于银行账号是个值对象,没有必要让它包含客户类的实例。
因为银行账号类的定义中并不包含客户类的实例,所以我们需要在客户类的配置方法中设定这个一对一关系。
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也是用于决定主键在哪个表的。

Code First处理类之间的继承关系
1.Table Per Hierarchy(TPH): 只建立一个表,把基类和子类中的所有属性都映射为表中的列。
Code First默认会把基类和子类的所有属性都映射成一个表中的列,并且会增加一个Discriminator列标识存进去的是哪个类的实例。
如:
 
2.Table Per Type(TPT): 为基类和每个子类建立一个表,每个与子类对应的表中只包含子类特有的属性对应的列。
在这种处理方式中,Entity Framework Code First会为每个基类和子类建立一个表,子类的表中只包含子类特有的属性。
如:
 
3.Table Per Concrete Type(TPC):为每个子类建立一个表,每个与子类对应的表中包含基类的属性对应的列和子类特有属性对应的列。
Code First默认使用的是TPC方式。

在这种处理方式中,Entity Framework Code First为每一个子类建立一个表,在子类对应的表中除了子类特有的属性外还有基类的属性对应的表。
和TPT一样,我们也需要通过Map方法进行设置。
public class SalesPersonValueObjectConfiguration: EntityTypeConfiguration<SalesPerson>
{
    public SalesPersonValueObjectConfiguration()
    {
        HasKey(p => p.EmployeeID).Property(p => p.EmployeeID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        Property(p => p.Name).IsRequired().HasMaxLength(100);
        Property(p => p.Gender).IsRequired().HasMaxLength(1);
        Map<SalesMan>(salesman => { salesman.ToTable("SalesMan"); salesman.MapInheritedProperties(); });
        Map<SalesManager>(manager => { manager.ToTable("Manager"); manager.MapInheritedProperties(); });
    }
}

通过MapInheritedProperties方法就可以强制Code First使用TPC方式。

我们重新编译之后执行我们原来的测试方法,可以得到不同的数据表结构,Code First不会为基类建立表,而是为每个子类都建立一个表,将子类的内容和基类的内容都存储到各个子类对应的表中。
如:
 
PS:如果你的基类是abstract,效果也是一样的。
最后需要探讨的一个问题是我们在实际项目中应该使用哪种方式呢?

1.不推荐使用TPC(Type Per Concrete Type),因为在TPC方式中子类中包含的其他类的实例或实例集合不能被映射为表之间的关系。你必须通过手动地在类中添加依赖类的主键属性,从而让Code First感知到它们之间的关系,而这种方式是和使用Code First的初衷相反的。
2.从查询性能上来说,TPH会好一些,因为所有的数据都存在一个表中,不需要在数据查询时使用join。
3.从存储空间上来说,TPT会好一些,因为使用TPH时所有的列都在一个表中,而表中的记录不可能使用所有的列,于是有很多列的值是null,浪费了很多存储空间。
4.从数据验证的角度来说,TPT好一些,因为TPH中很多子类属性对应的列是可为空的,就为数据验证增加了复杂性。
所以说具体的项目中选择哪种方式取决于你的实际项目需要。

本文内容来源:http://www.cnblogs.com/lk8167/archive/2013/01/22/2871011.html

EF Code First弊端(自己总结):
虽然微软从EF4.1版本开始支持“Code First”这种编程方式,但是这种方式存在以下弊端:
1、配置数据表关联关系复杂。虽然用Fluent API可以动态设置数据表之间的关联关系,如主键、外键、一对多、多对多等,但是设置代码相对复杂,如果代码层次设计不好的话很难读懂和维护;
2、数据迁移麻烦;
3、项目改造时,与现有系统整合性差;
posted @ 2018-10-08 14:28  skybirdzw  阅读(218)  评论(0编辑  收藏  举报