Fluent API - 配置和映射属性和类型

使用 Entity Framework Code First 时,默认行为是使用 EF 中的一组约定将 POCO 类映射到表。 但有时,你无法或不想遵循这些约定,并且需要将实体映射到约定规定之外的内容。

可以通过两种主要方式将 EF 配置为使用约定之外的其他内容,即注释或 EF Fluent API。 注释仅包含 Fluent API 功能的一个子集,因此存在无法使用注释实现的映射方案。 本文旨在演示如何使用 Fluent API 配置属性。

通常通过重写派生的 DbContext 上的 OnModelCreating 方法来访问 Code First Fluent API。 下面的示例旨在演示如何使用 Fluent API 完成各种任务,并使你能够复制代码并对其进行自定义,使之适用于你的模型。如果你希望查看可以按原样使用的模型,则本文末尾会提供这些模型。

模型范围内的设置

默认架构(EF6 以上版本)

从 EF6 开始,可以使用 DbModelBuilder 上的 HasDefaultSchema 方法来指定要用于所有表、存储过程等的数据库架构。对于为其显式配置不同架构的任何对象,将覆盖此默认设置。

modelBuilder.HasDefaultSchema("sales");

自定义约定(EF6 以上版本)

从 EF6 开始,你可以创建自己的约定来对 Code First 中包含的约定进行补充。 有关详细信息,请参阅自定义 Code First 约定

属性映射

属性方法用于为属于实体或复杂类型的每个属性配置特性。 属性方法用于获取给定属性的配置对象。 配置对象上的选项特定于正在配置的类型;例如,IsUnicode 仅适用于字符串属性。

配置主键

有关主键的实体框架约定如下:

  1. 类定义一个名称为“ID”或“Id”的属性
  2. 或后跟“ID”或“Id”的类名

若要将属性显式设置为主键,可以使用 HasKey 方法。 在下面的示例中,HasKey 方法用于对 OfficeAssignment 类型配置 InstructorID 主键。

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

配置组合主键

下面的示例将 DepartmentID 和 Name 属性配置为 Department 类型的复合主键。

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);

指定属性的最大长度

在下面的示例中,Name 属性的长度不应超过 50 个字符。 如果该值的长度超过 50 个字符,你会收到 DbEntityValidationException 异常。 如果 Code First 通过此模型创建数据库,则还会将 Name 列的最大长度设置为 50 个字符。

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

将属性配置为必需项

在下面的示例中,Name 属性是必需项。 如果未指定 Name,则会出现 DbEntityValidationException 异常。 如果 Code First 通过此模型创建数据库,则用于存储此属性的列通常不可为 null。

注意

在某些情况下,即使此属性是必需的,数据库中的该列也可能为 null。 例如,使用 TPH 继承策略时,多种类型的数据存储在单个表中。 如果派生的类型包含所需的属性,则该列可能为 null,因为层次结构中的所有类型并非都具有此属性。

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

对一个或多个属性配置索引

注意

仅 EF6.1 以上版本 - Index 特性已引入 Entity Framework 6.1 中。 如果使用的是早期版本,则本部分中的此信息不适用。

Fluent API 本身不支持创建索引,但你可以通过 Fluent API 利用对 IndexAttribute 的支持。 Index 特性的处理方式是在模型中包含模型注释,然后在管道中将注释转换为数据库中的索引。 可以使用 Fluent API 手动添加这些相同的注释。

执行此操作的最简单方法是创建一个 IndexAttribute 的实例,其中包含新索引的所有设置。 然后,可以创建一个 IndexAnnotation 的实例,该实例是一种 EF 特定类型,可将 IndexAttribute 设置转换为可存储在 EF 模型上的模型注释。 然后,可以将这些方法传递到 Fluent API 上的 HasColumnAnnotation 方法,为注释指定名称“索引”

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

有关 IndexAttribute 中可用设置的完整列表,请参阅Code First 数据注释的“索引”部分。 这包括自定义索引名称、创建唯一索引以及创建多列索引。

可以通过将 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>();

将实体类型映射到数据库中的特定表

Department 的所有属性都将映射到名为 t_ Department 的表中的列。

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

还可以指定架构名称,如下所示:

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

映射每个层次结构一张表 (TPH) 继承

在 TPH 映射方案中,继承层次结构中的所有类型都映射到同一个表。 鉴别器列用于标识每一行的类型。 使用 Code First 创建模型时,TPH 是针对参与继承层次结构的类型的默认策略。 默认情况下,鉴别器列会添加到名称为“鉴别器”的表中,层次结构中每种类型的 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 时整数键属性的默认值。

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");
});

将某个实体类型的属性映射到数据库中的多个表(实体拆分)

通过实体拆分,可将某个实体类型的属性分布到多个表中。 在下面的示例中,Department 实体拆分为两个表:Department 和 DepartmentDetails。 实体拆分使用对 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 插入/更新/删除存储过程

示例中使用的模型

本页上的示例使用以下 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; }
}