自訂程式碼第一慣例
注意
僅限 EF6 及更新版本 - Entity Framework 6 已引進此頁面中所討論的功能及 API 等等。 如果您使用的是較早版本,則不適用部分或全部的資訊。
使用 Code First 時,會使用一組慣例,從類別計算模型。 預設 的 Code First 慣例 會決定哪些屬性會變成實體的主鍵、實體所對應的資料表名稱,以及小數點資料行預設的精確度和小數位數。
有時候,這些預設慣例不適合您的模型,而且您必須使用資料批註或 Fluent API 來設定許多個別實體來解決這些問題。 自訂程式碼第一慣例可讓您定義自己的慣例,為您的模型提供組態預設值。 在本逐步解說中,我們將探索不同類型的自訂慣例,以及如何建立它們。
模型型慣例
此頁面涵蓋自訂慣例的 DbModelBuilder API。 此 API 應該足以撰寫大部分的自訂慣例。 不過,也能夠撰寫模型型慣例-一旦建立後操作最終模型的慣例,來處理進階案例。 如需詳細資訊,請參閱 模型型慣例 。
我們的模型
讓我們先定義可與慣例搭配使用的簡單模型。 將下列類別新增至您的專案。
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));
此程式碼會將模型中的類型設定為包含 int Key 資料行和字串 Name 資料行的複合索引鍵。 如果我們在設計工具中檢視模型,看起來會像這樣:
屬性慣例的另一個範例是設定我模型中的所有 DateTime 屬性,以對應至 SQL Server 中的 datetime2 類型,而不是 datetime。 您可以使用下列專案來達成此目的:
modelBuilder.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
慣例類別
定義慣例的另一種方式是使用慣例類別來封裝您的慣例。 使用慣例類別時,您會在 System.Data.Entity.ModelConfiguration.Convention 命名空間中建立繼承自 Convention 類別的類型。
我們可以藉由執行下列動作,使用我們稍早顯示的 datetime2 慣例來建立慣例類別:
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
若要告訴 EF 使用此慣例,請將它新增至 OnModelCreating 中的 Convention 集合,如果您已遵循此慣例,本逐步解說看起來會像這樣:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name.EndsWith("Key"))
.Configure(p => p.IsKey());
modelBuilder.Conventions.Add(new DateTime2Convention());
}
如您所見,我們會將慣例的實例新增至慣例集合。 繼承自慣例提供跨小組或專案分組和共用慣例的便利方式。 例如,您可以讓類別庫具有一組通用慣例,讓所有組織專案都使用。
自訂屬性
另一個絕佳的慣例用法是啟用在設定模型時要使用的新屬性。 為了說明這一點,讓我們建立可用來將 String 屬性標示為非 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;
}
}
一旦有了這個,我們可以在 屬性上設定 bool,以告訴慣例屬性是否應該是 Unicode。 我們可以在慣例中執行這項操作,方法是存取組態類別的 ClrProperty,如下所示:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
.Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
這已經足夠簡單,但使用慣例 API 的 Having 方法可以更簡潔地達成此目標。 Having 方法具有 Func < PropertyInfo 類型的參數,T > 接受 PropertyInfo 與 Where 方法相同,但預期會傳回物件。 如果傳回的物件為 Null,則不會設定屬性,這表示您可以篩選掉屬性與 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,將類型名稱傳遞為上述資料表的名稱,則您會將預設的「每一階層」對應策略變更為「每一類型資料表」(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 的 MaxLength,而任何其他字串,例如描述,都會是 500。 以這種方式使用慣例表示您可以為模型中的類型或屬性提供一般慣例,然後針對不同子集來覆寫這些慣例。
Fluent API 和資料批註也可以用來覆寫特定案例中的慣例。 在上述範例中,如果我們已使用 Fluent API 來設定屬性的最大長度,則我們可以在慣例之前或之後放置它,因為更具體的 Fluent API 將勝過較一般組態慣例。
內建慣例
因為自訂慣例可能會受到預設 Code First 慣例的影響,所以新增慣例以在另一個慣例之前或之後執行會很有用。 若要這樣做,您可以在衍生的 DbCoNtext 上使用慣例集合的 AddBefore 和 AddAfter 方法。 下列程式碼會新增我們稍早建立的慣例類別,以便在內建金鑰探索慣例之前執行。
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
新增需要在內建慣例之前或之後執行的慣例時,這會是最常使用的慣例,您可以在這裡找到內建慣例的清單: System.Data.Entity.ModelConfiguration.Conventions 命名空間 。
您也可以移除您不想要套用至模型的慣例。 若要移除慣例,請使用 Remove 方法。 以下是移除 PluralizingTableNameConvention 的範例。
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}