------------------------------------------------------------------------------------------------------------
注意:以下所讨论的功能或 API 等只针对 Entity Framework 6 ,如果你使用早期版本,可能部分或全部功能不起作用!
------------------------------------------------------------------------------------------------------------
Entity Framework Code First 默认的 Conventions 约定解决了一些诸如哪一个属性是实体的主键、实体所 Map 的表名、以及列的精度等问题,但是某些时候,这些默认的约定对于我们的模型是不够理想的,此时我们就希望能够自定义一些约定。当然通过使用 Data Annotations 或者 Fluent API 也能实现这样的目的,无非就是对许多实体作出配置,但是这样的工作是极其繁琐和繁重的。而定制约定能很好地解决我们的问题,接下来就将展示如何来实现这些定制约定。
Our Model
为了定制约定,本文引入了DbModelBuilder API ,这个 API 对于编程实现大部分的定制约定是足够的,但它还有更多的能力,例如 model-based 约定,更过信息,请参考 http://msdn.microsoft.com/en-us/data/dn469439
在开始之前,我们先定义一个简单的模型
Custom Conventions
下面这个约定使得任何以 key 命名的属性都将成为实体的主键
我们也可以使得约定变得更加精确:过滤类型属性(如只有 integer 型并且名称为 key 的才能成为主键)
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
}
关于 IsKey 方法,有趣的是它是可添加的,这意味着如果你在多个属性上施加这个方法,那么这些属性都将变成组合键的一部分,对于组合键,指定属性的顺序是必须的。指定的方法如下
modelBuilder.Properties<int>()
.Where(x => x.Name == "Key")
.Configure(x => x.IsKey().HasColumnOrder()); modelBuilder.Properties()
.Where(x => x.Name == "Name")
.Configure(x => x.IsKey().HasColumnOrder());
Convention Classes
另一种定义约定的方式是通过约定类来封装约定,为了使用约定类,你定义一个类型,继承约定基类(位于 System.Data.Entity.ModelConfiguration.Conventions 命名空间下)
public class DateTime2Convention : Convention
{
public DateTime2Convention() {
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
为了通知 Entity Framework 使用这个约定,需把它添加到约定集合中,代码如下
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey()); modelBuilder.Conventions.Add(new DateTime2Convention());
}
如你所见,我们在约定集合中添加了一个上面定义的约定的实例。
从 Convention 继承为我们提供了一种非常方便的方式,使得组织、管理非常便捷并且易于跨项目使用。例如你可以为此建立一个类库,专门提供这些约定的合集。
Custom Attribute
定制属性:另一种使用约定的方式就是通过在模型上配置属性(Attribute)。示例如下:我们建立一个属性(Attribute)用于标识字符属性(Property)为非Unicode
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NonUnicode : Attribute
{ }
现在让我们在模型上新建约定以使用此属性
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
.Configure(c => c.IsUnicode(false));
通过这个约定,我们可以把 NonUnicode 属性(Attribute)施加于任何字符属性(Property),这也意味着此列在数据库中将以 varchar 的而非 nvarchar 的形式存储。
需要注意的是,如果你把此约定施加于任何非字符属性都将引发异常,这是因为 IsUnicode 只能施加于 string (其它类型都不可以),为此我们需使得约定变得更加精确,即过滤掉任何非 string 的东西
上面的约定解决了定义定制属性的问题,我们需要注意的是还有另一个 API 非常易于使用,尤其是你想使用 Attribute Class 的 Properties
让我们对上面的类做一些更新
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class IsUnicode : Attribute
{
public bool Unicode { get; set; }
public IsUnicode(bool isUnicode)
{
Unicode = isUnicode;
}
}
一旦我们有了这个,我们就可以在 Attribute 上设置一个 bool 通知约定 Property 是否是 Unicode. 配置如下
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
.Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
上面的足够简单,但是还有一种更简洁的方式 - 就是使用 Conventions API 的 Having 方法,这个 Having 方法有一个 Func<PropertyInfo, T> 类型参数,这个参数能够像 Where 一样接收 PropertyInfo. 但是前者返回的是一个 object. 如果返回对象为 null, 那么 property 将不会被配置 -- 这意味着我们可以像 Where 一样过滤某些 properties -- 但是它们又是不同的,因为前者还可以捕获并返回 object 然后传递给 Configure 方法
modelBuilder.Properties()
.Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, att) => config.IsUnicode(att.Unicode));
当然定制属性并不是我们使用 Having 方法的唯一原因,在任何时候,当我们配置类型或属性,需要过滤某些东西的时候是非常有用的。
Configuring Types
到目前为止,所有的约定都是针对属性(properties)而言,其实还有其它的 conventions API 用于针对模型的类型配置。前者是在属性级别(Property Level),后者是在类型级别(Type Level)
Type Level Conventions 一个显而易见的用处是更改表的命名约定,既可以改变 Entity Framework 默认提供的从而匹配于现有的 schema, 也可以基于完全不同的命名约定创建一个全新的数据库,为此我们首先需要一个方法,接收 the TypeInfo for a type, 返回 the table name for that type
private string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[] + "_" + m.Value[]); return result.ToLower();
}
上面的方法意味着,如果施加于 ProductCategory 类,则该类将会被映射于表名 product_category 而不是 ProductCategories
我们可以在一个约定中这样使用它
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
这个约定将配置模型中的每一个类型与方法 GetTableName 返回的表名相匹配,这与通过 Fluent API 为模型中每一个实体使用方法 ToTable 是等效的。
需要注意的是方法 ToTable 需要一个字符串参数来作为确切的表名,如果没有复数化( pluralization )要求,我们通常会这么做。这也是为什么上面约定表名是 product_category 而不是 ProductCategories, 这可以在约定中通过调用 pluralization service 来解决
在接下来的示例中,我们将使用 Entity Framewrok 6 中新增加的功能 Dependency Resolution 来获得 pluralization service, 从而实现表名复数化
private string GetTableName(Type type)
{
var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>(); var result = pluralizationService.Pluralize(type.Name); result = Regex.Replace(result, ".[A-Z]", m => m.Value[] + "_" + m.Value[]); return result.ToLower();
}
注意:GetService 的泛型版本是命名空间 System.Data.Entity.Infrastructure.DependencyResolution 下的一个扩展方法
ToTable and Inheritance
ToTable 的另一个重要方面是如果你明确一个类型映射到给定的表,那么你可以改变 EF 使用的映射策略。如果你在继承层次中为每一个类型都调用此方法,像上面所做的那样 -- 把类型名当参数传递作为表名,那么你将改变默认的映射策略 Table-Per-Hierarchy (TPH) -- 使用 Table-Per-Type (TPT). 为了更好的说明举例如下
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
} public class Manager : Employee
{
public string SectionManaged { get; set; }
}
默认情况下,Employee 和 Manager 都将映射成数据库中的同一张表(Employees),表中同时包含 employees 和 managers , 存储在每一行的实例类型将由一个标识列来决定,这就是 TPH 策略带来的结果 -- 对层级只有一张表。但是如果你对每一个类都使用 ToTable, 那么每一个类型都将各自映射成自己的表,这正如 TPT 策略所示的那样
modelBuilder.Types()
.Configure(c=>c.ToTable(c.ClrType.Name));
上面代码映射成的表结构如下图
你可以通过如下几种方式来避免此问题并且维护默认的 TPH 映射
- 使用相同的表名为层级中的每一个类型调用 ToTable ;
- 只为层级中的基类调用ToTable (上例中为 Employee)
Execution Order
最后一个约定生效,这和 Fluent API 是一样的。这意味着如果在同一个属性上有两个约定,那最后一个起作用。
modelBuilder.Properties<string>()
.Configure(c => c.HasMaxLength()); modelBuilder.Properties<string>()
.Where(x => x.Name == "Name")
.Configure(c => c.HasMaxLength());
由于最大长度250约定设置位于500后面,所以字符串的长度将会被限定在250。以这种方式可以实现约定的覆写(override)
在一些特殊的情况下,Fluent API 和 Data Annotations 也可被用来 override Conventions
Built-in Conventions
因为定制约定会受到默认 Code First Conventions 的影响,所以在一个约定运行之前或之后添加另一个约定是有意义的,为了实现这个,我们可以在约定集合中使用方法 AddBefore 和 AddAfter
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
内建约定列表请参考命名空间 System.Data.Entity.ModelConfiguration.Conventions Namespace
当然你也可以移除一个约定,示例如下
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}