实体框架 - Fluent API



Fluent API 是一种指定模型配置的更高级方法,它涵盖了数据注解可以执行的所有操作,此外还有一些数据注解无法实现的更高级配置。数据注解和 Fluent API 可以一起使用,但 Code First 优先级为 Fluent API > 数据注解 > 默认约定。

  • Fluent API 是配置领域类(domain classes)的另一种方法。

  • Code First Fluent API 最常通过覆盖派生 DbContext 上的 OnModelCreating 方法来访问。

  • Fluent API 提供比 DataAnnotations 更强大的配置功能。Fluent API 支持以下类型的映射。

在本章中,我们将继续使用简单的示例,该示例包含 Student、Course 和 Enrollment 类以及一个名为 MyContext 的上下文类,如下面的代码所示。

using System.Data.Entity; 
using System.Linq; 
using System.Text;
using System.Threading.Tasks;  

namespace EFCodeFirstDemo {

   class Program {
      static void Main(string[] args) {}
   }
   
   public enum Grade {
      A, B, C, D, F
   }

   public class Enrollment {
      public int EnrollmentID { get; set; }
      public int CourseID { get; set; }
      public int StudentID { get; set; }
      public Grade? Grade { get; set; }
		
      public virtual Course Course { get; set; }
      public virtual Student Student { get; set; }
   }

   public class Student {
      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
		
      public DateTime EnrollmentDate { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class Course {
      public int CourseID { get; set; }
      public string Title { get; set; }
      public int Credits { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class MyContext : DbContext {
      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
   }

}	  

要访问 Fluent API,您需要重写 DbContext 中的 OnModelCreating 方法。让我们来看一个简单的示例,在这个示例中,我们将学生表中的列名从 FirstMidName 重命名为 FirstName,如下面的代码所示。

public class MyContext : DbContext {

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      modelBuilder.Entity<Student>().Property(s ⇒ s.FirstMidName)
      .HasColumnName("FirstName");}

      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
}

DbModelBuilder 用于将 CLR 类映射到数据库模式。它是主类,您可以在其中配置所有领域类。这种以代码为中心的构建实体数据模型 (EDM) 的方法称为代码优先。

Fluent API 提供了许多重要的方法来配置实体及其属性,以覆盖各种 Code First 约定。以下是一些方法。

序号 方法名称及描述
1

ComplexType<TComplexType>

将类型注册为模型中的复杂类型,并返回一个可用于配置复杂类型的对象。可以对同一类型多次调用此方法以执行多行配置。

2

Entity<TEntityType>

将实体类型注册为模型的一部分,并返回一个可用于配置实体的对象。可以对同一实体多次调用此方法以执行多行配置。

3

HasKey<TKey>

配置此实体类型的主键属性。

4

HasMany<TTargetEntity>

配置从此实体类型到另一个实体类型的多对多关系。

5

HasOptional<TTargetEntity>

配置从此实体类型到另一个实体类型的可选关系。即使未指定此关系,也可以将实体类型的实例保存到数据库中。数据库中的外键将可为空。

6

HasRequired<TTargetEntity>

配置从此实体类型到另一个实体类型的必需关系。除非指定此关系,否则无法将实体类型的实例保存到数据库中。数据库中的外键将不可为空。

7

Ignore<TProperty>

从模型中排除属性,使其不会映射到数据库。(继承自 StructuralTypeConfiguration<TStructuralType>)

8

Property<T>

配置在此类型上定义的结构属性。(继承自 StructuralTypeConfiguration<TStructuralType>)

9

ToTable(String)

配置此实体类型映射到的表名。

Fluent API 允许您配置实体或其属性,无论您是想更改它们映射到数据库的方式还是它们彼此之间如何关联。您可以使用配置影响各种映射和建模。以下是 Fluent API 支持的主要映射类型:

  • 实体映射
  • 属性映射

实体映射

实体映射只是一些简单的映射,这些映射会影响实体框架对类如何映射到数据库的理解。所有这些我们在数据注解中都讨论过,在这里我们将看到如何使用 Fluent API 来实现相同的功能。

  • 因此,与其进入领域类来添加这些配置,我们可以在上下文中执行此操作。

  • 首先是重写 OnModelCreating 方法,该方法提供 modelBuilder 进行操作。

默认架构

生成数据库时,默认架构为 dbo。您可以使用 DbModelBuilder 上的 HasDefaultSchema 方法指定用于所有表、存储过程等的数据库架构。

让我们来看下面的示例,其中应用了 admin 架构。

public class MyContext : DbContext {
   public MyContext() : base("name = MyContextDB") {}

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      //Configure default schema
      modelBuilder.HasDefaultSchema("Admin");
   }
	
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

将实体映射到表

根据默认约定,Code First 将使用上下文类中 DbSet 属性的名称创建数据库表,例如 Courses、Enrollments 和 Students。但是,如果您想要不同的表名,则可以覆盖此约定,并提供与 DbSet 属性不同的表名,如下面的代码所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Map entity to table
   modelBuilder.Entity<Student>().ToTable("StudentData");
   modelBuilder.Entity<Course>().ToTable("CourseDetail");
   modelBuilder.Entity<Enrollment>().ToTable("EnrollmentInfo");
}

生成数据库时,您将看到在 OnModelCreating 方法中指定的表名。

OnModel Method

实体拆分(将实体映射到多个表)

实体拆分允许您将来自多个表的数据组合到单个类中,并且只能与表之间存在一对一关系的表一起使用。让我们来看下面的示例,其中学生信息映射到两个表中。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Map entity to table
   modelBuilder.Entity<Student>().Map(sd ⇒ {
      sd.Properties(p ⇒ new { p.ID, p.FirstMidName, p.LastName });
      sd.ToTable("StudentData");
   })

