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就能通过一些引用属性.导航属性等检测到模型之 ...
随机推荐
- SQL Server 还原错误“restore database正在异常终止 错误 3154”
今天在还原数据库时,先建立相同名字的数据库,然后在该数据库上右键还原数据库.遇到了这样的一个错误: “备份集中的数据库备份与现有的 'RM_DB' 数据库不同. RESTORE DATABASE 正在 ...
- HDU4027(Can you answer these queries?)
题目链接:传送门 题目大意:有一个长度为n的数组,有m次操作,每次操作输入 v x y,v==0时x~y区间内的数都开平方并且向下取整,v==1时求x~y区间内所有数的和. 题目思路:long lon ...
- 巨蟒python全栈开发django3:url&&视图
1.url正则匹配分组和命名分组 2.路由分发 3.url别名和反向解析 4.httprequest和httpresponse的使用 内容回顾: .jinja2(flask框架,没有内置模板对象,需要 ...
- api文档的书写
写文档写要与写代码一样,增加复用. 比如 model 说明就只需要一个,api中含有哪些字段,就在api说明中增加到那些 models 的链接. 使用 sophinx 如何生成目录 .. toctre ...
- p:nth-last-child(2)
<!DOCTYPE html><html><head><style> p:nth-last-child(2){background:#ff0000;}& ...
- 删除datatable中的行
今天遇到一问题,无论如何也删除不干净datatable.我想全部删除.但总是得不到想要的结果. ; i < dt.Rows.Count; i++) { //dt.Rows.Remove(dt.R ...
- iOS 面试题总结
最近项目做完了 比较空闲 在网上看了一份面试题 想自己整理一下 一.为什么说Objective-C是一门动态的语言?NSUInteger和NSInteger 的区别? 静态 动态是相对的,这里的动态语 ...
- C# 调用ArcGIS server admin api
一.AGS server admin api 介绍 1.1什么是admin api AGS Server Admin api 官方的称呼是 AGS Server administrator api, ...
- Python3.6全栈开发实例[024]
24.文件a1.txt内容(注意每行中的空格是不一样的,需要对空格进行处理)序号 部门 人数 平均年龄 备注 1 python 30 26 单身狗 2 Linu ...
- COM 组件创建实例失败,原因是出现以下错误: c001f011 (Microsoft.SqlServer.ManagedDTS)
从 IClassFactory 为 CLSID 为 {AA40D1D6-CAEF-4A56-B9BB-D0D3DC976BA2} 的 COM 组件创建实例失败,原因是出现以下错误: c001f011. ...