【原文地址】Code Only Enhancements
【原文发表日期】 03 August 09 11:11
自从第一个预览版发布之后,我们一直在奋力增强Code Only功能。
在下一个版本中,你将能够指定- 导航属性的倒转(Inverse)关系
- 属性的细节(Facets),象like Nullability(可null性), MaxLength(最大长度), Precision(精度)等等
- 属性与字段间的映射
- 类型与数据表间的映射
- 继承策略
- 配置的封装
本贴的余下部分将依次对这些特性进行详述。
注册导航属性的倒转关系:
你现在可以注册倒转关系,即一个导航属性到另一个导航属性的倒转(inverse)关系,象这样:
builder.RegisterInverse(
(Customer c) => c.Orders,
(Order o) => o.Customer)
);
这代码表示Customer.Orders是Order.Customer关系的另一头。把order1 加到customer1.Orders集合中去,与把order1.Customer设置成customer1具有同等的效果。
指定属性细节(Facets):
你还可以指定属性的细节,即象Nullability(可null性), MaxLength(最大长度), Precision(精度)等等这样的东西,象这样:
var customerConfig = new EntityConfiguration<Customer>();
// 我们可以推断出ID是主键
// 但无法推断出它在插入时是在数据库生成的
customerConfig.ForProperty(c => c.ID)
.Identity();
customerConfig.ForProperty(c => c.Name)
.MaxLength(100)
.NonUnicode();
customerConfig.ForProperty(c => c.Website)
.MaxLength(200)
.Nullable()
builder.Configure(customerConfig);
这把Customer类型配置成:
- ID 属性是Identity字段,即在我们插入数据到数据库时,其值是由数据库计算出来的。
- Name属性,其MaxLength为100个字符,是NonUnicode(非Unicode),即在SQL Server中是VARCHAR而不是NVARCHAR。
- Website属性,其MaxLength为200个字符串,是Nullable。
这些细节是针对概念模型Conceptual Model,即CSDL)的,从那里,也传到数据库(即SSDL)。
封装细节配置
你可以创建一个EntityConfiguration<T>的继承类来封装所有这些配置。
例如:
public class CustomerConfig: EntityConfiguration<Customer>
{
public CustomerConfig(){
ForProperty(c => c.ID)
.Identity();
ForProperty(c => c.Name)
.MaxLength(100)
.NonUnicode();
ForProperty(c => c.Website)
.MaxLenght(200)
.Nullable();
}
}
我们建议你创建象这样的类,而不是配置EntityConfiguration<>,因为封装的好处。
指定数据表名
在你使用Configure<T>(..) 时,实体框架为你推断出默认的映射,继承策略(TPH)和表名。
但如果你要指定表名,你可以这么做:
var customerConfig = new EntityConfiguration<Customer>();
// 象上面那样配置细节
...
// 用一个特定的表名注册配置
builder.Tables[“dbo.Custs”] = customerConfig;
指定映射:
如果你需要对映射更多的控制(例如,要映射到一个现有的数据库或者使用企业的命名规则),那么你可以象这样指定映射:
EntityMap<Customer> customerMap =
Map.OfType<Customer>(
c => new {
cid = c.ID,
c.Name,
csite = c.Website
}
);
映射解释
这个映射表明,ID是映射到‘cid’字段,Name属性是保存到‘Name’字段,而Website是映射到‘csite’ 字段的。
没有被引用的属性是不会被持久化的,就象使用实体框架默认的代码生成时生成的部分类上的属性一样。
LINQ内涵句法(Comprehension Syntax)
你甚至也可以使用LINQ内涵句法来指定同样的事情:
EntityMap<Customer> customerMap =
from c in Map.OfType<Customer>()
select new {
cid = c.ID,
c.Name,
csite = c.Website
};
用映射指定细节(Facets)
在配置完映射后,你还可以象这样在映射上指定facets:
customerMap.ForProperty(c => c.ID)
.Identity();
customerMap.ForProperty(c => c.Name)
.MaxLength(100)
.NonUnicode();
customerMap.ForProperty(c => c.Website)
.MaxLenght(200)
.Nullable();
指定数据表
最后一步是指派数据表映射。
builder.Tables[“dbo.Custs”] = customerMap;
至此,我们为Customer类指定了自定义表,映射和自定义facets。
指定继承:
CodeOnly默认所用的继承策略是Table Per Hierarchy (每个类分层结构共用一个数据表)(或 TPH)。
但如果你需要一个不同的策略,你需要动手配置相应映射
设想一下,如果你要映射三个类:Vehicle , Car 和 Boat,其中Car 和 Boat是从Vehicle继承而来,而Vehicle本身是个抽象类。
Table Per Hierarchy (TPH,每个类分层结构共用一个数据表)
如果你想要使用TPH做映射,你可以这么做:
var vehicleMap =
Map.OfTypeOnly<Vehicle>(
v => new {
vid = v.ID,
v.Name,
vdesc = v.Description
v.MaxPassengers,
}
).Union(Map.OfTypeOnly<Car>(
c => new {
vid = c.ID,
c.Name,
vdesc = c.Description
c.MaxPassengers,
trans = c.Transmission,
tspd = c.Topspeed,
ccty = c.EngineCapacity,
ncyld = c.NoCylinder,
discriminator = “CAR”
})
).Union(Map.OfTypeOnly<Boat>(
b => new {
vid = b.ID,
b.Name,
vdesc = b.Description
b.MaxPassengers,
lng = b.Length,
b.HasSail,
b.HasEngine
discriminator = “BOAT”
})
);
builder.Tables[“dbo.vehicles”] = vehicleMap;
在TPH映射中:
- OfTypeOnly() 用来创建映射片段。
- 然后,映射片段可以联合(union)起来,这样,它们就可以指派到一个表上(TPH中整个类分层结构共用一个数据表)。
- 每个非抽象类型需要一个鉴别器字段(discriminator column),该字段可以是任何名称,但类分层结构中的每个非抽象类型必须有一个不同的常数(即“CAR”)。
- 在映射继承类时,你必须重新映射基类中已经映射了的所有属性。
Table Per Type (TPT,每个类型一个表)
如果你想要使用Table Per Type (每个类型一个表) 或TPT来映射同个类分层结构,你该这么做:
builder.Table[“dbo.Vehicles”]=
Map.OfType<Vehicle>(
v => new {
vid = v.ID,
v.Name,
vdesc = v.Description
v.MaxPassengers,
}
);
builder.Tables[“dbo.Cars”] =
Map.OfType<Car>(
c => new {
cid = c.ID,
trans = c.Transmission,
tspd = c.Topspeed,
ccty = c.EngineCapacity,
ncyld = c.NoCylinders,
}
);
builder.Tables[“dbo.Boats”] =
Map.OfType<Boat>(
b => new {
bid = b.ID,
lng = b.Length,
b.HasSail,
b.HasEngine
}
);
在TPT映射中:
- OfType<>()用于每个映射“片段”。
- 每个映射“片段”指派到不同的表上。
- 每个映射“片段”只映射在当前类上声明的属性,除非。。。
- 键属性必须在每个片段中映射(这允许参与该类型的表间的JOIN)。
- 没有鉴别器字段。
Table Per Class (TPC,每个类一个表,【译注】常规的说法是,每个非抽象类或每个具体类一个表)
你还可以象这样使用TPC来映射这个类分层结构:
builder.Tables[“dbo.Cars”] =
Map.OfTypeOnly<Car>(
c => new {
cid = c.ID,
c.Name,
vdesc = c.Description
c.MaxPassengers,
trans = c.Transmission,
tspd = c.Topspeed,
ccty = c.EngineCapacity,
ncyld = c.NoCylinders,
}
);
builder.Tables[“dbo.Boats”] =
Map.OfTypeOnly<Boat>(
b => new {
bid = b.ID,
b.Name,
description = b.Description
b.MaxPassengers,
lng = b.Length,
b.HasSail,
b.HasEngine
}
);
在TPC映射中:
- 跟TPH一样,我们使用OfTypeOnly(..)。
- 但跟TPH不一样,每个非抽象的类型拥有自己的表(所以,Vehicle没有自己的表,因为它是个抽象类)。
- 每个映射片段重新映射每个持久(non-transient)属性。
- 类分层结构中的抽象类没有映射。
- 没有鉴别器字段。
默认的外键的位置:
如果我们看到一个引用(譬如Order.Customer),我们假定其多重性(multiplicity)为0..1。意即,其外键或FK是可null的。
如果我们看到一个集合(譬如Customer.Orders),我们假定其多重性为多(many)。
然后,在注册倒转(inverse)时,我们知道一个关系的两头的多重性,譬如,在上面的例子中,我们知道,每个Order可以有0..1个 Customer,每个Customer可以有多个Order。
所以按约定,共有三种主要的关系类型,我们需要推断出其FK的位置:
0..1 to many –> 按约定,我们把FK放在many一端,所以上面的Customer.Orders例子中, FK放在Orders 表上。
many to many –> 没什么选择,只能引进一个连接表(join table)。
0..1 to 0..1 –> 你可以配置FK应该在什么地方,但不配置的话,我们会引进一个连接表(join table)。
但有时候,引用可以不是0..1,而是1,例如,FK(不管它在什么地方),也许不可以null的。
你可以这样来指定FK不是nullable的:
var orderConfig = builder.Configure<Order>();
orderConfig.RegisterInverse(o => o.Customer, c => c.Orders);
orderConfig.ForProperty(o => o.Customer).NonNullable();
这告诉我们,每个Order正好有一个1个Customer,而每个Customer有多个Orders。
能够区分一个引用是非Nullable,会引进几个新的多重性组合,对此,我们也需要约定:
1 to many –> 按约定,我们把FK放在many的一端, 并且在数据库中将其设置为非nullable。
0..1 to 1 –> 按约定,我们把FK放在1的一端, 并且将其设置为非nullable。
1 to 1 –> 跟0..1 to 0..1一样,我们无法决定该把FK放在何处,所以按约定,我们要引进一个连接表(join table)。
指定外键(FK)映射:
至此,我们为实体的属性创建了映射。
那么导航属性和外键怎么办?
所有的关系类型(除了many to many)可以不用数据库中的连接表(join table)来建模,所以我们允许你象这样,作为EntityMap的一部分来映射外键:
EntityMap<Customer> customerMap =
from c in Map.OfType<Customer>()
select new {
cid = c.ID,
c.Name,
csite = c.Website,
salesPersonFK = c.SalesPerson.ID
};
customerMap.RegisterInverse(c => c.SalesPerson, s => c.Clients);
builder.Tables[“dbo.Custs”] = customerMap;
这指定,Customer.SalesPerson导航属性,以及它的倒转(inverse)SalesPerson.Customers是保存在 dbo.Custs表的salesPersonFK字段中的。因为映射片段把 salesPersonFK 字段映射到了c.SalesPerson.ID上,而 SalesPerson.ID是相关的SalesPerson实体的主键(或者是主键的一部分),当然,外键是指向主键的。
指定连接表(Join Table)映射:
在many to many关系的情形下,你必须有一个连接表,所以按约定,没有映射信息,我们就会产生一个连接表(join table)。
但假如你需要更多的控制,你可以这么做:
var blogPostsMap = new AssociationMap<Blog, Post>(
b => b.Posts
).Map(
(b, p) => new {BlogId = b.ID, PostId = p.ID}
);
builder.Tables[“dbo.BlogPosts”] = blogPostsMap;
这是说, Blog 与 Post间的many to many关系是保存在dbo.BlogPosts表中的,它有2个字段:
- ‘BlogId’ 是个外键,指向Blog的ID属性映射到的字段。
- ‘PostId’ 是个外键,指向Post的ID属性映射到的字段。
分割实体(Entity Splitting):
Code Only甚至还支持象分割实体这样高级的映射策略:
builder.Tables[“dbo.Customer”] = Map.OfType<Customer>(
c => new {
cid = c.ID,
c.Name,
active = c.IsActive
}
);
builder.Tables[“dbo.CustomerDetails”] = Map.OfType<Customer>(
c => new {
cid = c.ID,
c.Size,
c.Industry
}
);
这是说,Customer实体是分开保存在dbo.Customer和dbo.CustomerDetails两个表中的。
封装所有的配置:
你还可以编写一个类,从EntityMap<T>继承而来,含有所有的映射,facets等等。例如,下面是一个类,包含了Product的配置:
public class ProductMap: EntityMap<Product>
{
public ProductMap{
this.Map( p => new {
pid = p.ID,
pcode = p.Name,
cid = p.Category.ID
});
this.ForProperty(p => p.ID).Identity();
this.ForProperty(p => p.Name).MaxLength(100)
.NonUnicode();
this.ForProperty(p => p.Category).NonNullable();
this.RegisterInverse(p => p.Category,
c => c.Products);
}
}
这是个高度推荐的做法,因为配置Product类型变得容易之极:
builder.Tables[“dbo.Products”] = new ProductMap();
结束语:
正如你看到的,我们正在计划许多的增强,以允许最核心的场景。
你觉得怎么样?你喜欢这些API么?有什么东西你想要改变的?
一如既往,我们期待听到你的反馈。
Alex James
微软Entity Framework开发团队的Program Manager
本贴是Entity Framework开发团队的透明设计实践的一部分。想了解其工作原理,以及你的反馈是如何被使用的,请参阅 这个贴子。
推荐文章