Entity Framework Code First (二)Custom Conventions


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

  注意:以下所討論的功能或 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM