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())
.Map<CreditCard>(m => m.Requires("BillingDetailType").HasValue());
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 = "",
CardType =
};
User user = new User()
{
UserId = ,
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 = }; 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 =
};
CreditCard creditCard = new CreditCard()
{
BillingDetailId = ,
CardType =
}; context.BillingDetails.Add(bankAccount);
context.BillingDetails.Add(creditCard); context.SaveChanges();
}
如果User中有BillingDetail的引用,查询的话会生成如下sql语句
如上所示,将两个子类的表进行联合查询Union All
选择策略指南
我想强调的是,没有一个单一的“最佳策略适合所有场景”存在
每种方法都有各自的优点和缺点
如果确实不需要多态关联或查询,用TPC(换句话说,如果你不查询或者很少查询BillingDetails ,并且你没有与BillingDetail基类相关联的类),推荐用TPC,
如果你需要多态关联或查询,和子类声明相对较少的性能(特别是如果子类之间的主要区别是他们的行为),倾向于TPH。你的目标是减少可为空的列数和说服自己(和你的DBA),一个规范化的模式不会产生长期的问题。
如果你需要多态关联或查询,和子类(子类声明的许多性质不同主要由他们所持有的数据),倾向于TPT。或者,取决于继承层次结构的宽度和深度,以及联接与工会的可能成本,请使用TPC。
EF Code-First 学习之旅 继承策略的更多相关文章
- EF Code First学习笔记
EF Code First学习笔记 初识Code First EF Code First 学习笔记:约定配置 Entity Framework 复杂类型 Entity Framework 数据生成选项 ...
- EF Code First学习系列
EF Model First在实际工作中基本用不到,前段时间学了一下,大概的了解一下.现在开始学习Code First这种方式.这也是在实际工作中用到最多的方式. 下面先给出一些目录: 1.什么是Co ...
- EF Code First 学习笔记:表映射
多个实体映射到一张表 Code First允许将多个实体映射到同一张表上,实体必须遵循如下规则: 实体必须是一对一关系 实体必须共享一个公共键 观察下面两个实体: public class Perso ...
- EF Code First 学习笔记:表映射 多个Entity到一张表和一个Entity到多张表
多个实体映射到一张表 Code First允许将多个实体映射到同一张表上,实体必须遵循如下规则: 实体必须是一对一关系 实体必须共享一个公共键 观察下面两个实体: public class Per ...
- EF Code First 学习笔记:表映射(转)
多个实体映射到一张表 Code First允许将多个实体映射到同一张表上,实体必须遵循如下规则: 实体必须是一对一关系 实体必须共享一个公共键 观察下面两个实体: public class Per ...
- EF Code First学习笔记 初识Code First
Code First是Entity Framework提供的一种新的编程模型.通过Code First我们可以在还没有建立数据库的情况下就开始编码,然后通过代码来生成数据库. 下面通过一个简单的示例来 ...
- EF Code First学习笔记 初识Code First(转)
Code First是Entity Framework提供的一种新的编程模型.通过Code First我们可以在还没有建立数据库的情况下就开始编码,然后通过代码来生成数据库. 下面通过一个简单的示例来 ...
- EF Code First 学习笔记:约定配置 Data Annotations+Fluent API
要更改EF中的默认配置有两个方法,一个是用Data Annotations(在命名空间System.ComponentModel.DataAnnotations;),直接作用于类的属性上面;还有一个就 ...
- EF Code First 学习笔记:关系
一对多关系 项目中最常用到的就是一对多关系了.Code First对一对多关系也有着很好的支持.很多情况下我们都不需要特意的去配置,Code First就能通过一些引用属性.导航属性等检测到模型之 ...
随机推荐
- ASP.NET MVC 使用dataTable(3)--更多选项参考
ASP.NET MVC 使用dataTable(3)--更多选项参考 jQuery dataTables 插件是一个优秀的表格插件,是后台工程师的福音!它提供了针对数据表格的排序.浏览器分页.服务器 ...
- FastExcel遇到的问题
第一次使用FastExcel发现在创建excel文件的时候不成功,一直报这个问题: org.apache.poi.EmptyFileException: The supplied file was e ...
- jq cookie
//$.cookie("xx");//读取xx的值 //$.cookie("xx","123");//设置xx的值为123 //$.cook ...
- Asp.Net MVC anti-forgery token的问题:nameidentifier or identityprovider not present
当使用ClaimsIdentity的时候,Asp.Net MVC在生成AntiForgeryToken的时候会默认使用User.Identity中两种ClaimsType的值:NameIdentifi ...
- LeNet5
Lecun Y, Bottou L, Bengio Y, et al. Gradient-based learning applied to document recognition[J]. Proc ...
- ubuntu搭建mib2c环境
1.下载net-snmphttp://net-snmp.sourceforge.net/download.html例如,下载5.5版本2.进入下载目录,解压net-snmp压缩包#tar zxf ne ...
- python中颜色设置
实现过程: 终端的字符颜色使用转义序列控制的,是文本模式下的系统显示功能,和具体的语言无关. 转义序列是以ESC开头,即用\033来表示(ESC是ASCII码用十进制表示是27,用八进制表示就是033 ...
- C#将图片白色背景设置为透明
Image image = System.Drawing.Image.FromFile(@"C:\A.JPG"); Bitmap pbitmap = new Bitmap(imag ...
- Hibernate学习---缓存机制
前言:这些天学习效率比较慢,可能是手头的事情比较多,所以学习进度比较慢. 在之前的Hibernate学习中,我们无论是CURD,对单表查询还是检索优化,我们好像都离不开session,session我 ...
- 面对 to B 业务该如何构建研发管理体系?
未来离我们越来越近,而过去并未走远,我们发现科技公司2B业务兴起,腾讯认为互联网下半场属于产业互联网,需要进行一次重要的战略升级.它们在国庆节最后一天进行新一轮组织架构调整,最亮眼的就是新成立云与智慧 ...