Fluent API - 設定與映射屬性與類型

使用 Entity Framework Code First 時,預設行為是將 POCO 類別映射到資料表,使用內建於 EF 的一組慣例。 然而,有時候你可能無法或不想遵循這些慣例,需要將實體映射到與慣例不同的其他項目。

你可以透過兩種主要方式來配置 Entity Framework 以使用非慣例方法,分別是註解或 Entity Framework 的流暢 API 語法。 註解只涵蓋流暢 API 功能的部分,因此存在無法僅靠註解實現的映射情境。 本文旨在示範如何使用 Fluent API 來設定屬性。

通常,您可以透過在衍生DbContext中覆寫OnModelCreating方法來存取程式碼優先API。 以下範例旨在展示如何使用流暢 API 執行各種任務,並允許您複製程式碼並自訂以符合您的模型,若您想 as-is 查看可用於的模型,本文末尾已提供。

模型全域設定

預設架構(EF6 起)

從 EF6 開始,你可以使用 DbModelBuilder 上的 HasDefaultSchema 方法,指定所有資料表、儲存程序等的資料庫結構。這個預設設定會被你明確設定不同結構的物件覆蓋。

modelBuilder.HasDefaultSchema("sales");

自訂慣例(EF6 起)

從 EF6 開始,你可以建立自己的慣例來補充 Code First 所包含的。 欲了解更多細節,請參閱 自訂程式碼優先慣例

屬性映射

Property 方法用於為屬於實體或複雜型態的每個屬性配置屬性。 Property 方法用於取得特定屬性的配置物件。 設定物件上的選項依被設定的類型而異;例如,IsUnicode 僅適用於字串屬性。

主鍵配置

Entity Framework 對主鍵的慣例如下:

  1. 你的類別定義了一個屬性,其名稱為「ID」或「Id」
  2. 或是類別名稱後接「ID」或「Id」

要明確設定屬性為主鍵,可以使用 HasKey 方法。 以下範例中,使用 HasKey 方法來設定 OfficeAssignment 類型的 InstructorID 主鍵。

modelBuilder.Entity<OfficeAssignment>().HasKey(t => t.InstructorID);

配置複合主鍵

以下範例將 DepartmentID 與 Name 屬性配置為部門類型的複合主鍵。

modelBuilder.Entity<Department>().HasKey(t => new { t.DepartmentID, t.Name });

關閉數值型主鍵的識別功能

以下範例將 DepartmentID 屬性設為 System.ComponentModel.DataAnnotations.DatabaseGeneratedOption.None,表示該值不會被資料庫產生。

modelBuilder.Entity<Department>().Property(t => t.DepartmentID)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

指定屬性的最大長度

以下範例中,名稱屬性不應超過50個字元。 如果你讓值超過 50 個字元,就會收到 DbEntityValidationException 例外。 若 Code First 從此模型建立資料庫,也會將名稱欄位的最大長度設為 50 個字元。

modelBuilder.Entity<Department>().Property(t => t.Name).HasMaxLength(50);

設定屬性為必須

以下範例中,需要 Name 屬性。 如果你沒有指定名稱,將會收到 DbEntityValidationException 例外。 如果 Code First 從此模型建立資料庫,則用來儲存此屬性的欄位通常是不可空的。

備註

在某些情況下,即使該屬性是必需的,資料庫中的欄位可能無法成為非空。 例如,使用 TPH 繼承策略時,多種類型的資料會儲存在單一表格中。 若導出型態包含必修屬性,該欄位無法成為非空,因為階層中並非所有型別都具備此屬性。

modelBuilder.Entity<Department>().Property(t => t.Name).IsRequired();

在一個或多個屬性上設定索引

備註

僅適用 EF6.1 及後續版本 - 索引屬性於 Entity Framework 6.1 中引入。 如果您使用的是較早期版本,本節的資訊不適用。

Fluent API 原生不支援建立索引,但你可以利用 Fluent API 對 IndexAttribute 的支援。 索引屬性的處理方式是先在模型上加入模型註解,然後在後續流程中轉為資料庫中的索引。 你可以用 Fluent API 手動新增這些註解。

最簡單的方法是建立一個包含新索引所有設定的 IndexAttribute 實例。 接著你可以建立一個 IndexAnnotation 實例,這是 EF 專屬的類型,會將 IndexAttribute 設定轉換成可儲存在 EF 模型上的模型註解。 這些註解接著可以傳給 Fluent API 上的 HasColumnAnnotation 方法,並指定註解名稱為 Index

modelBuilder
    .Entity<Department>()
    .Property(t => t.Name)
    .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()));

欲了解 IndexAttribute 中可完整設定清單,請參閱 Code First Data Annotations索引區塊。 這包括自訂索引名稱、建立唯一索引,以及建立多欄位索引。

你可以透過將 IndexAttribute 的陣列傳給 IndexAnnotation 的建構子,來指定單一屬性上的多個索引註解。

