EF Code-First 学习之旅 继承策略

Code First中有三种不同的方法表示继承层次关系

 

1.Table per Hierarchy (TPH)

  这种方法建议用一个表来表示整个类的继承层次关系,表中包含一个识别列来区分继承类,在EntityFramework中,这是默认的实现

 


类与数据库表的映射最简单的策略应该是:每个持久类对应一张表。这种方法听起来很简单,继承是一个可见的结构之间的不匹配的面向对象和关系世界,因为面向对象系统模型都是“is a”和“has a”的关系。

SQL中的实体关系都是“has a”的关系。SQL数据库管理系统不支持继承的关系

我将解释这些策略的一个系列,这是一个致力于TPH,在这一系列中,我们将深入挖掘每一个策略,将学习“为什么”来选择它们以及“如何”来实现它们,希望它给你一个比较好的想法在具体的场景中使用哪个策略。

所有的继承映射策略,在这一系列的讨论,我们将第一ctp5 EF代码实现。建立新的ctp5 EF代码库已通过ADO.NET团队本月早些时候公布的。EF代码首先启用了一个功能强大的以代码为中心的开发工作流。我是一个大风扇的EF Code First的方法,我很兴奋的生产力和权力,它带来了很多。当涉及到继承映射,不仅代码第一完全支持所有的策略,但也给你最终的灵活性与涉及继承的域模型。在ctp5继承映射Fluent API有了很大的提高,现在它更直观、简洁的比较ctp4。

如果你遵循EF的“数据库第一”或“模型第一”的方法,我仍然建议阅读本系列,虽然实施是代码第一具体的,但周围的每一个战略的解释是完全适用于所有的方法,它的代码第一或其他。

public abstract class BillingDetail 
{
    public int BillingDetailId { get; set; }
    public string Owner { get; set; }        
    public string Number { get; set; }
}
 
public class BankAccount : BillingDetail
{
    public string BankName { get; set; }
    public string Swift { get; set; }
}
 
public class CreditCard : BillingDetail
{
    public int CardType { get; set; }                
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}
 
public class InheritanceMappingContext : DbContext
{
    public DbSet<BillingDetail> BillingDetails { get; set; }
}

 

定义如上代码,在DbContext中,只定义基类BillingDetail的DbSet,Code First通过类型发现约定找到继承基类的类型

 

多态查询:

  LINQ to Entities 和 EntitySQL作为面向对象查询语言,都支持多态的查询,查询这个对象的所有实例和子类的所有实例,

IQueryable<BillingDetail> linqQuery = from b in context.BillingDetails select b;
List<BillingDetail> billingDetails = linqQuery.ToList();

 


用EntitySQL语言查询

string eSqlQuery = @"SELECT VAlUE b FROM BillingDetails AS b";
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
ObjectQuery<BillingDetail> objectQuery = objectContext.CreateQuery<BillingDetail>(eSqlQuery);
List<BillingDetail> billingDetails = objectQuery.ToList();

linqQuery 和 eSqlQuery 都是多态的并返回类型是BillingDetail的对象集合(BillingDetail是抽象类),不过集合中的具体对象则是BillingDetail的具体子类:BankAccount和CreditCard

 

不是多态的查询:

  所有的LINQ to Entities和EntitySQL查询都是多态的,不仅返回具体实体类的实例,也返回所有的子类。非多态查询是多态性受限的查询,只返回特定子类的实例。在LINQ to Entities中,用OfType<T>方法返回,下面的例子中,查询返回的是BankCount的实例

IQueryable<BankAccount> query = from b in context.BillingDetails.OfType<BankAccount>() 
                                select b;

 

 

string eSqlQuery = @"SELECT VAlUE b FROM OFTYPE(BillingDetails, Model.BankAccount) AS b";

 

 

整个类的层次都映射到一张表中,这个表包含的列为所有类和子类的所有属性,每个子类用特殊的列值来区分

 

 

 

上面的例子中,BillingDetail抽象类的属性和对应子类BankAccount、CreditCard的属性都映射到表中,Code First默认添加列名为Discriminator 的列来区分不同的子类(类型为不可null的nvarchar)

默认值为:BankAccount和CreditCard

TPH要求子类的属性在数据库表中类型为Nullable

TPH有一个主要的问题是:子类的属性在数据库表中的映射类型是Nullable的,例如,Code First创建一列(INT,NULL)对应CreditCard类中的CardType属性,然而,在一个特别的映射场景中,Code First总是创建一列(INT,Not Null)对应实体中的int类型属性。但在这个例子中,BankAccount实例没有CardType属性,在这一行CardType必须为NULL,Code Fist用(INT,NOT NULL)代替。

如果子类中有定义为non-nullable的属性,NOT NULL转换将出现异常

另一个问题是:TPH违反第三范式

决定鉴别器列中的值的列,属于子类的相应值(例如bankname)但鉴频器是不是该表的主键的一部分。

当查询BillingDetails 时,生成如下SQL语句

