EF Code First(2)自定义约定

注意:以下所讨论的功能或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(1));
 
modelBuilder.Properties()
            .Where(x => x.Name == "Name")
            .Configure(x => x.IsKey().HasColumnOrder(2));

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 APIHaving 方法,这个 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[0] + "_" + m.Value[1]);
    
    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[0] + "_" + m.Value[1]);
 
    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),表中同时包含 employeesmanagers , 存储在每一行的实例类型将由一个标识列来决定,这就是 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(500));
 
modelBuilder.Properties<string>()
            .Where(x => x.Name == "Name")
            .Configure(c => c.HasMaxLength(250));

由于最大长度250约定设置位于500后面,所以字符串的长度将会被限定在250。以这种方式可以实现约定的覆写(override

在一些特殊的情况下, Fluent API Data Annotations 也可被用来 override Conventions

Built-in Conventions

因为定制约定会受到默认 Code First Conventions 的影响,所以在一个约定运行之前或之后添加另一个约定是有意义的,为了实现这个,我们可以在约定集合中使用方法 AddBeforeAddAfter

modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());

内建约定列表请参考命名空间 System.Data.Entity.ModelConfiguration.Conventions Namespace

当然你也可以移除一个约定,示例如下

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}

参考原文: http://msdn.microsoft.com/en-us/data/jj819164