備註
僅限 EF6 及以上 版本 - 本頁討論的功能、API 等皆於 Entity Framework 6 中引入。 如果你使用的是較早期版本,部分或全部資訊可能不適用。
使用 Code First 時,模型是根據一組慣例從類別計算出來的。 預設的 程式碼優先慣例會 決定像是哪個屬性成為實體的主鍵、實體對應到的資料表名稱,以及小數點欄位預設的精確度和縮放。
有時這些預設慣例並不適合你的模型,你必須透過使用 Data Annotations 或 Fluent API 來設定多個獨立實體來繞過它們。 自訂代碼優先慣例允許你自行定義,為你的模型提供配置預設值。 在這篇攻略中,我們將探討不同類型的自訂慣例,以及如何建立它們。
基於模型的慣例
本頁介紹了用於自訂慣例的 DbModelBuilder API。 這個 API 應該足以用於撰寫大多數自訂慣例。 然而,也可撰寫基於模型的慣例——這些慣例會在模型建立後操作最終模型——以處理進階情境。 欲了解更多資訊,請參閱 Model-Based 會議。
我們的模型
讓我們先定義一個簡單的模型,可以用來搭配我們的慣例。 將以下課程加入你的專案。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
}
public class Product
{
public int Key { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; }
public DateTime? ReleaseDate { get; set; }
public ProductCategory Category { get; set; }
}
public class ProductCategory
{
public int Key { get; set; }
public string Name { get; set; }
public List<Product> Products { get; set; }
}
介紹自訂慣例
我們來寫一個慣例,將任何名為 Key 的屬性設定為其實體類型的主鍵。
模型建構器啟用了慣例,可透過在上下文中覆寫 OnModelCreating 來存取。 更新 ProductContext 類別如下:
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
}
}
現在,我們模型中任何名為 Key 的屬性都會被設定為其所屬實體的主鍵。
我們也可以透過篩選將要設定的屬性類型來使慣例更具體:
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
這會將所有稱為 Key 的屬性設定為其實體的主鍵,但前提是它們是整數。
IsKey 方法的一個有趣特點是它是可加法的。 這表示如果你呼叫多個屬性的 IsKey,它們都會成為複合金鑰的一部分。 唯一的注意是,當你為一個鍵指定多個屬性時,也必須為這些屬性指定順序。 你可以像下面那樣呼叫 HasColumnOrder 方法來完成:
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));
此程式碼將配置模型中的類型,使其擁有由整數鍵欄位與字串名稱欄位組成的複合鍵。 如果我們在設計器中查看模型,會是這樣的:
另一個屬性慣例的例子是將模型中的所有 DateTime 屬性設定為 SQL Server 中的 datetime2 類型,而非 datetime。 你可以透過以下方式達成這個目標:
modelBuilder.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
規範類別
另一種定義慣例的方法是使用 Convention 類別來封裝你的慣例。 使用 Convention 類別時,會在 System.Data.Entity.ModelConfiguration.Conventions 命名空間中建立一個繼承 Convention 類別的型別。
我們可以透過以下操作建立一個包含先前 datetime2 約定的 Convention 類別:
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
要告訴 EF 使用這個慣例,你可以把它加入 OnModelCreating 的 Conventions 集合,如果你有跟著攻略操作,它會是這樣顯示的:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name.EndsWith("Key"))
.Configure(p => p.IsKey());
modelBuilder.Conventions.Add(new DateTime2Convention());
}
如您所見,我們將一個我們的慣例實例新增到 conventions 集合中。 從慣例繼承提供了一種方便的方式,讓人們能在團隊或專案間分組並分享慣例。 例如,你可以有一個類別函式庫,裡面有一套所有組織專案都共用的慣例。
自訂屬性
慣例的另一個重要用途是讓模型配置時能使用新的屬性。 為了說明這一點,讓我們建立一個屬性,用來標記字串屬性為非 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 屬性,這表示資料庫中的欄位會以 varchar 而非 nvarchar 儲存。
關於這個慣例,有一點要注意,如果你把 NonUnicode 屬性放在字串以外的屬性上,它就會拋出例外。 這是因為你無法在字串以外的任何型態上設定 IsUnicode。 如果發生這種情況,你可以讓慣例更具體,過濾掉所有非字串的內容。
雖然上述慣例適合定義自訂屬性,但還有另一個 API 會更簡單,尤其是當你想使用屬性類別的屬性時。
在這個例子中,我們將更新屬性並將其更改為 IsUnicode 屬性,所以看起來是這樣:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Unicode { get; set; }
public IsUnicode(bool isUnicode)
{
Unicode = isUnicode;
}
}
一旦有了這個,我們就可以在屬性上設定一個布爾,告訴該約定是否應該是 Unicode。 我們可以依照已有的慣例,透過存取配置類別的屬性 ClrProperty 來實現。
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> ,接受 PropertyInfo 與 Where 方法相同,但預期會回傳物件。 如果回傳的物件是空,屬性就不會被設定,這表示你可以像 Where(Where)一樣過濾掉屬性,但不同的是它也會擷取回傳物件並傳給 Configure 方法。 這個流程如下:
modelBuilder.Properties()
.Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, att) => config.IsUnicode(att.Unicode));
自訂屬性並不是使用 Having 方法的唯一原因,該方法很有用,在設定類型或屬性時,當你需要針對篩選的某些內容進行推理時。
配置類型
到目前為止,我們所有的慣例都是針對屬性,但 API 裡還有另一個區域用來設定模型中的型別。 這個體驗和我們目前看到的慣例類似,但配置裡的選項會是在實體層級而非屬性層級。
類型層級慣例非常有用的一件事是改變資料表命名慣例,無論是映射到與 EF 預設不同的現有結構,或是建立一個擁有不同命名慣例的新資料庫。 為此,我們首先需要一個方法,能接受模型中某型態的 TypeInfo,並回傳該型別的表名:
private string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
此方法會取一個型別,回傳一個字串,該字串使用小寫並加底線,而非 CamelCase。 在我們的模型中,這表示 ProductCategory 類別會映射到一個稱為 product_category 的表格,而非 ProductCategories。
一旦有了這個方法,我們就可以用類似這樣的慣例來呼叫它:
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
此慣例會將模型中的每個型別配置為對應到 GetTableName 方法回傳的資料表名稱。 此慣例相當於使用 Fluent API 呼叫模型中每個實體的 ToTable 方法。
要注意的是,當你呼叫 ToTable 時,EF 會把你提供的字串當作精確的表名,沒有像一般決定資料表名稱時那樣的複數。 這也是為什麼我們大會的桌名是product_category而不是product_categories。 我們可以在協議中自己調用複數化服務來解決問題。
在以下程式碼中,我們將使用 EF6 新增的 相依性解決 功能,取得 EF 原本會使用的複數化服務,並將我們的資料表名稱複數化。
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 命名空間中的擴充方法,你需要在上下文中加入 using 語句才能使用。
ToTable 與繼承
ToTable 另一個重要面向是,如果你明確將型別映射到特定資料表,就能改變 EF 所使用的映射策略。 如果你對繼承階層中的每個型別呼叫 ToTable,並將型別名稱當作表表名稱傳遞,就像我們之前做的,那麼你會將預設的 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; }
}
預設情況下,員工和經理都映射到資料庫中的同一個資料表(Employees)。 該表格會包含員工和經理,並有一個判別器欄位,告訴你每列儲存的實例類型。 這是 TPH 映射,因為階層結構只有一個表格。 然而,如果你對兩個類別都呼叫 ToTable,那麼每個類型將被映射到它自己的表格,這種情況也被稱為 TPT,因為每個類型都有自己的表格。
modelBuilder.Types()
.Configure(c=>c.ToTable(c.ClrType.Name));
上述程式碼會映射到一個看起來像以下的表格結構:
你可以透過幾種方式避免這種情況,並維持預設的TPH映射:
- 呼叫 ToTable,並對階層中每個型別使用相同資料表名稱。
- 只在階層的基底類別呼叫 ToTable,在我們的例子中是指員工。
執行順序
慣例以最後勝利的方式運作,與 Fluent API 相同。 這表示如果你寫了兩個設定相同屬性選項的慣例,最後執行的那個會贏。 舉例來說,在下面的程式碼中,所有字串的最大長度設為 500,但我們將模型中所有稱為 Name 的屬性設定為最大長度為 250。
modelBuilder.Properties<string>()
.Configure(c => c.HasMaxLength(500));
modelBuilder.Properties<string>()
.Where(x => x.Name == "Name")
.Configure(c => c.HasMaxLength(250));
因為將最大長度設為 250 的慣例是在將所有字串設為 500 的慣例之後,因此我們模型中所有稱為 Name 的屬性的最大長度都會是 250,而其他字串(如描述)則是 500。 這樣使用慣例,代表你可以為模型中的型別或屬性提供一個通用慣例,然後對不同的子集加以覆寫。
Fluent API 及資料註解也可以用來在特定情況下覆蓋預設規則。 在我們上面的例子中,如果我們用 Fluent API 來設定屬性的最大長度,那麼我們可以在慣例之前或之後放置,因為更具體的 Fluent API 會勝過更一般的配置慣例。
內建慣例
由於自訂慣例可能會受到預設 Code First 慣例的影響,因此新增可在另一個慣例之前或之後執行的慣例會很有用。 你可以在你的衍生 DbContext 中的 Conventions 集合上,應用 AddBefore 和 AddAfter 這兩個方法。 以下程式碼會加入我們先前建立的慣例類別,使其能在內建的金鑰發現慣例之前執行。
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
這在新增需要在內建慣例之前或之後執行的慣例時最有用,內建慣例清單可在此找到: System.Data.Entity.ModelConfiguration.Conventions 命名空間。
你也可以移除不希望套用到模型上的慣例。 要移除慣例,請使用移除方法。 這裡有一個移除 PluralizingTableNameConvention 的範例。
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}