   .Map(si ⇒ {
      si.Properties(p ⇒ new { p.ID, p.EnrollmentDate });
      si.ToTable("StudentEnrollmentInfo");
   });

   modelBuilder.Entity<Course>().ToTable("CourseDetail");
   modelBuilder.Entity<Enrollment>().ToTable("EnrollmentInfo");
}

在上面的代码中,您可以看到 Student 实体通过使用 Map 方法将一些属性映射到 StudentData 表,将一些属性映射到 StudentEnrollmentInfo 表来拆分为以下两个表。

  • **StudentData** - 包含 Student FirstMidName 和 Last Name。

  • **StudentEnrollmentInfo** - 包含 EnrollmentDate。

生成数据库时,您将在数据库中看到以下表,如下图所示。

Entity Splitting

属性映射

Property 方法用于配置属于实体或复杂类型的每个属性的属性。Property 方法用于获取给定属性的配置对象。您还可以使用 Fluent API 映射和配置领域类的属性。

配置主键

主键的默认约定是:

  • 类定义一个名为“ID”或“Id”的属性
  • 类名后跟“ID”或“Id”

如果您的类不遵循主键的默认约定,如下面的 Student 类代码所示:

public class Student {
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

然后,要显式地将属性设置为主键,可以使用 HasKey 方法,如下面的代码所示:

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
	
   // Configure Primary Key
   modelBuilder.Entity<Student>().HasKey<int>(s ⇒ s.StdntID); 
}

配置列

在实体框架中,默认情况下,Code First 将为属性创建具有相同名称、顺序和数据类型的列。但是,您也可以覆盖此约定,如下面的代码所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Configure EnrollmentDate Column
   modelBuilder.Entity<Student>().Property(p ⇒ p.EnrollmentDate)
	
   .HasColumnName("EnDate")
   .HasColumnType("DateTime")
   .HasColumnOrder(2);
}

配置 MaxLength 属性

在下面的示例中,Course Title 属性的长度不应超过 24 个字符。当用户指定的值超过 24 个字符时,用户将收到 DbEntityValidationException 异常。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
   modelBuilder.Entity<Course>().Property(p ⇒ p.Title).HasMaxLength(24);
}

配置 Null 或 NotNull 属性

在下面的示例中,Course Title 属性是必需的,因此使用 IsRequired 方法创建 NotNull 列。类似地,Student EnrollmentDate 是可选的,因此我们将使用 IsOptional 方法允许此列中出现空值,如下面的代码所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
   modelBuilder.Entity<Course>().Property(p ⇒ p.Title).IsRequired();
   modelBuilder.Entity<Student>().Property(p ⇒ p.EnrollmentDate).IsOptional();
	
   //modelBuilder.Entity<Student>().Property(s ⇒ s.FirstMidName)
   //.HasColumnName("FirstName"); 
}

配置关系

在数据库的上下文中,关系是指两个关系数据库表之间存在的一种情况,其中一个表具有引用另一个表主键的外键。使用 Code First 时,您可以通过定义领域 CLR 类来定义模型。默认情况下,实体框架使用 Code First 约定将您的类映射到数据库模式。

  • 如果您使用 Code First 命名约定,在大多数情况下,您可以依靠 Code First 根据外键和导航属性来设置表之间的关系。

  • 如果它们不符合这些约定,您还可以使用配置来影响类之间的关系以及在 Code First 中添加配置时这些关系如何在数据库中实现。

  • 部分注解可在数据注解中找到,您甚至可以使用 Fluent API 应用更复杂的注解。

配置一对一关系

在模型中定义一对一关系时,您在每个类中使用一个引用导航属性。在数据库中,两个表在关系的任何一侧都只能有一条记录。每个主键值只与相关表中的一条记录(或没有记录)相关。

  • 如果两个相关列都是主键或具有唯一约束,则会创建一对一关系。

  • 在一对一关系中,主键还充当外键,并且任一表都没有单独的外键列。

  • 这种类型的关系并不常见,因为大多数以这种方式相关的信息都将位于一个表中。

