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