(翻译)Entity Framework技巧系列之三 - Tip 9 – 12
提示9. 怎样直接删除一个对象而无需检索它
问题
最常见的删除Entity Framework中实体的方式是将你要删除的实体传入Context中并像如下这样删除:
1 // 按ID查找一个类别 2 // 小提示:在.NET4.0中可以使用.Single()方法 3 // 以在有多于1项匹配结果时抛出异常。 4 var category = (from c in ctx.Categories 5 where c.ID == 3 6 select c).First(); 7 // 删除此项 8 ctx.DeleteObject(category); 9 // 保存更改 10 ctx.SaveChanges();
然而这段代码会触发两条数据库命令而不是一条。我的意思是我实际需要完成的就是:
1 DELETE FROM [Categories] WHERE ID = 3
大多数情况下这还不太坏,但是如果性能与可扩展性对你很关键,则这种方式绝不理想。
解决方案
幸运的是Entity Framework提供了名为 AttachTo(…) 的方法,它使你可以在未改变的状态下将实体放入ObjectContext中。
你可以使用这个方法像下面这样"伪造"一个查询:
1 // 创建一个实体来表示你想要删除的实体 2 // 注意你不需要知道所有的属性, 3 // 在这个例子中仅有ID足矣。 4 Category stub = new Category { ID = 4 }; 5 // 将这个category的stub对象attach到”Categories”集中。 6 // 这将实体在未改变的状态下放入context中, 7 // 这是与你做出查询时对象拥有的状态一样的状态 8 ctx.AttachTo("Categories", stub); 9 // 删除这个category对象 10 ctx.DeleteObject(stub); 11 //将删除提交到数据库 12 ctx.SaveChanges();
现在你已经在无需首先进行查询的情况下由数据中删除一个对象。
但是…
这种做法不是很快
在上面的例子中我需要提供给stub对象的仅仅是ID,也就是实体的主键(PK)。但是这也仅因为这是一个来说明核心原理的人为编写的例子。
不幸的是,在现实世界中情况可能不这么简单。
有两种情况使其更复杂。
-
并发值
-
外键(如一个Product拥有一个Category)
如果你要删除的实体有其中之一的情况,在删除实际执行前你需要给Entity Framework提供更多的信息。
并发值
你需要提供并发值原因是相当明确的。在概念模型中,Entity Framework需要使用这些值来验证所有更新与删除是否是针对实体的最新已知非脏版本。
所以当创建用于删除的stub对象时,你需要设置PK与并发值,到数据中当前的值。
如果你知道那些值,没有问题,你只需简单的将其放于stub实体的初始化代码中,就像下面这样:
1 Category stub = new Category { ID = 4, Version = 6 };
如果你不知道它们,哦,你很不走运,你不得不通过查询去得到它们,那样你又回到问题的起点!
外键值
你需要提供外键(FK)值的原因相比就不是很直观了。这是Entity Framework的独立关联模型的一个副作用。这篇文章介绍了独立关联的一些副作用。
注意:当我写这篇文章的时候,我一直使用"独立关联(Independent Associations)","第一级关联(First Class Association)",但是随着这篇FK关联(FK Associations)介绍,事情有所变化。如果我们继续称之"独立关联","第一级关联"可能会给出这样信号即"FK关联"在某种程度上是第二级的。对此混淆我表示愧疚。我不好的表达…
如果你删除了关系任意一端的实体,Entity Framework需要同时删除此关系。但是因为关联被认为是独立的,且由两个东西来决定:依赖实体的主键与(主实体的)外键值,事实上Entity Framework需要FK来标识要被删除的关联。
如果你不理解这个没有关系,这不简单,我也一直与这个概念斗争!
这样事实上产生的结果就是,如果实体有一个于表中的FK,你需要提供此FK的当前值来确保删除可以成功完成。
提供FK值事实上没那么困难,像如下这样简单的创建一个到另一个伪实体的关系即可:
1 // 创建一个要删除的stub对象,同时模拟生成一个Category的stub对象 2 // 并告诉EF在Products这个表中CategoryID这个外键的值 3 Product stub = new Product { 4 ID = 3, 5 Category = new Category { ID = 1 } 6 }; 7 // Attach, 删除并保存更改 8 ctx.AttachTo("Products", stub); 9 ctx.DeleteObject(stub); 10 ctx.SaveChanges();
这样就完成了。
这是为你省下一条数据库命令的正式方法。
将来
如果你使用独立关联,你不得不对外键(FK)百依百顺,在.NET 3.5 SP1中,这是唯一必须的方法,别无选择。然而如果你使用.NET 4.0中新增的FK关联(FK Associations),则不再需要提供FK值,除非你已显式将FK属性标记为一个并发值。
结果呢?
你的代码最终看起来将像我第一个示例那样。
很简单吧。
提示10. 怎样理解Entity Framework的行话
Entity Framework是一个相当大的工程,所以当Entity Framework团队内部讨论一些事情时,我们希望通过使用一些行话来增加我们的共同语言。
不幸的是有时候我们让这些行话流入我们的API与交流中。
如果你参与了这些机密的东西,行话看起来很好,否则,坦白地讲这些行话很令人迷惑。
下面列出了一些较常用的Entity Framework行话的大纲,让我们看一下它们是什么意思:
1. EF
这个最简单,就是Entity Framework!
2. EDM/CSDL/Conceptual Model(概念模型)/Model
所有这些基本上指的是一个东西。CSDL是描述一个Entity Data Model/EDM的XML格式的文档。偶尔当试图尽可能少的使用行话来使意思更可理解,我们说Conceptual Model(概念模型)甚至仅仅是Model。我们完全意识到这已经引起的混淆,并试图尽我们最大努力避免同样的错误再次发生。
3. MetadataWorkspace
这是Entity Framework用来记住数据库,模型,CLR类与它们之间不同的映射所需的所有元数据的容器。最简单的控制这个东西的方法就是通过操作直属于ObjectContext的MetadataWorkspace属性。
4. C-Space, S-Space, O-Space
Entity Framework的MetadataWorkspace将其元数据存储于"工作区"中。所以为了使用元数据API你需要理解这些工作区的含义:
a. C-Space. 这是存储元数据中概念模型的地方,其由CSDL中加载。
b. S-Space. 这是存储元数据中数据库模型的地方,其由SSDL中加载。
c. CS-Space. 这是映射信息存储的地方,对,它由MSL加载而来。
d. O-Space. 这是对象空间,或称CLR空间。其中存储着元数据中与概念模型映射的CLR类型。在.NET3.5 SP1中其由Entity Framework中C LR类型的特性加载而来。
e. OC-Space. EF将概念模型(C-Space)与CLR对象(O-Space)的映射关系存储于此。
5. O-C Mapping
当我们谈及"映射"时,通常是讨论概念模型与数据库模型间的映射,这也是最具灵活性的所在。然而EF中存在另一种类型的映射 – O-C Mapping,即CLR对象(O-Space)与概念模型(C-Space)间的映射。在.NET 3.5 SP1中O-C Mapping的能力非常有限,但在.NET 4.0中由于新增对POCO的支持,这种映射的能力有引人注目的增强,但仍然有很长的路要走…
6. Relationship Fix-up
Relationship Fix-up(关系建立)由EF,与链接(links)相关的实体一起来处理。例如,当一个已知相关的客户可以被由数据库中得到时,将会被设置到一个Order的"Customer"属性。一旦你理解了fix-up,你可以使用其做各类有趣的事情。
7. Relationship Span
如果你读过independent associations(独立关联)与foreign key associations(外键关联)这两篇文章,你知道在.NET 3.5 SP1中,在结构上外键不是实体的一部分。严格地说这意味着当由数据库检索一个实体将不会返回其中的外键值。实际上其后果是这些外键值不会存在于内存中,这将导致各类可用性问题(如Tip7与Tip9中所讨论的问题)。所以为避免这些问题,EF将外键及相关实体一起返回,正如你的猜测,这个特性被称为relationship span(关系跨越)。
8. CQTs与CCTs
Canonical Query Trees(标准查询树与)与Canonical Command Trees(标准命令树)是entity framework发送到数据库提供程序来描述其要在一个提供程序上以不可知(或标准)方式执行的命令。树本身由一系列命令操作符与我们期望所有数据库提供程序支持的函数组成。提供程序的工作是将这些提供程序的未知树翻译为本地SQL命令。
9. POCO
POCO这个缩写或Plain Old CLR Objects这个词汇不是Entity Framework团队发明的,但是我们明确表示会支持这个特性!这种想法是使你可以持久化完全没有依赖的类(如基类,接口或特性)到Entity Framework本身。这是我们团队针对.NET 4.0的主要关注点之一。
自从我加入Microsoft以来我学到的事情之一就是如果有很多人与客户讨论,则不得不对介绍的术语非常明确。如果你习惯盲目且使用令人困惑的内部行话来在客户面前站稳脚跟,误解会常常发生!
如果你知道更多行话,告诉我,我将添加到上述列表中。
提示11. 怎样避免关系跨度
背景与动机
在上一篇博文EF行话中,我介绍了关系跨度(Relationship Span)的概念。
如果你记得关系跨度仅仅是对实体中缺少外键属性的一种补偿。
一个实体的关系跨度,让我们以StaffMember为例,可以确保Entity Framework知道与StaffMember有0..1关系的其它实体的键(EntityKey)(如DisciplineHistory)。
这些键很重要,没有它们Entity Framework不知道怎么删除或更新StaffMember(见提示7与提示9,有更多关于此概念的信息)。
目前通常0..1类型的关系是通过在数据库中由StaffMember表指向目标DisciplineHistory表的外键的外键来确立的。
在这种情况下关系跨度可以很容易的取得DisciplineHistory的键,因为我们可以进行"联合消除"。
虽然没有两个数据库完全一样,且完全有可能以完全不同的方式对相同的关系进行建模:你可以将FK放置在DisciplineHistorty表中。
一种实现这个目的的方法是使相关的DisciplineHistory表的PK与StaffMember的PK相同,换言之,PK即FK。事实上在数据库限制设计为1对0..1关系的情况下,这是EF支持的唯一方法。
这种方式最普遍用于对数据库建模时给实体添加额外的"方面"的情况。
在这些场合下,关系跨度开销有点偏大,因为Entity Framework不足以智能到知道怎么进行联合消除或这本就不可能(两个情况都是可能的)。
如果你更进一步推断这种情况,如有多个通过0..1关系关联的实体存在(例如StaffMember有许多如SalaryHistory,DisciplineHistory,AuditLog与BIO等),这时你会很容易发现,对StaffMember表进行一个简单查询的开销也将很大,这完全由于完成关系跨度操作的开销。
问题很明显…
怎样做来避免关系跨度?
这非常非常简单,你仅需要进行非跟踪(non-tracking)查询:
1 var source = ctx.Staff; 2 source.MergeOption == MergeOption.NoTracking; 3 4 var staff = (from s in source 5 where s.ID == 12 6 select s).First();
这样结果查询将不会"跨越"所有关联表如SalaryHistory,DisciplineHistory等中的信息。
不幸的是结果也不会"存在于"ctx(上下文)中。所以如果你打算使用这个结果,你不得不在手动附加一次。
在.NET 4.0中对这个问题有一种变通方法,称为FK联合,如果你使用FK联合代替独立联合则根本无需进行关系跨度。
对于一个短暂的上下文这很少引起问题,但是对于一个持续的上下文你必须小心同一个实体(担不是同一个对象)已经不再是上下文中之前查询所得到的结果了。同样如果你想要更新这个实体,你不能简单的使用这个实体来建立新的关系,而是要首先获取所有关联的实体的所有PK。
提示12. 怎样选择一种继承策略
Entity Framework都支持哪些策略?
Entity Framework支持3种主要的继承策略:
每个层次系统一个表(TPH):
在TPH方式中,一个类型层次体系中的所有数据存储于一个表中,有一个鉴别列用于确定一个特定列的类型(如,“C”代表Car或“B”代表Boat)。
存储不是所有类型公共的属性的列应该被设置为可空,即使它们不是可空的属性。这意味着数据库不能完全确保你的非空约束。
一个TPH表可能如下所示这样:
ID |
Description |
Discriminator |
HorsePower |
KeelLength |
1 |
Porsche 911 |
C |
600 |
NULL |
2 |
Yacht - KZ7 |
B |
NULL |
2.2 |
每个类型一个表(TPT):
在TPT模式中,一个基类型的属性存储于一个共享表中。例如这个交通工具表:
ID |
Description |
1 |
Porsche 911 |
2 |
Yacht - KZ7 |
然后每个字类型有一个单独的表,此表包含主键与仅在子类型中声明的属性。所以这些表可以被联合(join),这样将得到一个汽车表:
ID |
HorsePower |
1 |
600 |
或得到一个轮船表:
ID |
KeelLength |
2 |
2.2 |
每个具体类一张表(TPC):
在每个具体类一张表这种模式中,每个类拥有一张表,这些表中都有一个列来存储对应类型的独有的属性,如一个汽车表:
ID |
Description |
HorsePower |
1 |
Porsche 911 |
600 |
及一个船舶表:
ID |
Description |
KeelLength |
2 |
Yacht - KZ7 |
2.2 |
那种策略是最佳的?
狡猾的问题!但从你需求角度孤立考虑,没有一个策略是“最佳”的。
但是…
虽然EF运行时支持TPC,设计器不支持,在EF使用TPC强制你在基类中避免使用关联。由于这些问题的存在,我们一般不鼓励在EF中使用TPC。
这意味着这个问题最佳的答案是TPH或TPT?
下面是一些当你做出决定时一些你可能想要考虑的事情:
关注点 |
优胜者 |
原因 |
性能 |
TPH |
每个层次系统一张表性能更佳。因为这种模式无需表联合,所有东西都在一张表里。 |
可扩展性 |
TPT |
ISV(独立软件开发商)常使用每个类型一张表这种模型。由于这种模型允许在不修改“基”表的情况下进行定制。 |
数据库验证 |
TPT |
TPH需要派生类型中的在数据库中的列为可为空,这样其它派生类型可以被存储在同一个表中。 |
审美观 |
TPT |
这完全是主观判断。但我感觉TPT更面向对象一些:) |
存储空间 |
TPT |
Ward Bell指出这点: |
正如你看到的,一但你知道了什么是你关系点是啥,选择一个策略就是一个相当容易的任务。大部分情况下TPH是被推荐的,因为一般情况下其表现胜过其它问题。
但是对每种情况选择是不同的,关键是理解你最需要及关心什么,然后做出相应的决定。
如果你有什么问题告诉我。