modelBuilder
    .Entity<Department>()
    .Property(t => t.Name)
    .HasColumnAnnotation(
        "Index",  
        new IndexAnnotation(new[]
            {
                new IndexAttribute("Index1"),
                new IndexAttribute("Index2") { IsUnique = true }
            })));

指定不要將 CLR 屬性映射到資料庫中的欄位

以下範例說明如何指定 CLR 類型的屬性不會映射到資料庫中的欄位。

modelBuilder.Entity<Department>().Ignore(t => t.Budget);

將 CLR 屬性映射到資料庫中的特定欄位

以下範例將 Name CLR 屬性映射到 DepartmentName 資料庫欄位。

modelBuilder.Entity<Department>()
    .Property(t => t.Name)
    .HasColumnName("DepartmentName");

重新命名模型中未定義的外鍵

如果你選擇不在 CLR 類型上定義外鍵,但想指定它在資料庫中應有的名稱,請執行以下步驟:

modelBuilder.Entity<Course>()
    .HasRequired(c => c.Department)
    .WithMany(t => t.Courses)
    .Map(m => m.MapKey("ChangedDepartmentID"));

設定字串屬性是否支援 Unicode 內容

預設字串為 Unicode(SQL Server 中為 nvarchar)。 你可以使用 IsUnicode 方法指定字串應該是 varchar 型。

modelBuilder.Entity<Department>()
    .Property(t => t.Name)
    .IsUnicode(false);

設定資料庫欄位的資料型態

HasColumnType 方法允許將資料映射到同一基本型別的不同表示方式。 使用此方法無法在執行時進行任何資料轉換。 請注意,IsUnicode 是將欄位設定為 varchar 的首選方式,因為它與資料庫無關。

modelBuilder.Entity<Department>()   
    .Property(p => p.Name)   
    .HasColumnType("varchar");

複雜型態上的屬性配置

在複雜型態上配置純量屬性有兩種方式。

你可以在 ComplexTypeConfiguration 上呼叫 Property。

modelBuilder.ComplexType<Details>()
    .Property(t => t.Location)
    .HasMaxLength(20);

你也可以用點符號來存取複數屬性。

modelBuilder.Entity<OnsiteCourse>()
    .Property(t => t.Details.Location)
    .HasMaxLength(20);

配置屬性作為樂觀並發代幣

要指定實體中的屬性代表並行標記,您可以使用 ConcurrencyCheck 屬性或 IsConcurrencyToken 方法。

modelBuilder.Entity<OfficeAssignment>()
    .Property(t => t.Timestamp)
    .IsConcurrencyToken();

你也可以使用 IsRowVersion 方法,將該屬性設定為資料庫中的列版本。 將屬性設置為行版本時,會自動將其配置為樂觀並發控制標記。

modelBuilder.Entity<OfficeAssignment>()
    .Property(t => t.Timestamp)
    .IsRowVersion();

類型映射

指定一個類別是複數型態

依慣例,未指定主鍵的型別被視為複數型。 有些情況下,Code First 無法偵測到複合型別(例如:如果您有一個名為 ID 的屬性,但您並不打算將其設為主鍵)。 在這種情況下,你會使用 fluent API 明確指定一個型別是複型態。

modelBuilder.ComplexType<Details>();

指定不將 CLR 實體類型映射到資料庫中的資料表

以下範例說明如何排除 CLR 類型被映射到資料庫中的資料表。

modelBuilder.Ignore<OnlineCourse>();

將實體類型映射到資料庫中的特定資料表

部門的所有屬性都會映射到一個稱為 t_ 部門的欄位。

modelBuilder.Entity<Department>()  
    .ToTable("t_Department");

你也可以這樣指定結構名稱:

modelBuilder.Entity<Department>()  
    .ToTable("t_Department", "school");

映射單一繼承結構(Table-Per-Hierarchy,TPH)

在 TPH 映射情境中,繼承階層中的所有類型都映射到同一個資料表。 判別子欄位用來識別每列的類型。 在使用 Code First 建立模型時,TPH 是參與繼承階層的類型預設策略。 預設情況下,判別器欄位會以「Discriminator」名稱加入資料表,並使用階層中每個型態的 CLR 類型名稱作為判別器值。 你可以透過使用 Fluent API 來修改預設行為。

modelBuilder.Entity<Course>()  
    .Map<Course>(m => m.Requires("Type").HasValue("Course"))  
    .Map<OnsiteCourse>(m => m.Requires("Type").HasValue("OnsiteCourse"));

類型表映射(TPT)繼承

在 TPT 映射情境中,所有類型都被映射到個別資料表。 只屬於基底類型或衍生型別的屬性會儲存在對應至該類型的數據表中。 對應至衍生型別的數據表也會儲存外鍵,以聯結衍生數據表與基表。

modelBuilder.Entity<Course>().ToTable("Course");  
modelBuilder.Entity<OnsiteCourse>().ToTable("OnsiteCourse");

映射具體類別的表格(TPC)繼承