SELECT 
[Extent1].[Discriminator] AS [Discriminator], 
[Extent1].[BillingDetailId] AS [BillingDetailId], 
[Extent1].[Owner] AS [Owner], 
[Extent1].[Number] AS [Number], 
[Extent1].[BankName] AS [BankName], 
[Extent1].[Swift] AS [Swift], 
[Extent1].[CardType] AS [CardType], 
[Extent1].[ExpiryMonth] AS [ExpiryMonth], 
[Extent1].[ExpiryYear] AS [ExpiryYear]
FROM [dbo].[BillingDetails] AS [Extent1]
WHERE [Extent1].[Discriminator] IN ('BankAccount','CreditCard')

 

查询BankAccount 时,生成如下语句

SELECT 
[Extent1].[BillingDetailId] AS [BillingDetailId], 
[Extent1].[Owner] AS [Owner], 
[Extent1].[Number] AS [Number], 
[Extent1].[BankName] AS [BankName], 
[Extent1].[Swift] AS [Swift]
FROM [dbo].[BillingDetails] AS [Extent1]
WHERE [Extent1].[Discriminator] = 'BankAccount'

 

 

用Fluent API配置识别列

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<BillingDetail>()
                .Map<BankAccount>(m => m.Requires("BillingDetailType").HasValue("BA"))
                .Map<CreditCard>(m => m.Requires("BillingDetailType").HasValue("CC"));
}

 

 

modelBuilder.Entity<BillingDetail>()
            .Map<BankAccount>(m => m.Requires("BillingDetailType").HasValue(1))
            .Map<CreditCard>(m => m.Requires("BillingDetailType").HasValue(2));

 

 

2.Table per Type (TPT):

  这种方法建议用分开的表来表示每个领域里面的类

  Table per Type用关系外键表示继承关系

  每一个类、子类、抽象类都有它们自己的表

子类的表包含的列仅仅是非继承的属性(子类中自己声明的属性),子类表的主键是基类的主键

 

 

例如,如果子类CreditCard持久化,BillingDetail基类中的属性值将被持久化到BillingDetail表中,CreditCard子类中声明的属性被持久化到CreditCard表中,两个表中对应的这两行将共享一个主键

最后,查询子类实例时将联合子类表和基类表进行查询

TPT的好处有:

  主要的好处是这符合SQL规范模式,

 Code First实现TPT

public abstract class BillingDetail
{
    public int BillingDetailId { get; set; }
    public string Owner { get; set; }
    public string Number { get; set; }
}
 
[Table("BankAccounts")]
public class BankAccount : BillingDetail
{
    public string BankName { get; set; }
    public string Swift { get; set; }
}
 
[Table("CreditCards")]
public class CreditCard : BillingDetail
{
    public int CardType { get; set; }
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}
 
public class InheritanceMappingContext : DbContext
{
    public DbSet<BillingDetail> BillingDetails { get; set; }
}

 

 

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<BankAccount>().ToTable("BankAccounts");
    modelBuilder.Entity<CreditCard>().ToTable("CreditCards");
}

 

 

 多态关联:是一个基类的关联,因此说有的层次类都是在运行时才能确定是哪个具体的类

例如,User类中的BillingInfo属性,它引用一个BillingDetail对象,运行时,这个属性可以是这个类的任何一个具体实例

public class User
{
    public int UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int BillingDetailId { get; set; }
 
    public virtual BillingDetail BillingInfo { get; set; }
}
using (var context = new InheritanceMappingContext())
{
    CreditCard creditCard = new CreditCard()
    {                    
        Number   = "987654321",
        CardType = 1
    };                
    User user = new User()
    {
        UserId      = 1,    
        BillingInfo = creditCard
    }; 
    context.Users.Add(user);
    context.SaveChanges();
}

 

 

 查询

 

var query = from b in context.BillingDetails.OfType<BankAccount>() select b;

var query = from b in context.BillingDetails select b;

 

As you can see, EF Code First relies on an INNER JOIN to detect the existence (or absence) of rows in the subclass tables CreditCards and BankAccounts so it can determine the concrete subclass for a particular row of the BillingDetails table. Also the SQL CASE statements that you see in the beginning of the query is just to ensure columns that are irrelevant for a particular row have NULL values in the returning flattened table. (e.g. BankName for a row that represents a CreditCard type)

 

 TPT考虑到的问题:

  虽然这看似简单的映射策略,经验表明,复杂的类层次结构的性能是不能接受的,因为查询总是需要跨多个表的联接,另外,这种映射策略更难以手工实现,

这是一个重要的考虑因素,如果你打算使用手写SQL在您的应用程序,专案报告,数据库视图提供了一种方法来抵消TPT策略的复杂性

 

3.Table per Concrete class (TPC):

  这种方法建议用一个表表示一个具体的类。但是不表示抽象方法。所以,如果在多个具体类中继承抽象方法,抽象类中的属性将是具体类对应表中的一部分

 为每个具体的类型创建一张表(不包括抽象类),类中的所有属性(包括继承属性),都会映射到表中的列

