Entity Framework Code First (二)Custom Conventions

来源:互联网 发布:移动数据安全 编辑:程序博客网 时间:2024/05/16 23:46

原文地址:http://www.cnblogs.com/panchunting/p/entity-framework-code-first-custom-conventions.html

------------------------------------------------------------------------------------------------------------

  注意:以下所讨论的功能或 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 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[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),表中同时包含 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(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 的影响,所以在一个约定运行之前或之后添加另一个约定是有意义的,为了实现这个,我们可以在约定集合中使用方法 AddBefore 和 AddAfter 

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


0 0
原创粉丝点击