OrderIT使用两种继承策略:每个层次结构一张表(table per hierachy(TPH))每个类型一张表(table per type(TPT))。数据库不支持继承,但是使用这些继承策略,可以模拟这一行为。我们先从TPH策略开始。
TPH将所有的数据保存在一个表中,使用一个或多个字段的值来识别每行记录所属的类型。在OrderIT中,Customer和Supplier类被持久化到Company表。Company有一个Type列标识一行是customer还是supplier。在第二章中已经讨论了这种继承模型的好处;这里,我们集中在实际的映射工作上,先从设计类开始吧。
设计类
继承在设计类是不需要其他工作,知识简单创建基类(本例中是Company),然后创建其他类(Customer,Supplier),让它们继承自基类。注意标识列不能映射,因为它由EF处理。
public abstract class Company { ... } public class Customer:Company { ... } public class Supplier : Company { ... }
跟前边一样,下一步是在EDM中描述新添加的类。
修改概念模式(conceptual schema)
在概念模式的EntityContainer元素内,为整个层次结构只放一个实体集。描述类需要Abstract和BaseType特性。对于Company,Abstract设置为true,BaseType为空。对于Customer和Supplier,Abstract设置为false,BaseType设置为Company的完全限定名(FQN)。它们的映射如下面的清单所示:
<EntityContainer Name="OrderITEntities" annotation:LazyLoadingEnabled="true"> <EntitySet Name="Companies" EntityType="OrderITModel.Company" /> </EntityContainer> <EntityType Name="Company" Abstract="true"> <Key> <PropertyRef Name="CompanyId" /> </Key> <Property Type="Int32" Name="CompanyId" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Type="String" Name="Name" Nullable="false" MaxLength="30" /> <Property Type="Binary" Name="Version" Nullable="false" annotation:StoreGeneratedPattern="Computed" MaxLength="8" FixedLength="true" /> </EntityType> <EntityType Name="Customer" BaseType="OrderITModel.Company" > <Property Name="BillingAddress" Type="OrderITModel.AddressInfo" Nullable="false" /> <Property Name="ShippingAddress" Type="OrderITModel.AddressInfo" Nullable="false" /> <Property Type="String" Name="Username" Nullable="false" MaxLength="20" /> <Property Type="String" Name="Password" Nullable="false" MaxLength="20" /> <Property Type="Boolean" Name="WSEnabled" /> </EntityType> <EntityType Name="Supplier" BaseType="OrderITModel.Company" > <Property Type="Int16" Name="PaymentDays" Nullable="false" /> <Property Type="String" Name="IBAN" Nullable="false" FixedLength="true" MaxLength="27" /> <NavigationProperty Name="Products" Relationship="OrderITModel.ProductSupplier" FromRole="Supplier" ToRole="Product" /> </EntityType>
注意每个EntityType节点必须仅定义它描述类的属性。
修改映射模式(mapping schema)
EntitySetMapping允许在一个实体集中映射多个类。
我们从映射Compan开始。EntitySetMapping的Name特性必须设置为Companies——这是在概念模式中定义的实体集。在EntityTypeMapping内,TypeName必须设置为IsTypeOf(OrderITModel.Company)。最后,MappingFragment元素内的SotreEntitySet特性必须设置为Company——这是存储实体的名字(默认匹配表的名字)。在MappingFragment,放入在概念模式中定义的所有属性。如下面所示:
<EntitySetMapping Name="Companies"> <EntityTypeMapping TypeName="IsTypeOf(OrderITModel.Company)"> <MappingFragment StoreEntitySet="Company"> <ScalarProperty Name="Version" ColumnName="Version" /> <ScalarProperty Name="Name" ColumnName="Name" /> <ScalarProperty Name="CompanyId" ColumnName="CompanyId" /> </MappingFragment> </EntityTypeMapping> </EntitySetMapping>
Customer映射不需要创建新的EntitySetMapping,而是在它内部添加EntityTypeMapping节点。加入这个新的节点,在放入一个MappingFragment节点。在EntityTypeMapping节点中,设置TypeName特性为IsTypeOf(OrderITModel.Customer)。
<EntityTypeMapping TypeName="IsTypeOf(OrderITModel.Customer)"> <MappingFragment StoreEntitySet="Company"> <ScalarProperty Name="CompanyId" ColumnName="CompanyId" /> <ScalarProperty Name="Password" ColumnName="WSPassword" /> <ScalarProperty Name="Username" ColumnName="WSUserName" /> <ScalarProperty Name="WSEnabled" ColumnName="WSEnabled" /> <ComplexProperty Name="BillingAddress"> <ScalarProperty Name="Country" ColumnName="BillingCountry" /> <ScalarProperty Name="ZipCode" ColumnName="BillingZipCode" /> <ScalarProperty Name="City" ColumnName="BillingCity" /> <ScalarProperty Name="Address" ColumnName="BillingAddress" /> </ComplexProperty> <ComplexProperty Name="ShippingAddress"> <ScalarProperty Name="Country" ColumnName="ShippingCountry" /> <ScalarProperty Name="ZipCode" ColumnName="ShippingZipCode" /> <ScalarProperty Name="City" ColumnName="ShippingCity" /> <ScalarProperty Name="Address" ColumnName="ShippingAddress" /> </ComplexProperty> <Condition ColumnName="Type" Value="C" /> </MappingFragment> </EntityTypeMapping>
映射Supplier就是Customer的复制了,修改TypeName特性和映射的属性,如下:
<EntityTypeMapping TypeName="IsTypeOf(OrderITModel.Supplier)"> <MappingFragment StoreEntitySet="Company"> <ScalarProperty Name="CompanyId" ColumnName="CompanyId" /> <ScalarProperty Name="PaymentDays" ColumnName="PaymentDays" /> <ScalarProperty Name="IBAN" ColumnName="IBAN" /> <Condition ColumnName="Type" Value="S" /> </MappingFragment> </EntityTypeMapping>
最后的调整就是鉴别器(discriminator)。你已经知道TPH映射策略需要一个标识列(discriminator column)指定一行应该映射到什么类型。在OrderIT中,标识列指定行是customer还是supplier。这在映射模式中必须指定。
指定鉴别器,在MappingFragment中使用Condition元素为标识列指定一个值。当列有指定的值,行就属于正在映射的类型。C指定行是customer,而S指定是supplier。在前面的代码片段中可以看见这个标签。
现在已经成功的使用TPH策略完成了映射继承结构了。这并不难,只是学到了一个新的节点,Condition,其他的都和前边的一样。
TPT方法将层次结构中的每一个实体映射到一个专门的表。表包含和相关类型定义一样多的列。在OrderIT中,有Product,Shoe,Shirt表。
在TPH模型中,标识列负责分别customer和supplier。在TPT模型中,没必要这样一个列,因为主键指定产片的类型。例如,如果一个产品的ID为1,它在Product和Shirt表中,它就是shirt;如果在Product和Shoe表中,那么它就是一对鞋。通过使用特别的SQL链接,EF能能够在查询和更新数据时确定每一个产品的类型。
映射TPT并不复杂,因为你已经掌握了必需的知识。代码和TPH中使用的相同;你可以如下面的清单定义类和它们的继承关系。
public abstract class Product { ... } public class Shoe : Product { ... } public class Shirt : Product { ... }
TPT和TPH策略之间改变的是映射模式和存储模式,而不是类。
在概念模型中,定义实体和在TPH策略完全一样并且为每一个层次结构的根创建一个EntitySet。存储模式不同,因为每一个实体不只一个表,但是已经知道了如何定义表,这里不再演示。
在映射文件中,使用EntitySetMapping元素,为每一个实体嵌入一个EntityTypeMapping节点。在EntityTypeMapping内,包含进一个MappingFragment元素关联表和实体。在MappingFragment内,只映射在实体中声明的实体加上主键。映射结果如下:
<EntitySetMapping Name="Products"> <EntityTypeMapping TypeName="IsTypeOf(OrderITModel.Product)"> <MappingFragment StoreEntitySet="Product"> <ScalarProperty Name="ProductId" ColumnName="ProductId" /> <ScalarProperty Name="Version" ColumnName="Version" /> <ScalarProperty Name="ReorderLevel" ColumnName="ReorderLevel" /> <ScalarProperty Name="AvailableItems" ColumnName="AvailableItems" /> <ScalarProperty Name="Price" ColumnName="Price" /> <ScalarProperty Name="Brand" ColumnName="Brand" /> <ScalarProperty Name="Description" ColumnName="Description" /> <ScalarProperty Name="Name" ColumnName="Name" /> </MappingFragment> </EntityTypeMapping> <EntityTypeMapping TypeName="IsTypeOf(OrderITModel.Shirt)"> <MappingFragment StoreEntitySet="Shirt"> <ScalarProperty Name="Material" ColumnName="Material" /> <ScalarProperty Name="Gender" ColumnName="Gender" /> <ScalarProperty Name="Size" ColumnName="Size" /> <ScalarProperty Name="Color" ColumnName="Color" /> <ScalarProperty Name="SleeveType" ColumnName="SleeveType" /> <ScalarProperty Name="ProductId" ColumnName="ProductId" /> </MappingFragment> </EntityTypeMapping> <EntityTypeMapping TypeName="IsTypeOf(OrderITModel.Shoe)"> <MappingFragment StoreEntitySet="Shoe"> <ScalarProperty Name="Sport" ColumnName="Sport" /> <ScalarProperty Name="Gender" ColumnName="Gender" /> <ScalarProperty Name="Size" ColumnName="Size" /> <ScalarProperty Name="Color" ColumnName="Color" /> <ScalarProperty Name="ProductId" ColumnName="ProductId" /> </MappingFragment> </EntityTypeMapping></EntitySetMapping>
无数次,你使用已有的知识创建新的特性。尽管错综复杂,但EDM也不难掌握。
EDM有三个XML文件组成,和其他XML文件一样,它可以通过添加自定义命名空间和链接节点被扩展。即使所有的映射信息已存在,但是当你需要额外的数据而不包含在默认模式中时添加自定义信息就很有用了。
一个简单的例子就是验证。Supplier类有一个IBAN属性(IBAN作为国际格式用于标识银行账户)。你可以在EDM中指定它的格式,然后使用模板自定义类代码生成在IBAN属性setter中添加验证码。甚至更好,你可以生成DataAnnotation特性,以便类符合ASP.NET MVC验证规范。不论你选择哪种,EDM自定义是一切开始的地方。
另一个例子涉及到集合属性。假设一个订单不能超过10个详细。你可以在EDM中添加这个值到属性,然后在Add方法中创建一个CLR自定义集合和详细的限制数比较,如果超过限制就引发一个异常或者执行其他操作。
还有其他关于在EDM中自定义是个好的选择的例子。它是一个强大的工具,现在你知道为什么在EDM自定义了,下面看一下如何定义。
添加自定义批注非常简单。你只需要在要子定义的标签内添加一个命名空间。
假设你需要在EDM中添加IBAN验证信息。验证一个字符串最简单的方式是用正则表达式。下面的清单给IBAN属性添加了一个正则表达式。
当然,你可以遵循相同的原则添加特性。没有技术限制,完全取决于你。
记住,在EDM中并不是所有的标签都接受内部自定义标签。下面的标签不允许自定义设置:
Using
Schema
Key
PropertyRef
这种限制的原因是这些标签在EF元数据对象模型中无法与之对应,所以它们无法访问。同样,MSL标签不能自定义,而SSDL和CSDL标签可以。
这一章就结束了,下一章是理解实体的生命周期。