如下

如上面所示,SQL不知道继承,事实上,我们将两个无关的表映射成更具表达性的类结构,如果基类是具体的,则需要额外的表来控制类的实例,必须强调的是这些表是没有什么关联的,实际上除了它们共享的一些列

就像TPT一样,我们需要为每个子类指定分开的表。我们也需要告诉Code First所有的继承属性都映射到表中,

public abstract class BillingDetail
{
    public int BillingDetailId { get; set; }
    public string Owner { get; set; }
    public string Number { get; set; }
}
        
public class BankAccount : BillingDetail
{
    public string BankName { get; set; }
    public string Swift { get; set; }
}
        
public class CreditCard : BillingDetail
{
    public int CardType { get; set; }
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}
    
public class InheritanceMappingContext : DbContext
{
    public DbSet<BillingDetail> BillingDetails { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BankAccount>().Map(m =>
        {
            m.MapInheritedProperties();
            m.ToTable("BankAccounts");
        });
 
        modelBuilder.Entity<CreditCard>().Map(m =>
        {
            m.MapInheritedProperties();
            m.ToTable("CreditCards");
        });            
    }
}

 

 

 

namespace System.Data.Entity.ModelConfiguration.Configuration.Mapping
{
    public class EntityMappingConfiguration<TEntityType> where TEntityType : class
    {
        public ValueConditionConfiguration Requires(string discriminator);
        public void ToTable(string tableName);
        public void MapInheritedProperties();
    }
}

 

 

using (var context = new InheritanceMappingContext())
{
    BankAccount bankAccount = new BankAccount();
    CreditCard creditCard = new CreditCard() { CardType = 1 };
                
    context.BillingDetails.Add(bankAccount);
    context.BillingDetails.Add(creditCard);
 
    context.SaveChanges();
}

 

 

运行上面的代码发生如下异常

The changes to the database were committed successfully, but an error occurred while updating the object context. The ObjectContext might be in an inconsistent state. Inner exception message: AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges.

该原因是因为DbContext.SaveChanges()内部调用了ObjectContext的SaveChanges方法,ObjectContext的调用SaveChanges方法对其将默认调用acceptallchanges后已完成数据库的修改,acceptallchanges方法只遍历所有条目在ObjectStateManager并调用AcceptChanges对它们

当实体处于Added状态,AcceptChanges 方法用数据中生成的主键来替换它们的临时EntityKey,因此当所有实体被赋予相同的主键值时会发生异常,

问题是ObjectStateManager 不能跟踪类型相同且主键相同的对象,打开数据库的表看到的数据是BankAccounts表和CreditCards表有相同的主键值

 

如何解决 TPC中的ID问题

 

如你看到的,SQL Server中的int型标识列不能与TPC很好的一起工作,当插入到子类表中时会复制实体的主键,

因此,解决该问题,可以使用连续的种子,(每个表都有自己的初始种子),使用GUID或者int标识列(以不同的种子开头)可以解决该问题,

public abstract class BillingDetail
{
    [DatabaseGenerated(DatabaseGenerationOption.None)]
    public int BillingDetailId { get; set; }
    public string Owner { get; set; }
    public string Number { get; set; }
}

 

 

modelBuilder.Entity<BillingDetail>()
            .Property(p => p.BillingDetailId)
            .HasDatabaseGenerationOption(DatabaseGenerationOption.None);

 

 

using (var context = new InheritanceMappingContext())
{
    BankAccount bankAccount = new BankAccount() 
    { 
        BillingDetailId = 1                     
    };
    CreditCard creditCard = new CreditCard() 
    { 
        BillingDetailId = 2,
        CardType = 1
    };
                
    context.BillingDetails.Add(bankAccount);
    context.BillingDetails.Add(creditCard);
 
    context.SaveChanges();
}

 

 

如果User中有BillingDetail的引用,查询的话会生成如下sql语句

 

 如上所示,将两个子类的表进行联合查询Union All

 

 选择策略指南

我想强调的是,没有一个单一的“最佳策略适合所有场景”存在

每种方法都有各自的优点和缺点

如果确实不需要多态关联或查询,用TPC(换句话说,如果你不查询或者很少查询BillingDetails ,并且你没有与BillingDetail基类相关联的类),推荐用TPC,

 

如果你需要多态关联或查询,和子类声明相对较少的性能(特别是如果子类之间的主要区别是他们的行为),倾向于TPH。你的目标是减少可为空的列数和说服自己(和你的DBA),一个规范化的模式不会产生长期的问题。

 

如果你需要多态关联或查询,和子类(子类声明的许多性质不同主要由他们所持有的数据),倾向于TPT。或者,取决于继承层次结构的宽度和深度,以及联接与工会的可能成本,请使用TPC。

 

posted @ 2017-03-28 00:53  蓝平凡  阅读(2435)  评论(0编辑  收藏  举报