让我们来看下面的例子,我们将向模型中添加另一个类来创建一对一关系。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual StudentLogIn StudentLogIn { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class StudentLogIn {
   [Key, ForeignKey("Student")]
   public int ID { get; set; }
   public string EmailID { get; set; }
   public string Password { get; set; }
	
   public virtual Student Student { get; set; }
}

正如您在上面的代码中看到的,为了将`StudentLogIn`类的`ID`属性标记为主键和外键,使用了`Key`和`ForeignKey`属性。

要使用 Fluent API 配置`Student`和`StudentLogIn`之间的一对零或一对一关系,您需要覆盖`OnModelCreating`方法,如下面的代码所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure ID as PK for StudentLogIn
   modelBuilder.Entity<StudentLogIn>()
   .HasKey(s ⇒ s.ID);

   // Configure ID as FK for StudentLogIn
   modelBuilder.Entity<Student>()
   
   .HasOptional(s ⇒ s.StudentLogIn) //StudentLogIn is optional
   .WithRequired(t ⇒ t.Student); // Create inverse relationship
}

在大多数情况下,实体框架可以推断出哪种类型是依赖类型,哪种类型是关系中的主类型。但是,当关系的两端都需要或两端都是可选时,实体框架无法识别依赖项和主项。当关系的两端都需要时,您可以使用`HasRequired`,如下面的代码所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure ID as PK for StudentLogIn
   modelBuilder.Entity<StudentLogIn>()
   .HasKey(s ⇒ s.ID);

   // Configure ID as FK for StudentLogIn
   modelBuilder.Entity<Student>()
   .HasRequired(r ⇒ r.Student)
   .WithOptional(s ⇒ s.StudentLogIn);  
}

生成数据库时,您将看到关系的创建方式,如下面的图像所示。

Created Relationship

配置一对多关系

主键表只包含一条记录,该记录与相关表中的零条、一条或多条记录相关。这是最常用的关系类型。

  • 在这种类型的关系中,表 A 中的一行可以有多行与表 B 中的行匹配,但表 B 中的一行只能与表 A 中的一行匹配。

  • 外键定义在表示关系多端的表上。

  • 例如,在上图中,`Student`和`Enrollment`表具有多对一关系,每个学生可能有多个注册,但每个注册只属于一个学生。

以下是具有多对一关系的`Student`和`Enrollment`,但`Enrollment`表中的外键不遵循默认的 Code First 约定。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
	
   //StdntID is not following code first conventions name
   public int StdntID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }
}

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual StudentLogIn StudentLogIn { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

在这种情况下,要使用 Fluent API 配置一对多关系,您需要使用`HasForeignKey`方法,如下面的代码所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Configure FK for one-to-many relationship
   modelBuilder.Entity<Enrollment>()

   .HasRequired<Student>(s ⇒ s.Student)
   .WithMany(t ⇒ t.Enrollments)
   .HasForeignKey(u ⇒ u.StdntID);  
}

生成数据库时,您将看到关系的创建方式,如下面的图像所示。

HasRequired Method

在上面的例子中,`HasRequired`方法指定`Student`导航属性必须为`Null`。因此,每次添加或更新`Enrollment`时,都必须为`Enrollment`实体分配`Student`。为了处理这个问题,我们需要使用`HasOptional`方法而不是`HasRequired`方法。

配置多对多关系

两个表中的每条记录都可以与另一个表中的任意数量的记录(或没有记录)相关。

  • 您可以通过定义一个第三个表(称为连接表)来创建这种关系,该表的键由表 A 和表 B 的外键组成。

  • 例如,`Student`表和`Course`表具有多对多关系。

以下是`Student`和`Course`类,其中`Student`和`Course`具有多对多关系,因为这两个类都具有作为集合的导航属性`Students`和`Courses`。换句话说,一个实体拥有另一个实体集合。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Course> Courses { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Student> Students { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

要使用 Fluent API 配置`Student`和`Course`之间的多对多关系,您可以使用以下代码所示的 Fluent API。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure many-to-many relationship
   modelBuilder.Entity<Student>()
   .HasMany(s ⇒ s.Courses) 
   .WithMany(s ⇒ s.Students);
}

生成数据库时,将使用默认的 Code First 约定创建一个连接表。结果,将创建`StudentCourses`表,其中包含`Course_CourseID`和`Student_ID`列,如下面的图像所示。

Join Table

如果要指定连接表的名称和表中列的名称,则需要使用`Map`方法进行其他配置。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure many-to-many relationship 
   modelBuilder.Entity<Student>()

   .HasMany(s ⇒ s.Courses)
   .WithMany(s ⇒ s.Students)
   
   .Map(m ⇒ {
      m.ToTable("StudentCoursesTable");
      m.MapLeftKey("StudentID");
      m.MapRightKey("CourseID");
   }); 
}

您可以看到,生成数据库时,表和列的名称将按上述代码中指定的创建。

Join Table

我们建议您逐步执行以上示例,以便更好地理解。

广告