在 TPC 映射情境中,階層中所有非抽象型態都會映射到個別資料表。 映射到衍生類別的資料表與資料庫中映射到基底類別的資料表沒有任何關聯。 類別的所有屬性,包括繼承的屬性,都會映射到對應資料表的欄位。

呼叫 MapInheritedProperties 方法來設定每個導出型別。 MapInheritedProperties 會將所有從基底類別繼承的屬性重新映射到該衍生類別的表格中新欄位。

備註

請注意,由於參與 TPC 繼承階層的資料表並未共用主鍵,因此若資料庫產生的值具有相同的識別種子,當插入映射至子類別的資料表時,可能會出現重複的實體鍵。 為了解決這個問題,你可以為每個資料表指定不同的初始種子值,或是關閉主鍵屬性的身份。 在使用 Code First 時,整數鍵屬性的預設值是 Identity。

modelBuilder.Entity<Course>()
    .Property(c => c.CourseID)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

modelBuilder.Entity<OnsiteCourse>().Map(m =>
{
    m.MapInheritedProperties();
    m.ToTable("OnsiteCourse");
});

modelBuilder.Entity<OnlineCourse>().Map(m =>
{
    m.MapInheritedProperties();
    m.ToTable("OnlineCourse");
});

將某一實體類型的屬性映射到資料庫中的多個資料表(實體分割)

實體分割允許將一個實體類型的屬性分散在多個資料表中。 在以下範例中,部門實體被拆分為兩個表格:部門與部門詳細資料。 實體分割會多次呼叫 Map 方法,將部分屬性映射到特定資料表。

modelBuilder.Entity<Department>()
    .Map(m =>
    {
        m.Properties(t => new { t.DepartmentID, t.Name });
        m.ToTable("Department");
    })
    .Map(m =>
    {
        m.Properties(t => new { t.DepartmentID, t.Administrator, t.StartDate, t.Budget });
        m.ToTable("DepartmentDetails");
    });

將多個實體類型映射到資料庫中的一個資料表(資料表分割)

以下範例將兩個共用主鍵的實體類型對應到同一個資料表。

modelBuilder.Entity<OfficeAssignment>()
    .HasKey(t => t.InstructorID);

modelBuilder.Entity<Instructor>()
    .HasRequired(t => t.OfficeAssignment)
    .WithRequiredPrincipal(t => t.Instructor);

modelBuilder.Entity<Instructor>().ToTable("Instructor");

modelBuilder.Entity<OfficeAssignment>().ToTable("Instructor");

映射實體類型以插入/更新/刪除儲存程序(EF6 起)

從 EF6 開始,你可以對應實體,以使用預存程序進行插入、更新和刪除。 更多細節請參見「 程式碼優先插入/更新/刪除儲存程序」。

樣品中使用的模型

以下 Code First 模型用於本頁的範例。

using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
// add a reference to System.ComponentModel.DataAnnotations DLL
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System;

public class SchoolEntities : DbContext
{
    public DbSet<Course> Courses { get; set; }
    public DbSet<Department> Departments { get; set; }
    public DbSet<Instructor> Instructors { get; set; }
    public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Configure Code First to ignore PluralizingTableName convention
        // If you keep this convention then the generated tables will have pluralized names.
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }
}

public class Department
{
    public Department()
    {
        this.Courses = new HashSet<Course>();
    }
    // Primary key
    public int DepartmentID { get; set; }
    public string Name { get; set; }
    public decimal Budget { get; set; }
    public System.DateTime StartDate { get; set; }
    public int? Administrator { get; set; }

    // Navigation property
    public virtual ICollection<Course> Courses { get; private set; }
}

public class Course
{
    public Course()
    {
        this.Instructors = new HashSet<Instructor>();
    }
    // Primary key
    public int CourseID { get; set; }

    public string Title { get; set; }
    public int Credits { get; set; }

    // Foreign key
    public int DepartmentID { get; set; }

    // Navigation properties
    public virtual Department Department { get; set; }
    public virtual ICollection<Instructor> Instructors { get; private set; }
}

public partial class OnlineCourse : Course
{
    public string URL { get; set; }
}

public partial class OnsiteCourse : Course
{
    public OnsiteCourse()
    {
        Details = new Details();
    }

    public Details Details { get; set; }
}

public class Details
{
    public System.DateTime Time { get; set; }
    public string Location { get; set; }
    public string Days { get; set; }
}

public class Instructor
{
    public Instructor()
    {
        this.Courses = new List<Course>();
    }

    // Primary key
    public int InstructorID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public System.DateTime HireDate { get; set; }

    // Navigation properties
    public virtual ICollection<Course> Courses { get; private set; }
}

public class OfficeAssignment
{
    // Specifying InstructorID as a primary
    [Key()]
    public Int32 InstructorID { get; set; }

    public string Location { get; set; }

    // When Entity Framework sees Timestamp attribute
    // it configures ConcurrencyCheck and DatabaseGeneratedPattern=Computed.
    [Timestamp]
    public Byte[] Timestamp { get; set; }

    // Navigation property
    public virtual Instructor Instructor { get; set; }
}