实体框架 - 快速指南



实体框架 - 概述

什么是实体框架?

实体框架于 2008 年首次发布,是 Microsoft 用于 .NET 应用程序与关系数据库之间交互的主要方法。实体框架是一个对象关系映射器 (ORM),它是一种简化软件中对象与关系数据库的表和列之间映射的工具。

  • 实体框架 (EF) 是 ADO.NET 的一个开源 ORM 框架,它是 .NET Framework 的一部分。

  • ORM 负责创建数据库连接和执行命令,以及获取查询结果并自动将这些结果具体化为应用程序对象。

  • ORM 还帮助跟踪对这些对象的更改,并在收到指示时,它还会将这些更改持久保存回数据库。

为什么使用实体框架?

实体框架是一个 ORM,而 ORM 的目标是通过减少应用程序中使用的数据的持久化冗余任务来提高开发人员的生产力。

  • 实体框架可以生成读取或写入数据库中数据的必要数据库命令,并为您执行这些命令。

  • 如果您正在查询,您可以使用 LINQ to Entities 对您的域对象表达您的查询。

  • 实体框架将在数据库中执行相关的查询,然后将结果具体化为您的域对象的实例,以便您在应用程序中使用。

市场上还有其他 ORM,例如 NHibernate 和 LLBLGen Pro。大多数 ORM 通常将域类型直接映射到数据库模式。

Typical ORM

实体框架具有更细粒度的映射层,因此您可以自定义映射,例如,通过将单个实体映射到多个数据库表,甚至将多个实体映射到单个表。

EF Runtime Metadata
  • 实体框架是 Microsoft 建议用于新应用程序的数据访问技术。

  • ADO.NET 似乎直接引用用于数据集和数据表的技术。

  • 实体框架是所有前瞻性投资正在进行的地方,这种情况已经持续数年了。

  • Microsoft 建议您在所有新开发中使用实体框架而不是 ADO.NET 或 LINQ to SQL。

概念模型

对于习惯于以数据库为中心的开发的开发人员来说,使用实体框架最大的转变在于它允许您专注于您的业务领域。您希望您的应用程序执行什么,而不会受到数据库功能的限制?

  • 在实体框架中,焦点被称为概念模型。它是应用程序中对象的模型,而不是您用于持久保存应用程序数据的数据库的模型。

  • 您的概念模型可能恰好与您的数据库模式一致,也可能完全不同。

  • 您可以使用 Visual Designer 定义您的概念模型,然后它可以生成您最终将在应用程序中使用的类。

  • 您只需定义您的类并使用实体框架的一个名为 Code First 的功能。然后实体框架将理解概念模型。

Conceptual Model

无论哪种方式,实体框架都会找出如何从您的概念模型移动到您的数据库。因此,您可以针对您的概念模型对象进行查询并直接使用它们。

功能

以下是实体框架的基本功能。此列表是根据最显著的功能以及关于实体框架的常见问题创建的。

  • 实体框架是 Microsoft 的工具。
  • 实体框架正在作为开源产品开发。
  • 实体框架不再与 .NET 发布周期绑定或依赖。
  • 适用于任何具有有效实体框架提供程序的关系数据库。
  • 从 LINQ to Entities 生成 SQL 命令。
  • 实体框架将创建参数化查询。
  • 跟踪对内存中对象的更改。
  • 允许插入、更新和删除命令生成。
  • 适用于可视化模型或您自己的类。
  • 实体框架支持存储过程。

实体框架 - 架构

实体框架的体系结构,从下到上,包括以下内容:

数据提供程序

这些是特定于源的提供程序,它们抽象 ADO.NET 接口以在针对概念模式进行编程时连接到数据库。

它将 LINQ 等通用 SQL 语言通过命令树转换为本地 SQL 表达式,并在特定的 DBMS 系统上执行。

实体客户端

此层将实体层公开给上层。实体客户端使开发人员能够以行和列的形式针对实体工作,使用实体 SQL 查询,而无需生成类来表示概念模式。实体客户端显示实体框架层,它们是核心功能。这些层称为实体数据模型。

Entity Data Model
  • 存储层包含以 XML 格式存储的整个数据库模式。

  • 实体层也是一个 XML 文件,它定义实体和关系。

  • 映射层是一个 XML 文件,它将概念层中定义的实体和关系与逻辑层中定义的实际关系和表映射。

  • 元数据服务也在实体客户端中表示,它提供集中式 API 来访问存储在实体、映射和存储层中的元数据。

对象服务

对象服务层是对象上下文,它表示应用程序与数据源之间交互的会话。

  • 对象上下文的主要用途是执行添加、删除实体实例以及在查询的帮助下将更改的状态保存回数据库等不同操作。

  • 它是实体框架的 ORM 层,它将数据结果表示为实体的对象实例。

  • 此服务允许开发人员通过使用 LINQ 和实体 SQL 编写查询来使用一些丰富的 ORM 功能,例如主键映射、更改跟踪等。

实体框架 - 环境设置

实体框架 6 中的新增功能?

框架具有复杂的 API,允许您对从建模到运行时行为的所有内容进行细粒度控制。实体框架 5 的一部分位于 .NET 内部。而它的另一部分位于使用 NuGet 分发的附加程序集中。

  • 实体框架的核心功能内置于 .NET Framework 中。

  • 代码优先支持,它允许实体框架使用类代替可视化模型,以及与 EF 交互的更轻量级 API 位于 NuGet 包中。

  • 核心是提供查询、更改跟踪以及将您的查询转换为 SQL 查询以及将数据返回转换为对象的所有转换。

  • 您可以将 EF 5 NuGet 包与 .NET 4 和 .NET 4.5 一起使用。

  • 一个主要的混淆点 - .NET 4.5 为核心实体框架 API 添加了对枚举和空间数据的支持,这意味着如果您将 EF 5 与 .NET 4 一起使用,则无法获得这些新功能。只有将 EF5 与 .NET 4.5 结合使用时,才能获得它们。

Framework 6

现在让我们看一下实体框架 6。在实体框架 6 中位于 .NET 内部的核心 API 现在是 NuGet 包的一部分。

Entity Framework 6

这意味着:

  • 所有实体框架都位于由 NuGet 分发的此程序集内部

  • 您将不会依赖 .NET 来提供特定功能,例如实体框架枚举支持和特殊数据支持。

  • 您会看到 EF6 的一项功能是它支持 .NET 4 的枚举和空间数据

要开始使用实体框架,您需要安装以下开发工具:

  • Visual Studio 2013 或更高版本
  • SQL Server 2012 或更高版本
  • 来自 NuGet 包的实体框架更新

Microsoft 提供了一个免费版本的 Visual Studio,其中还包含 SQL Server,您可以从 www.visualstudio.com 下载。

安装

步骤 1 - 下载完成后,运行安装程序。将显示以下对话框。

Visual Studio Installer

步骤 2 − 点击“安装”按钮,将开始安装过程。

Installation Process

步骤 3 − 安装过程完成后,您将看到以下对话框。关闭此对话框,如果需要,请重新启动计算机。

Setup Completed

步骤 4 − 从开始菜单打开 Visual Studio,将打开以下对话框。第一次准备可能需要一段时间。

Visual Studio

步骤 5 − 完成后,您将看到 Visual Studio 的主窗口。

Main Window

让我们从“文件”→“新建”→“项目”创建一个新项目

New Project

步骤 1 − 选择“控制台应用程序”并点击“确定”按钮。

步骤 2 − 在解决方案资源管理器中,右键点击您的项目。

Console Application

步骤 3 − 选择“管理 NuGet 程序包”,如上图所示,这将在 Visual Studio 中打开以下窗口。

Visual Studio 1

步骤 4 − 搜索“Entity Framework”并通过按下“安装”按钮安装最新版本。

Preview

步骤 5 − 点击“确定”。安装完成后,您将在输出窗口中看到以下消息。

Output Window

您现在可以开始您的应用程序了。

实体框架 - 数据库设置

在本教程中,我们将使用一个简单的大学数据库。整个大学数据库可能会更加复杂,但出于演示和学习目的,我们使用的是该数据库的最简单形式。下图包含三个表。

  • 学生
  • 课程
  • 选课
Database

每当使用术语数据库时,我们脑海中就会直接想到不同类型的表,这些表之间存在某种关系。表之间存在三种关系类型,不同表之间的关系取决于相关列的定义方式。

  • 一对多关系
  • 多对多关系
  • 一对一关系

一对多关系

一对多关系是最常见的关系类型。在这种关系类型中,表 A 中的一行可以在表 B 中有多行匹配行,但表 B 中的一行只能在表 A 中有一行匹配行。例如,在上图中,学生和选课表之间存在一对多关系,每个学生可能有多个选课记录,但每个选课记录只属于一个学生。

多对多关系

在多对多关系中,表 A 中的一行可以在表 B 中有多行匹配行,反之亦然。您可以通过定义一个第三个表(称为连接表)来创建这种关系,该表的 primary key 由表 A 和表 B 的 foreign keys 组成。例如,学生和课程表之间存在多对多关系,该关系由这两个表到选课表的每一个一对多关系定义。

一对一关系

在一对一关系中,表 A 中的一行最多只能在表 B 中有一行匹配行,反之亦然。如果两个相关列都是 primary key 或具有唯一约束,则会创建一对一关系。

这种关系类型并不常见,因为大多数以这种方式相关的信息都将包含在一个表中。您可以使用一对一关系来 -

  • 将一个包含许多列的表进行分割。
  • 出于安全原因隔离表的一部分。
  • 存储短暂的数据,可以通过简单地删除表来轻松删除这些数据。
  • 存储仅适用于主表子集的信息。

实体框架 - 数据模型

实体数据模型 (EDM) 是实体关系模型的扩展版本,它使用各种建模技术指定数据的概念模型。它还指的是描述数据结构的一组概念,而不管其存储形式如何。

EDM 支持一组定义概念模型中属性的原始数据类型。我们需要考虑构成 Entity Framework 基础的 3 个核心部分,它们统称为实体数据模型。以下是 EDM 的三个核心部分。

  • 存储模式模型
  • 概念模型
  • 映射模型

存储模式模型

存储模型也称为存储模式定义层 (SSDL),它表示后端数据存储的模式表示。

EDM

概念模型

概念模型也称为概念模式定义层 (CSDL),是真正的实体模型,我们针对它编写查询。

映射模型

映射层只是概念模型和存储模型之间的映射。

逻辑模式及其与物理模式的映射表示为 EDM。

  • Visual Studio 还提供实体设计器,用于可视化创建 EDM 和映射规范。

  • 该工具的输出是指定模式和映射的 XML 文件(*.edmx)。

  • Edmx 文件包含 Entity Framework 元数据工件。

模式定义语言

ADO.NET Entity Framework 使用基于 XML 的数据定义语言,称为模式定义语言 (SDL),来定义 EDM 模式。

  • SDL 定义与其他原始类型类似的简单类型,包括 String、Int32、Double、Decimal 和 DateTime 等。

  • 枚举,它定义了原始值和名称的映射,也被认为是一种简单类型。

  • 枚举仅从框架版本 5.0 开始受支持。

  • 复杂类型是由其他类型的聚合创建的。这些类型的属性集合定义实体类型。

数据模型主要包含三个关键概念来描述数据结构 -

  • 实体类型
  • 关联类型
  • 属性

实体类型

实体类型是描述 EDM 中数据结构的基本构建块。

  • 在概念模型中,实体类型由属性构成,并描述顶级概念的结构,例如业务应用程序中的学生和选课。

  • 实体表示特定对象,例如特定学生或选课记录。

  • 每个实体都必须在实体集中具有唯一的实体键。实体集是特定实体类型实例的集合。实体集(和关联集)在逻辑上分组在实体容器中。

  • 实体类型支持继承,即一个实体类型可以从另一个实体类型派生。

Entity Type

关联类型

它是描述 EDM 中关系的另一个基本构建块。在概念模型中,关联表示两个实体类型之间的关系,例如学生和选课。

  • 每个关联都有两个关联端,它们指定关联中涉及的实体类型。

  • 每个关联端还指定关联端多重性,该多重性指示关联该端的实体数量。

  • 关联端多重性可以具有值 1、0..1 或 *。

  • 关联一端的实体可以通过导航属性或通过 foreign keys(如果它们在实体类型上公开)进行访问。

属性

实体类型包含定义其结构和特征的属性。例如,学生实体类型可能具有属性,如学生 ID、姓名等。

属性可以包含原始数据(例如字符串、整数或布尔值)或结构化数据(例如复杂类型)。

实体框架 - DbContext

Entity Framework 使您能够使用公共语言运行时 (CLR) 对象(称为实体)查询、插入、更新和删除数据。Entity Framework 将模型中定义的实体和关系映射到数据库。它还提供以下功能 -

  • 将从数据库返回的数据具体化为实体对象
  • 跟踪对对象所做的更改
  • 处理并发
  • 将对象更改传播回数据库
  • 将对象绑定到控件

负责以对象形式与数据交互的主要类是 System.Data.Entity.DbContext。DbContext API 不是作为 .NET Framework 的一部分发布的。为了能够更灵活、更频繁地向 Code First 和 DbContext API 发布新功能,Entity Framework 团队通过 Microsoft 的 NuGet 分发功能分发 EntityFramework.dll。

  • NuGet 允许您通过从 Web 直接将相关的 DLL 拉入您的项目来向您的 .NET 项目添加引用。

  • 一个名为库包管理器的 Visual Studio 扩展提供了一种简单的方法,可以将相应的程序集从 Web 拉入您的项目。

DbContext
  • DbContext API 主要针对简化您与 Entity Framework 的交互。

  • 它还减少了您需要访问的常用任务的方法和属性的数量。

  • 在早期版本的 Entity Framework 中,这些任务通常难以发现和编写代码。

  • 上下文类在运行时管理实体对象,包括使用数据库中的数据填充对象、更改跟踪以及将数据持久化到数据库。

定义 DbContext 派生类

使用上下文的工作推荐方法是定义一个从 DbContext 派生的类,并公开 DbSet 属性,这些属性表示上下文中指定实体的集合。如果您使用的是 EF Designer,则将为您生成上下文。如果您使用的是 Code First,则通常会自己编写上下文。

以下代码是一个简单的示例,它显示 UniContext 是从 DbContext 派生的。

  • 您可以将自动属性与 DbSet 一起使用,例如 getter 和 setter。

  • 它还使代码更加简洁,但当您没有其他逻辑要应用时,您不需要使用它来创建 DbSet。

public class UniContext : DbContext {
   public UniContext() : base("UniContext") { }
   public DbSet<Student> Students { get; set; }
   public DbSet<Enrollment> Enrollments { get; set; }
   public DbSet<Course> Courses { get; set; }
}
  • 以前,EDM 用于生成从 ObjectContext 类派生的上下文类。

  • 使用 ObjectContext 有点复杂。

  • DbContext 是 ObjectContext 的包装器,实际上类似于 ObjectContext,在所有开发模型(如 Code First、Model First 和 Database First)中都很有用且易于使用。

查询

您可以使用三种类型的查询,例如 -

  • 添加新实体。
  • 更改或更新现有实体的属性值。
  • 删除现有实体。

添加新实体

使用 Entity Framework 添加新对象就像构造对象的新的实例并在 DbSet 上使用 Add 方法注册它一样简单。以下代码适用于您想要将新学生添加到数据库时。

private static void AddStudent() {

   using (var context = new UniContext()) {

      var student = new Student {
         LastName = "Khan", 
         FirstMidName = "Ali", 
         EnrollmentDate = DateTime.Parse("2005-09-01") 
      };

      context.Students.Add(student); 
      context.SaveChanges();

   }
}

更改现有实体

更改现有对象就像更新分配给要更改的属性的值并调用 SaveChanges 一样简单。在以下代码中,Ali 的姓氏已从 Khan 更改为 Aslam。

private static void AddStudent() {

   private static void ChangeStudent() {

      using (var context = new UniContext()) {

         var student = (from d in context.Students
            where d.FirstMidName == "Ali" select d).Single();
         student.LastName = "Aslam";
         context.SaveChanges();

      }
   }
}

删除现有实体

要使用 Entity Framework 删除实体,请在 DbSet 上使用 Remove 方法。Remove 对现有实体和新添加的实体都有效。对已添加但尚未保存到数据库的实体调用 Remove 将取消添加该实体。该实体将从更改跟踪器中删除,并且 DbContext 也不再跟踪它。对正在进行更改跟踪的现有实体调用 Remove 将在下次调用 SaveChanges 时注册要删除该实体。以下示例显示了一个实例,其中学生从数据库中删除,其名字是 Ali。

private static void DeleteStudent() {

   using (var context = new UniContext()) {
      var bay = (from d in context.Students where d.FirstMidName == "Ali" select d).Single();
      context.Students.Remove(bay);
      context.SaveChanges();
   }
}

实体框架 - 类型

在 Entity Framework 中,有两种类型的实体允许开发人员将他们自己的自定义数据类与数据模型一起使用,而无需对数据类本身进行任何修改。

  • POCO 实体
  • 动态代理

POCO 实体

  • POCO 代表“普通旧”CLR 对象,可以用作您数据模型中现有的域对象。

  • 映射到实体的 POCO 数据类在数据模型中定义。

  • 它还支持与实体数据模型工具生成的实体类型大部分相同的查询、插入、更新和删除行为。

  • 您可以使用 POCO 模板从概念模型生成不依赖于持久化的实体类型。

让我们看一下以下概念实体数据模型的示例。

Conceptual Entity Model

为上述实体模型生成 POCO 实体 -

步骤 1 − 右键单击设计器窗口。将显示以下对话框。

Designer Window

步骤 2 − 选择“添加代码生成项…”

Code Generation

步骤 3 − 选择 EF 6.x DbContext 生成器,输入名称,然后单击“添加”按钮。

您将在解决方案资源管理器中看到已生成 POCODemo.Context.tt 和 POCODemo.tt 模板。

Solution Explorer

POCODemo.Context 生成 DbContext 和您可以返回并用于查询的对象集,例如上下文、学生和课程等。

Generate

另一个模板处理所有类型 Student、Courses 等。以下是自动从实体模型生成的 Student 类的代码。

namespace ConsoleApplication1 {

   using System;
   using System.Collections.Generic;

   public partial class Student {

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2214:DoNotCallOverridableMethodsInConstructors")]

      public Student() {
         this.Enrollments = new HashSet<Enrollment>();
      }

      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
      public System.DateTime EnrollmentDate { get; set; }

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         CA2227:CollectionPropertiesShouldBeReadOnly")]

      public virtual ICollection<Enrollment> Enrollments { get; set; }

   }
}

从实体模型为 Course 和 Enrollment 表生成类似的类。

动态代理

创建 POCO 实体类型的实例时,Entity Framework 通常会创建动态生成的派生类型的实例,该实例充当实体的代理。也可以说它是在运行时的代理类,就像 POCO 实体的包装类。

  • 您可以覆盖实体的某些属性,以便在访问属性时自动执行操作。

  • 此机制用于支持关系的延迟加载和自动更改跟踪。

  • 此技术也适用于使用 Code First 和 EF Designer 创建的模型。

如果希望 Entity Framework 支持相关对象的延迟加载并跟踪 POCO 类中的更改,则 POCO 类必须满足以下要求:

  • 自定义数据类必须声明为公共访问。

  • 自定义数据类不能是密封的。

  • 自定义数据类不能是抽象的。

  • 自定义数据类必须具有一个没有参数的公共或受保护的构造函数。

  • 如果希望使用 CreateObject 方法创建 POCO 实体的代理,请使用没有参数的受保护构造函数。

  • 调用 CreateObject 方法并不能保证创建代理:POCO 类必须遵循本主题中描述的其他要求。

  • 该类不能实现 IEntityWithChangeTracker 或 IEntityWithRelationships 接口,因为代理类实现了这些接口。

  • ProxyCreationEnabled 选项必须设置为 true。

以下示例是动态代理实体类。

public partial class Course {

   public Course() {
      this.Enrollments = new HashSet<Enrollment>();
   }

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

要禁用创建代理对象,请将 ProxyCreationEnabled 属性的值设置为 false。

实体框架 - 关系

在关系数据库中,关系是通过外键存在于关系数据库表之间的关系。外键 (FK) 是一个列或列的组合,用于建立和强制两个表之间的数据链接。下图包含三个表。

  • 学生
  • 课程
  • 选课
Relational Database

在上图中,您可以看到表之间存在某种关联/关系。表之间存在三种类型的关系,不同表之间的关系取决于相关列的定义方式。

  • 一对多关系
  • 多对多关系
  • 一对一关系

一对多关系

  • 一对多关系是最常见的关系类型。

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

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

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

在实体框架中,这些关系也可以用代码创建。以下是与一对多关系关联的 Student 和 Enrollment 类的示例。

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

在上面的代码中,您可以看到 Student 类包含 Enrollment 的集合,但 Enrollment 类有一个单独的 Student 对象。

多对多关系

在多对多关系中,表 A 中的一行可以在表 B 中有多个匹配行,反之亦然。

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

  • 例如,Student 和 Course 表具有多对多关系,该关系由这两个表到 Enrollment 表的一对多关系定义。

以下代码包含 Course 类和上述两个类,即StudentEnrollment

public class Course {
   [DatabaseGenerated(DatabaseGeneratedOption.None)]
	
   public int CourseID { get; set; }
   public string Title { get; set; }
	
   public int Credits { get; set; } 
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 Course 类和 Student 类都具有 Enrollment 对象的集合,这通过连接类 Enrollment 实现了多对多关系。

一对一关系

  • 在一对一关系中,表 A 中的一行在表 B 中最多只能有一个匹配行,反之亦然。

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

  • 在一对一关系中,primary key 还会充当外键,并且两个表都没有单独的外键列。

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

  • 将一个包含许多列的表进行分割。
  • 出于安全原因隔离表的一部分。
  • 存储短暂的数据,可以通过简单地删除表来轻松删除这些数据。
  • 存储仅适用于主表子集的信息。

以下代码是添加另一个名为 StudentProfile 的类,该类包含学生的电子邮件 ID 和密码。

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 virtual StudentProfile StudentProfile { get; set; }
}

public class StudentProfile {

   public StudentProfile() {}
   public int ID { get; set; }
   public string Email { get; set; }
   public string Password { get; set; }
	
   public virtual Student Student { get; set; }
}

您可以看到 Student 实体类包含 StudentProfile 导航属性,而 StudentProfile 包含 Student 导航属性。

每个学生只有一个电子邮件和密码登录大学域。这些信息可以添加到 Student 表中,但出于安全原因,它被分离到另一个表中。

实体框架 - 生命周期

生命周期

上下文的生命周期从实例创建时开始,到实例被处置或垃圾回收时结束。

  • 上下文生命周期是在我们使用 ORM 时需要做出的一个非常重要的决定。

  • 上下文执行类似于实体缓存的操作,这意味着它保存对所有已加载实体的引用,这些引用可能会很快导致内存消耗增加,并且还可能导致内存泄漏。

  • 在下图中,您可以看到从应用程序到数据库通过上下文的数据工作流的上层,反之亦然。

Data Workflow

实体生命周期

实体生命周期描述了创建、添加、修改、删除等实体的过程。实体在其生命周期中具有许多状态。在查看如何检索实体状态之前,让我们先了解什么是实体状态。状态是类型System.Data.EntityState的枚举,它声明以下值:

  • Added:实体被标记为已添加。

  • Deleted:实体被标记为已删除。

  • Modified:实体已被修改。

  • Unchanged:实体尚未修改。

  • Detached:实体未被跟踪。

实体生命周期中的状态更改

有时实体的状态由上下文自动设置,但也可以由开发人员手动修改。即使所有从一种状态切换到另一种状态的组合都是可能的,但其中一些组合毫无意义。例如,将Added实体切换到Deleted状态,反之亦然。

让我们讨论一下不同的状态。

Unchanged 状态

  • 当实体处于 Unchanged 状态时,它绑定到上下文,但尚未修改。

  • 默认情况下,从数据库检索到的实体处于此状态。

  • 当实体附加到上下文(使用 Attach 方法)时,它也处于 Unchanged 状态。

  • 上下文无法跟踪其未引用的对象的更改,因此当它们附加时,它假设它们处于 Unchanged 状态。

Detached 状态

  • Detached 是新创建实体的默认状态,因为上下文无法跟踪代码中任何对象的创建。

  • 即使在上下文的 using 块内实例化实体,也适用此规则。

  • 即使在禁用跟踪时从数据库检索到的实体也处于 Detached 状态。

  • 当实体处于 Detached 状态时,它不会绑定到上下文,因此其状态不会被跟踪。

  • 它可以被处置、修改、与其他类组合使用,或者以您可能需要的任何其他方式使用。

  • 由于没有上下文跟踪它,因此它对 Entity Framework 没有任何意义。

Added 状态

  • 当实体处于 Added 状态时,您有几个选项。实际上,您只能将其从上下文中分离。

  • 当然,即使您修改了一些属性,状态仍然是 Added,因为将其移动到 Modified、Unchanged 或 Deleted 没有任何意义。

  • 它是一个新实体,并且与数据库中的行没有对应关系。

  • 这是处于这些状态之一的基本先决条件(但此规则不受上下文强制执行)。

Added State

Modified 状态

  • 当实体被修改时,这意味着它处于 Unchanged 状态,然后某些属性发生了更改。

  • 实体进入 Modified 状态后,可以移动到 Detached 或 Deleted 状态,但即使手动还原原始值,它也不能回滚到 Unchanged 状态。

  • 它甚至不能更改为 Added,除非您分离实体并将其添加到上下文,因为数据库中已存在具有此 ID 的行,并且在持久化它时会收到运行时异常。

Deleted 状态

  • 实体进入 Deleted 状态是因为它处于 Unchanged 或 Modified 状态,然后使用了 DeleteObject 方法。

  • 这是最严格的状态,因为将此状态更改为除 Detached 之外的任何其他值都没有意义。

using 语句,如果您希望上下文控制的所有资源在块结束时被处置。当您使用using语句时,编译器会自动创建一个 try/finally 块并在 finally 块中调用 dispose。

using (var context = new UniContext()) {

   var student = new Student {
      LastName = "Khan", 
      FirstMidName = "Ali", 
      EnrollmentDate = DateTime.Parse("2005-09-01")
   };

   context.Students.Add(student);
   context.SaveChanges();
}

在处理长期运行的上下文时,请考虑以下事项:

  • 随着您将更多对象及其引用加载到内存中,上下文的内存消耗可能会迅速增加。这可能会导致性能问题。

  • 在不再需要上下文时,请务必将其处置。

  • 如果异常导致上下文处于不可恢复的状态,则整个应用程序可能会终止。

  • 当数据查询和更新之间的时间间隔增加时,遇到与并发相关的问题的可能性也会增加。

  • 在处理 Web 应用程序时,请每个请求使用一个上下文实例。

  • 在处理 Windows Presentation Foundation (WPF) 或 Windows 窗体时,请每个窗体使用一个上下文实例。这使您可以使用上下文提供的更改跟踪功能。

经验法则

Web 应用程序

  • 现在,对于 Web 应用程序,每个请求使用一个上下文已成为一种常见且最佳的做法。

  • 在 Web 应用程序中,我们处理的是非常短的请求,但包含所有服务器事务,因此它是上下文生存的适当持续时间。

桌面应用程序

  • 对于桌面应用程序(如 Win Forms/WPF 等),每个窗体/对话框/页面使用一个上下文。

  • 由于我们不希望将上下文作为应用程序的单例,因此在从一个窗体移动到另一个窗体时,我们将对其进行处置。

  • 通过这种方式,我们将获得上下文的许多功能,并且不会受到长期运行上下文的负面影响。

Entity Framework - Code First 方法

实体框架提供三种创建实体模型的方法,每种方法都有其自身的优缺点。

  • 代码优先 (Code First)
  • 数据库优先 (Database First)
  • 模型优先 (Model First)

在本章中,我们将简要介绍代码优先方法。一些开发人员喜欢使用代码中的设计器,而另一些开发人员则宁愿只使用他们的代码。对于这些开发人员,实体框架提供了一种称为代码优先的建模工作流。

  • 代码优先建模工作流的目标数据库是不存在的,代码优先将创建它。

  • 如果数据库为空,它也可以使用,然后代码优先也将添加新表。

  • 代码优先允许您使用 C# 或 VB.Net 类定义模型。

  • 可以使用类和属性上的特性或使用流畅的 API 选择性地执行其他配置。

Code First Approach

为什么选择代码优先?

  • 代码优先实际上是由一组拼图块组成的。首先是您的领域类。

  • 领域类与实体框架无关。它们只是您的业务领域的项目。

  • 然后,实体框架有一个上下文来管理这些类与数据库之间的交互。

  • 上下文不特定于代码优先。它是实体框架的一个特性。

  • 代码优先添加了一个模型构建器,它检查上下文正在管理的类,然后使用一组规则或约定来确定这些类和关系如何描述模型,以及该模型应如何映射到您的数据库。

  • 所有这些都在运行时发生。您将永远看不到此模型,它仅存在于内存中。

  • 代码优先能够在需要时使用该模型创建数据库。

  • 如果模型发生更改,它还可以更新数据库,使用称为代码优先迁移的功能。

实体框架 - 模型优先方法

在本章中,让我们学习如何在设计器中使用称为模型优先的工作流创建实体数据模型。

  • 当您开始一个新项目并且数据库甚至不存在时,模型优先非常有用。

  • 模型存储在 EDMX 文件中,可以在实体框架设计器中查看和编辑。

  • 在模型优先中,您在实体框架设计器中定义模型,然后生成 SQL,它将创建与您的模型匹配的数据库模式,然后您执行 SQL 以在您的数据库中创建模式。

  • 您在应用程序中交互的类是从 EDMX 文件自动生成的。

以下是使用模型优先方法创建新控制台项目的简单示例。

步骤 1 - 打开 Visual Studio 并选择文件 → 新建 → 项目

Console Project

步骤 2 - 从左侧窗格中选择已安装 → 模板 → Visual C# → Windows,然后在中间窗格中选择控制台应用程序。

步骤 3 - 在名称字段中输入 EFModelFirstDemo。

步骤 4 - 要创建模型,首先右键单击解决方案资源管理器中的控制台项目,然后选择添加 → 新建项…

Model

将打开以下对话框。

Add New

步骤 5 - 从中间窗格中选择 ADO.NET 实体数据模型,并在名称字段中输入 ModelFirstDemoDB。

步骤 6 - 单击添加按钮,将启动实体数据模型向导对话框。

Model Wizard Dialog

步骤 7 - 选择空 EF 设计器模型,然后单击下一步按钮。实体框架设计器将打开一个空白模型。现在我们可以开始向模型添加实体、属性和关联。

步骤 8 - 右键单击设计图面并选择属性。在属性窗口中,将实体容器名称更改为 ModelFirstDemoDBContext。

Design Surface

步骤 9 - 右键单击设计图面并选择添加新建 → 实体…

Add New Entity

添加实体对话框将打开,如下面的图像所示。

Entity Dialog

步骤 10 - 输入 Student 作为实体名称,输入 Student Id 作为属性名称,然后单击确定。

Student

步骤 11 - 右键单击设计图面上的新实体,然后选择添加新建 → 标量属性,输入 Name 作为属性的名称。

New Entity

步骤 12 - 输入 FirstName,然后添加另外两个标量属性,例如 LastName 和 EnrollmentDate。

Scalar Properties

步骤 13 - 通过遵循上述所有步骤,添加另外两个实体 Course 和 Enrollment,并添加一些标量属性,如下面的步骤所示。

Visual Designer

步骤 14 - 我们在 Visual Designer 中有三个实体,让我们在它们之间添加一些关联或关系。

步骤 15 - 右键单击设计图面并选择添加新建 → 关联…

Add Association

步骤 16 - 使关系的一端指向 Student,其多重性为 1,另一端指向 Enrollment,其多重性为多。

Multiplicity Of One

步骤 17 - 这意味着一个 Student 有多个 Enrollments,而 Enrollment 属于一个 Student。

步骤 18 - 确保选中“向‘Post’实体添加外键属性”复选框,然后单击确定。

步骤 19 - 同样,在 Course 和 Enrollment 之间添加另一个关联。

Course And Enrollment

步骤 20 - 在实体之间添加关联后,您的数据模型将如下面的屏幕所示。

Data Model

现在我们有一个简单的模型,可以从中生成数据库并用于读取和写入数据。让我们继续生成数据库。

步骤 1 - 右键单击设计图面并选择从模型生成数据库…

Database From Model

步骤 2 - 您可以选择现有数据库或通过单击新建连接…来创建新连接。

Generate Database

步骤 3 - 要创建新数据库,请单击新建连接…

Connection Properties

步骤 4 - 输入服务器名称和数据库名称。

Server And Database

步骤 5 - 单击下一步。

Server And Database 1

步骤 6 - 单击完成。这将在项目中添加 *.edmx.sql 文件。您可以通过打开 .sql 文件,然后右键单击并选择执行来在 Visual Studio 中执行 DDL 脚本。

DDL Scripts

步骤 7 - 将显示以下对话框以连接到数据库。

Connect To Database

步骤 8 - 成功执行后,您将看到以下消息。

Successful Execution

步骤 9 - 转到服务器资源管理器,您将看到数据库已创建,其中包含三个指定的表。

Database Created

接下来,我们需要交换我们的模型以生成使用 DbContext API 的代码。

步骤 1 - 右键单击 EF 设计器中模型的空白处,然后选择添加代码生成项…

EF Designer

您将看到以下添加新项对话框打开。

Dialog Open

步骤 2 - 在中间窗格中选择 EF 6.x DbContext 生成器,并在名称字段中输入 ModelFirstDemoModel。

步骤 3 - 您将在解决方案资源管理器中看到生成了 ModelFirstDemoModel.Context.tt 和 ModelFirstDemoModel.tt 模板。

Template Generated

ModelFirstDemoModel.Context 生成 DbCcontext 和您可以返回并用于查询的对象集,例如上下文、Students 和 Courses 等。

另一个模板处理所有类型 Student、Courses 等。以下是自动从实体模型生成的 Student 类。

CSharp Code

以下是 C# 代码,其中一些数据已输入并从数据库中检索。

using System;
using System.Linq;

namespace EFModelFirstDemo {

   class Program {

      static void Main(string[] args) {

         using (var db = new ModelFirstDemoDBContext()) {

            // Create and save a new Student

            Console.Write("Enter a name for a new Student: ");
            var firstName = Console.ReadLine();

            var student = new Student {
               StudentID = 1,
               FirstName = firstName
            };
				
            db.Students.Add(student);
            db.SaveChanges();
				
            var query = from b in db.Students
               orderby b.FirstName select b;

            Console.WriteLine("All student in the database:");

            foreach (var item in query) {
               Console.WriteLine(item.FirstName);
            }

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
         }
      }
   }
}

执行上述代码时,您将收到以下输出 -

Enter a name for a new Student:
Ali Khan
All student in the database:
Ali Khan
Press any key to exit...

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

实体框架 - 数据库优先方法

在本章中,让我们学习如何使用数据库优先方法创建实体数据模型。

  • 数据库优先方法为实体数据模型提供了代码优先和模型优先方法的替代方案。它从项目中的数据库创建模型代码(类、属性、DbContext 等),这些类成为数据库和控制器之间的链接。

  • 数据库优先方法从现有数据库创建实体框架。我们以与在模型优先方法中相同的方式使用所有其他功能,例如模型/数据库同步和代码生成。

让我们举一个简单的例子。我们已经有一个数据库,其中包含 3 个表,如下面的图像所示。

New Console Project

步骤 1 - 让我们创建一个名为 DatabaseFirstDemo 的新控制台项目。

步骤 2 - 要创建模型,首先右键单击解决方案资源管理器中的控制台项目,然后选择添加 → 新建项…

Create Model

步骤 3 - 从中间窗格中选择 ADO.NET 实体数据模型,并在名称字段中输入 DatabaseFirstModel。

步骤 4 - 单击添加按钮,将启动实体数据模型向导对话框。

Model Contents

步骤 5 - 选择来自数据库的 EF 设计器,然后单击下一步按钮。

Entity Model Wizard

步骤 6 - 选择现有数据库,然后单击下一步。

Existing Database

步骤 7 - 选择 Entity Framework 6.x,然后单击下一步。

Entity Framework

步骤 8 - 选择要包含的所有表、视图和存储过程,然后单击完成。

您将看到从数据库生成的实体模型和 POCO 类。

POCO Classes

现在让我们通过在 program.cs 文件中编写以下代码来检索数据库中的所有学生。

using System;
using System.Linq;

namespace DatabaseFirstDemo {

   class Program {

      static void Main(string[] args) {

         using (var db = new UniContextEntities()) {

            var query = from b in db.Students
               orderby b.FirstMidName select b;

            Console.WriteLine("All All student in the database:");

            foreach (var item in query) {
               Console.WriteLine(item.FirstMidName +" "+ item.LastName);
            }

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
         }
      }
   }
}

执行上述程序时,您将收到以下输出 -

All student in the database:
Ali Khan
Arturo   finand
Bill Gates
Carson Alexander
Gytis Barzdukas
Laura Norman
Meredith Alonso
Nino Olivetto
Peggy Justice
Yan Li
Press any key to exit...

执行上述程序时,您将看到以前在数据库中输入的所有学生的姓名。

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

实体框架 - 开发方法

在本章中,让我们重点关注使用设计器或数据库优先或仅使用代码优先构建模型。以下是一些准则,将帮助您决定选择哪种建模工作流。

  • 我们已经看到了代码优先建模、数据库优先建模和模型优先建模工作流的示例。

  • 数据库优先和模型优先工作流使用设计器,但一个从数据库开始创建模型,另一个从模型开始创建数据库。

Designer Model
  • 对于那些不想使用 Visual Designer 加上代码生成的开发人员,实体框架提供了一种完全不同的工作流,称为代码优先。

  • 代码优先的典型工作流非常适合全新的应用程序,在这种应用程序中,您甚至没有数据库。您定义自己的类和代码,然后让代码优先确定数据库的外观。

  • 也可以从数据库开始代码优先,这使得代码优先有点矛盾。但是,有一个工具可以帮助您将数据库反向工程成类,这是一种开始编码的好方法。

鉴于这些选项,让我们看一下决策树。

  • 如果您更喜欢在生成的代码中使用 Visual Designer,那么您需要选择其中一个涉及 EF Designer 的工作流。如果您的数据库已存在,那么数据库优先是您的路径。

  • 如果您想在一个没有数据库的全新项目上使用 Visual Designer,那么您需要使用模型优先。

  • 如果您只想使用代码而不使用设计器,那么代码优先可能适合您,以及使用将数据库反向工程成类的工具的选项。

  • 如果您有现有的类,那么最好的选择是将它们与代码优先一起使用。

实体框架 - 数据库操作

在前面的章节中,您学习了定义实体数据模型的三种不同方法。

  • 其中两种,数据库优先和模型优先,依赖于实体框架设计器以及代码生成。

  • 第三种,代码优先,允许您跳过视觉设计器,只需编写自己的代码。

  • 无论您选择哪种路径,最终都会得到领域类和一个或多个实体框架 DbContext 类,这些类允许您检索和持久化与这些类相关的

应用程序中的 DbContext API 用作类和数据库之间的桥梁。DbContext 是实体框架中最重要的类之一。

  • 它能够表达和执行查询。

  • 它获取数据库中的查询结果,并将它们转换为模型类的实例。

  • 它可以跟踪实体的更改,包括添加和删除,然后触发创建插入、更新和删除语句,这些语句根据需要发送到数据库。

本章将对以下域广告上下文类执行不同的操作。这是我们在“数据库优先方法”一章中创建的相同示例。

上下文类实现

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Core.Objects;
using System.Linq;

namespace DatabaseFirstDemo {

   public partial class UniContextEntities : DbContext {

      public UniContextEntities(): base("name = UniContextEntities") {}

      protected override void OnModelCreating(DbModelBuilder modelBuilder) {
         throw new UnintentionalCodeFirstException();
      }

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

域类实现

课程类

namespace DatabaseFirstDemo {

   using System;
   using System.Collections.Generic;
	
   public partial class Course {

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2214:DoNotCallOverridableMethodsInConstructors")]

      public Course() {
         this.Enrollments = new HashSet<Enrollment>();
      }
	
      public int CourseID { get; set; }
      public string Title { get; set; }
      public int Credits { get; set; }
	
      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2227:CollectionPropertiesShouldBeReadOnly")]
			
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }
}

学生类

namespace DatabaseFirstDemo {

   using System;
   using System.Collections.Generic; 

   public partial class Student {

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2214:DoNotCallOverridableMethodsInConstructors")]

      public Student() {
         this.Enrollments = new HashSet<Enrollment>();
      }

      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
      public System.DateTime EnrollmentDate { get; set; }

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2227:CollectionPropertiesShouldBeReadOnly")]
			
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }
}

注册类

namespace DatabaseFirstDemo {

   using System;
   using System.Collections.Generic; 

   public partial class Enrollment {

      public int EnrollmentID { get; set; }
      public int CourseID { get; set; }
      public int StudentID { get; set; }
      public Nullable<int> Grade { get; set; }
		
      public virtual Course Course { get; set; }
      public virtual Student Student { get; set; }
   }
}

创建操作

使用 Entity Framework 添加新对象就像构造对象的新实例并在 DbSet 上使用 Add 方法注册它一样简单。以下代码允许您向数据库添加新学生。

class Program {

   static void Main(string[] args) {

      var newStudent = new Student();

      //set student name

      newStudent.FirstMidName = "Bill";
      newStudent.LastName = "Gates";
      newStudent.EnrollmentDate = DateTime.Parse("2015-10-21");
      newStudent.ID = 100;

      //create DBContext object

      using (var dbCtx = new UniContextEntities()) {

         //Add Student object into Students DBset
         dbCtx.Students.Add(newStudent);

         // call SaveChanges method to save student into database
         dbCtx.SaveChanges();
      }
   }
}

更新操作

更改现有对象就像更新分配给要更改的属性的值并调用 SaveChanges 一样简单。例如,以下代码用于将 Ali 的姓氏从 Khan 更改为 Aslam。

using (var context = new UniContextEntities()) {

   var student = (from d in context.Students where d.FirstMidName == "Ali" select d).Single();
   student.LastName = "Aslam";
   context.SaveChanges();
}

删除操作

要使用 Entity Framework 删除实体,请在 DbSet 上使用 Remove 方法。Remove 对现有实体和新添加的实体都有效。对已添加但尚未保存到数据库的实体调用 Remove 将取消该实体的添加。该实体将从更改跟踪器中删除,并且不再受 DbContext 跟踪。对正在进行更改跟踪的现有实体调用 Remove 将在下次调用 SaveChanges 时注册该实体以进行删除。以下代码示例演示了从数据库中删除名为 Ali 的学生的代码。

using (var context = new UniContextEntities()) {
   var bay = (from d in context.Students where d.FirstMidName == "Ali" select d).Single();
   context.Students.Remove(bay);
   context.SaveChanges();
}

读取操作

从数据库读取现有数据非常简单。以下是代码,其中检索了 Student 表中的所有数据,然后将显示一个程序,其中包含按字母顺序排列的学生姓名。

using (var db = new UniContextEntities()) {

   var query = from b in db.Students orderby b.FirstMidName select b;
   Console.WriteLine("All All student in the database:");

   foreach (var item in query) {
      Console.WriteLine(item.FirstMidName +" "+ item.LastName);
   }

   Console.WriteLine("Press any key to exit...");
   Console.ReadKey();
}

实体框架 - 并发

任何数据访问开发人员在回答有关数据并发性的问题时都会遇到困难,“如果多个人同时编辑相同的数据会发生什么?”

  • 我们当中比较幸运的人会处理这样的业务规则:“没问题,最后一个人获胜”。

  • 在这种情况下,并发不是问题。更有可能的是,情况并非如此简单,并且没有万能的解决方案可以立即解决所有场景。

  • 默认情况下,Entity Framework 将采用“最后一个人获胜”的路径,这意味着即使其他人更新了数据检索时间和数据保存时间之间的数据,也会应用最新的更新。

让我们举个例子来更好地理解它。以下示例在 Course 表中添加了一个新的列 VersionNo。

Course Table

转到设计器,右键单击设计器窗口,然后选择“从数据库更新模型…”。

Designer

您将看到在 Course 实体中添加了另一列。

Course Entity

右键单击新创建的列 VersionNo,然后选择“属性”,并将“并发模式”更改为“固定”,如下面的图像所示。

New Created Column

将 Course.VersionNo 的 ConcurrencyMode 设置为 Fixed 后,每次更新 Course 时,Update 命令都会使用其 EntityKey 和 VersionNo 属性查找 Course。

让我们来看一个简单的场景。两个用户同时检索相同的课程,用户 1 将该课程的标题更改为数学并在用户 2 之前保存更改。稍后,当用户 2 更改该课程的标题(该标题是在用户 1 保存其更改之前检索到的)时,在这种情况下,用户 2 将收到并发异常“User2:发生了乐观并发异常”

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;

namespace DatabaseFirstDemo {

   class Program {

      static void Main(string[] args) {

         Course c1 = null;
         Course c2 = null;

         //User 1 gets Course

         using (var context = new UniContextEntities()) {
            context.Configuration.ProxyCreationEnabled = false;
            c1 = context.Courses.Where(s ⇒ s.CourseID == 1).Single();
         }

         //User 2 also get the same Course

         using (var context = new UniContextEntities()) {
            context.Configuration.ProxyCreationEnabled = false;
            c2 = context.Courses.Where(s ⇒ s.CourseID == 1).Single();
         }

         //User 1 updates Course Title
         c1.Title = "Edited from user1";

         //User 2 updates Course Title
         c2.Title = "Edited from user2";

         //User 1 saves changes first

         using (var context = new UniContextEntities()) {

            try {
               context.Entry(c1).State = EntityState.Modified;
               context.SaveChanges();
            } catch (DbUpdateConcurrencyException ex) {
               Console.WriteLine("User1: Optimistic Concurrency exception occurred");
            }
         }

         //User 2 saves changes after User 1.
         //User 2 will get concurrency exection
         //because CreateOrModifiedDate is different in the database

         using (var context = new UniContextEntities()) {

            try {
               context.Entry(c2).State = EntityState.Modified;
               context.SaveChanges();
            } catch (DbUpdateConcurrencyException ex) {
               Console.WriteLine("User2: Optimistic Concurrency exception occurred");
            }
         }
      }
   }
}

实体框架 - 事务

在所有版本的 Entity Framework 中,无论何时执行SaveChanges()来插入、更新或删除数据库,框架都会将该操作包装在事务中。当您调用 SaveChanges 时,上下文会自动启动事务,并根据持久性是否成功提交或回滚事务。

  • 所有这些对您来说都是透明的,您无需处理它。

  • 此事务仅持续足够长的时间来执行操作,然后完成。

  • 当您执行另一个此类操作时,将启动一个新事务。

Entity Framework 6 提供以下功能:

Database.BeginTransaction()

  • 这是一种在现有 DbContext 中启动和完成用户事务的简单易用的方法。

  • 它允许将多个操作组合在同一事务中,因此要么全部提交,要么全部作为单个操作回滚。

  • 它还允许用户更轻松地指定事务的隔离级别。

Database.UseTransaction()

  • 它允许 DbContext 使用在 Entity Framework 外部启动的事务。

让我们来看以下示例,其中在单个事务中执行多个操作。代码如下:

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         using (var dbContextTransaction = context.Database.BeginTransaction()) {

            try {

               Student student = new Student() {
                  ID = 200, 
                  FirstMidName = "Ali", 
                  LastName = "Khan", 
                  EnrollmentDate = DateTime.Parse("2015-12-1")
               };

               context.Students.Add(student);

               context.Database.ExecuteSqlCommand(@"UPDATE Course SET Title = 
                  'Calculus'" + "WHERE CourseID = 1045");

               var query = context.Courses.Where(c ⇒ c.CourseID == 1045);

               foreach (var item in query) {
                  Console.WriteLine(item.CourseID.ToString()
                     + " " + item.Title + " " + item.Credits);
               }

               context.SaveChanges();
               var query1 = context.Students.Where(s ⇒ s.ID == 200);

               foreach (var item in query1) {
                  Console.WriteLine(item.ID.ToString() 
                     + " " + item.FirstMidName + " " + item.LastName);
               }

               dbContextTransaction.Commit();
            } catch (Exception) {
               dbContextTransaction.Rollback();
            }

         }
      }
   }
}
  • 启动事务需要底层存储连接处于打开状态。

  • 因此,调用 Database.BeginTransaction() 将打开连接(如果尚未打开)。

  • 如果 DbContextTransaction 打开了连接,则在调用 Dispose() 时将关闭它。

实体框架 - 视图

视图是一个包含通过预定义查询获得的数据的对象。视图是一个虚拟对象或表,其结果集派生自查询。它与真实表非常相似,因为它包含数据列和行。以下是视图的一些典型用途:

  • 筛选底层表的数据
  • 出于安全目的筛选数据
  • 集中分布在多个服务器上的数据
  • 创建可重用的数据集

视图的使用方式与表类似。要将视图用作实体,首先需要将数据库视图添加到 EDM。将视图添加到模型后,您可以像处理普通实体一样处理它,除了创建、更新和删除操作。

让我们看看如何从数据库中将视图添加到模型中。

步骤 1 - 创建一个新的控制台应用程序项目。

Application Project

步骤 2 - 在解决方案资源管理器中右键单击项目,然后选择“添加”→“新建项”。

Project Solution Explorer

步骤 3 - 从中间窗格中选择 ADO.NET 实体数据模型,并在“名称”字段中输入 ViewModel。

步骤 4 - 单击添加按钮,将启动实体数据模型向导对话框。

Add Button

步骤 5 - 选择来自数据库的 EF 设计器,然后单击下一步按钮。

Entity Model Wizard

步骤 6 - 选择现有数据库,然后单击下一步。

Existing Database

步骤 7 - 选择 Entity Framework 6.x,然后单击下一步。

Entity Framework Next

步骤 8 - 从数据库中选择表和视图,然后单击“完成”。

Table View

您可以在设计器窗口中看到已创建视图,并且可以在程序中将其用作实体。

在解决方案资源管理器中,您可以看到 MyView 类也是从数据库生成的。

让我们来看一个从视图中检索所有数据的示例。以下是代码:

class Program {

   static void Main(string[] args) {

      using (var db = new UniContextEntities()) {

         var query = from b in db.MyViews
            orderby b.FirstMidName select b;

         Console.WriteLine("All student in the database:");

         foreach (var item in query) {
            Console.WriteLine(item.FirstMidName + " " + item.LastName);
         }

         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();
      }
   }
}

执行上述代码时,您将收到以下输出 -

All student in the database:
Ali Khan
Arturo   finand
Bill Gates
Carson Alexander
Gytis Barzdukas
Laura Norman
Meredith Alonso
Nino Olivetto
Peggy Justice
Yan Li
Press any key to exit...

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

实体框架 - 索引

索引是基于表和视图的磁盘上数据结构。在大多数情况下,索引使数据检索更快、更高效。但是,如果表或视图上的索引过多,可能会对其他操作(如插入或更新)的性能产生不利影响。

  • 索引是实体框架的新功能,您可以通过减少从数据库查询数据所需的时间来提高 Code First 应用程序的性能。

  • 您可以使用Index属性向数据库添加索引,并覆盖默认的UniqueClustered设置以获得最适合您方案的索引。

让我们来看以下代码,其中在 Course 类中为 CourseID 添加了 Index 属性。

public partial class Course {

   public Course() {
      this.Enrollments = new HashSet<Enrollment>();
   }

   [Index]
   public int CourseID { get; set; }
   public string Title { get; set; }
	
   public int Credits { get; set; }
   public byte[] VersionNo { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

上面创建的键是非唯一的、非聚集的。有可用的重载来覆盖这些默认值:

  • 要将索引设为聚集索引,需要指定 IsClustered = true

  • 类似地,您也可以通过指定 IsUnique = true 将索引设为唯一索引

让我们来看以下 C# 代码,其中索引是聚集的且唯一的。

public partial class Course {

   public Course() {
      this.Enrollments = new HashSet<Enrollment>();
   }

   [Index(IsClustered = true, IsUnique = true)]
   public int CourseID { get; set; }
   public string Title { get; set; }
	
   public int Credits { get; set; }
   public byte[] VersionNo { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

Index 属性可用于在数据库中创建唯一索引。但是,这并不意味着 EF 在处理关系等时能够推断出该列的唯一性。此功能通常称为对“唯一约束”的支持。

Entity Framework - 存储过程

Entity Framework 允许您在实体数据模型中使用存储过程,而不是或结合其自动命令生成。

  • 您可以使用存储过程对数据库表执行预定义的逻辑,并且许多组织都制定了要求使用这些存储过程的策略。

  • 它还可以指定 EF 应该使用您的存储过程来插入、更新或删除实体。

  • 尽管动态生成的命令安全、高效,并且通常与您自己编写的命令一样好或更好,但在许多情况下,存储过程已经存在,并且您的公司惯例可能会限制对表的直接使用。

  • 或者,您可能只想对存储上执行的操作进行显式控制,并更愿意创建存储过程。

以下示例从“文件”→“新建”→“项目”创建新项目。

Procedure New Project

步骤 1 - 从中间窗格中选择“控制台应用程序”,并在“名称”字段中输入 StoredProceduresDemo。

步骤 2 - 在服务器资源管理器中右键单击您的数据库。

步骤 3 - 选择“新建查询”,并在 T-SQL 编辑器中输入以下代码以在数据库中添加新表。

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = 
   OBJECT_ID(N'[dbo].[StudentGrade]') AND type in (N'U'))

BEGIN

   CREATE TABLE [dbo].[StudentGrade](

      [EnrollmentID] [int] IDENTITY(1,1) NOT NULL,
      [CourseID] [int] NOT NULL,
      [StudentID] [int] NOT NULL,
      [Grade] [decimal](3, 2) NULL,

      CONSTRAINT [PK_StudentGrade] PRIMARY KEY CLUSTERED (
         [EnrollmentID] ASC
      )

      WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]

   ) ON [PRIMARY]

END
GO

步骤 4 - 右键单击编辑器,然后选择“执行”。

Editor

步骤 5 - 右键单击您的数据库,然后单击“刷新”。您将在数据库中看到新添加的表。

步骤 6 - 在服务器资源管理器中,再次右键单击您的数据库。

Server Database

步骤 7 - 选择“新建查询”,并在 T-SQL 编辑器中输入以下代码以在数据库中添加存储过程,该过程将返回学生成绩。

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = 
   OBJECT_ID(N'[dbo].[GetStudentGrades]') AND type in (N'P', N'PC'))

BEGIN

   EXEC dbo.sp_executesql @statement = N'
   CREATE PROCEDURE [dbo].[GetStudentGrades]
   @StudentID int
   AS
   SELECT EnrollmentID, Grade, CourseID, StudentID FROM dbo.StudentGrade 
   WHERE StudentID = @StudentID
   '
END
GO

步骤 8 - 右键单击编辑器,然后选择“执行”。

Execute

步骤 9 - 右键单击您的数据库,然后单击“刷新”。您将看到数据库中创建了一个存储过程。

Store Procedure Created

步骤 10 - 在解决方案资源管理器中右键单击项目名称,然后选择“添加”→“新建项”。

步骤 11 - 然后在“模板”窗格中选择 ADO.NET 实体数据模型。

Template Pane

步骤 12 - 输入 SPModel 作为名称,然后单击“添加”。

步骤 13 - 在“选择模型内容”对话框中,选择“从数据库中的 EF 设计器”,然后单击“下一步”。

Model Contents

步骤 14 - 选择您的数据库,然后单击“下一步”。

Database 1

步骤 15 - 在“选择数据库对象”对话框中,单击“表”、“视图”。

Database Objects

步骤 16 - 选择“存储过程和函数”节点下的 GetStudentGradesForCourse 函数,然后单击“完成”。

步骤 17 - 选择“视图”→“其他窗口”→“实体数据模型浏览器”,右键单击“函数导入”下的 GetStudentGrades,然后选择“编辑”。

Entity Browser

它将生成以下对话框。

Entity Browser Dialog

步骤 18 - 单击“实体”单选按钮,然后从组合框中选择 StudentGrade 作为此存储过程的返回类型,然后单击“确定”。

让我们来看以下 C# 代码,其中将通过将学生 ID 作为参数传递给 GetStudentGrades 存储过程来检索所有成绩。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         int studentID = 22;
         var studentGrades = context.GetStudentGrades(studentID);

         foreach (var student in studentGrades) {
            Console.WriteLine("Course ID: {0}, Title: {1}, Grade: {2} ", 
               student.CourseID, student.Course.Title, student.Grade);
         }

         Console.ReadKey();

      }
   }
}

编译并执行上述代码后,您将收到以下输出:

Course ID: 4022, Title: Microeconomics, Grade: 3.00
Course ID: 4041, Title: Macroeconomics, Grade: 3.50

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

Entity Framework - 断开连接的实体

在本节中,让我们看看如何更改不受上下文跟踪的实体。不受上下文跟踪的实体被称为“断开连接”实体。

  • 对于大多数单层应用程序,其中用户界面和数据库访问层在同一个应用程序进程中运行,您可能只会对受上下文跟踪的实体执行操作。

  • 对断开连接实体的操作在 N 层应用程序中更为常见。

  • N 层应用程序涉及在服务器上获取一些数据并将其通过网络返回到客户端计算机。

  • 然后,客户端应用程序操作这些数据,然后再将其返回到服务器以进行持久化。

以下是需要对断开连接的实体图甚至单个断开连接的实体执行的两个步骤。

  • 将实体附加到新的上下文实例并使上下文了解这些实体。

  • 手动为这些实体设置适当的 EntityStates。

Change To Entity

让我们看一下以下代码,其中 Student 实体添加了两个 Enrollment 实体。

class Program {

   static void Main(string[] args) {

      var student = new Student {

         ID = 1001,
         FirstMidName = "Wasim",
         LastName = "Akram", 

         EnrollmentDate = DateTime.Parse("2015-10-10"), 
            Enrollments = new List<Enrollment> {

               new Enrollment{EnrollmentID = 2001,CourseID = 4022, StudentID = 1001 },
               new Enrollment{EnrollmentID = 2002,CourseID = 4025, StudentID = 1001 },
         }
      };

      using (var context = new UniContextEntities()) {

         context.Students.Add(student);
         Console.WriteLine("New Student ({0} {1}): {2}", 
            student.FirstMidName, student.LastName, context.Entry(student).State);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0} State: {1}", 
               enrollment.EnrollmentID, context.Entry(enrollment).State);
         }

         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();
      }
   } 
}
  • 该代码构造一个新的 Student 实例,该实例还在其 Enrollments 属性中引用了两个新的 Enrollment 实例。

  • 然后,使用 Add 方法将新的 Student 添加到上下文中。

  • 添加 Student 后,代码使用 DbContext.Entry 方法访问 Entity Framework 关于新 Student 的更改跟踪信息。

  • 从这些更改跟踪信息中,State 属性用于输出实体的当前状态。

  • 然后对从新 Student 引用的每个新创建的 Enrollment 重复此过程。如果运行应用程序,您将收到以下输出 -

New Student   (Wasim  Akram): Added
Enrollment ID: 2001 State: Added
Enrollment ID: 2002 State: Added
Press any key to exit...

虽然 DbSet.Add 用于告诉 Entity Framework 关于新实体,但 DbSet.Attach 用于告诉 Entity Framework 关于现有实体。Attach 方法将实体标记为 Unchanged 状态。

让我们看一下以下 C# 代码,其中一个断开连接的实体附加到 DbContext。

class Program {

   static void Main(string[] args) {

      var student = new Student {

         ID = 1001,
         FirstMidName = "Wasim",
         LastName = "Akram",
         EnrollmentDate = DateTime.Parse("2015-10-10"), 

         Enrollments = new List<Enrollment> {
            new Enrollment { EnrollmentID = 2001, CourseID = 4022, StudentID = 1001 },
            new Enrollment { EnrollmentID = 2002, CourseID = 4025, StudentID = 1001 },
         }
			
      };

      using (var context = new UniContextEntities()) {

         context.Students.Attach(student);
         Console.WriteLine("New Student ({0} {1}): {2}", 
            student.FirstMidName, student.LastName, context.Entry(student).State);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0} State: {1}", enrollment.EnrollmentID, 
               context.Entry(enrollment).State);
         }

         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();
      }
   }
}

当使用 Attach() 方法执行上述代码时,您将收到以下输出。

New Student   (Wasim  Akram): Unchanged
Enrollment ID: 2001 State: Unchanged
Enrollment ID: 2002 State: Unchanged
Press any key to exit...

Entity Framework - 表值函数

在本节中,让我们学习如何使用 Entity Framework 设计器映射表值函数 (TVF) 以及如何从 LINQ 查询中调用 TVF。

  • TVF 目前仅在 Database First 工作流中受支持。

  • 它最早在 Entity Framework 版本 5 中引入。

  • 要使用 TVF,您必须面向 .NET Framework 4.5 或更高版本。

  • 它与存储过程非常相似,但有一个关键区别,即 TVF 的结果是可组合的。这意味着 TVF 的结果可以在 LINQ 查询中使用,而存储过程的结果则不能。

让我们看一下以下创建新项目的示例,方法是选择“文件”→“新建”→“项目”。

Create Project

步骤 1 - 从中间窗格中选择控制台应用程序,并在名称字段中输入 TableValuedFunctionDemo。

步骤 2 - 在服务器资源管理器中右键单击您的数据库。

Explorer Database

步骤 3 - 选择“新建查询”,并在 T-SQL 编辑器中输入以下代码以在数据库中添加新表。

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id =
   OBJECT_ID(N'[dbo].[StudentGrade]') AND type in (N'U'))

BEGIN

   CREATE TABLE [dbo].[StudentGrade](

      [EnrollmentID] [int] IDENTITY(1,1) NOT NULL,
      [CourseID] [int] NOT NULL,
      [StudentID] [int] NOT NULL,
      [Grade] [decimal](3, 2) NULL,

      CONSTRAINT [PK_StudentGrade] PRIMARY KEY CLUSTERED ([EnrollmentID] ASC)

      WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]

   ) ON [PRIMARY]

END
GO

步骤 4 - 右键单击编辑器,然后选择“执行”。

Select Execute

步骤 5 - 右键单击您的数据库,然后单击“刷新”。您将在数据库中看到新添加的表。

Added Table

步骤 6 - 现在创建一个函数,该函数将返回课程的学生成绩。在 T-SQL 编辑器中输入以下代码。

CREATE FUNCTION [dbo].[GetStudentGradesForCourse]

(@CourseID INT)

RETURNS TABLE

RETURN
   SELECT [EnrollmentID],
      [CourseID],
      [StudentID],
      [Grade]
   FROM   [dbo].[StudentGrade]
   WHERE  CourseID = @CourseID 

步骤 7 - 右键单击编辑器并选择“执行”。

Editor Select

现在您可以看到函数已创建。

Function Created

步骤 8 - 右键单击解决方案资源管理器中的项目名称,然后选择“添加”→“新建项”。

步骤 9 - 然后在“模板”窗格中选择 ADO.NET 实体数据模型。

Entity Template Pane

步骤 10 - 输入 TVFModel 作为名称,然后单击“添加”。

步骤 11 - 在“选择模型内容”对话框中,选择“从数据库中选择 EF 设计器”,然后单击“下一步”。

Content Dialog Box

步骤 12 - 选择您的数据库并单击“下一步”。

Select Database

步骤 13 - 在“选择数据库对象”对话框中,选择表、视图。

Object Dialog Box

步骤 14 - 选择位于“存储过程和函数”节点下的 GetStudentGradesForCourse 函数,然后单击“完成”。

步骤 15 - 选择“视图”→“其他窗口”→“实体数据模型浏览器”,右键单击“函数导入”下的 GetStudentGradesForCourse,然后选择“编辑”。

Select View

您将看到以下对话框。

Dialog

步骤 16 - 单击“实体”单选按钮,从组合框中选择 Enrollment 作为此函数的返回类型,然后单击“确定”。

让我们看一下以下 C# 代码,其中将检索数据库中注册课程 ID 为 4022 的所有学生的成绩。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         var CourseID = 4022;

         // Return all the best students in the Microeconomics class.
         var students = context.GetStudentGradesForCourse(CourseID);

         foreach (var result in students) {
            Console.WriteLine("Student ID:  {0}, Grade: {1}",
               result.StudentID, result.Grade);
         }

         Console.ReadKey();
      }
   }
}

编译并执行上述代码后,您将收到以下输出:

Student ID: 1, Grade: 2
Student ID: 4, Grade: 4
Student ID: 9, Grade: 3.5

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

实体框架 - 本机SQL

在 Entity Framework 中,您可以使用 LINQ 使用实体类进行查询。您还可以使用 DbCOntext 直接对数据库运行原始 SQL 查询。这些技术可以同样应用于使用 Code First 和 EF 设计器创建的模型。

现有实体上的 SQL 查询

DbSet 上的 SqlQuery 方法允许编写一个原始 SQL 查询,该查询将返回实体实例。返回的对象将由上下文跟踪,就像它们是由 LINQ 查询返回的一样。例如 -

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         var students = context.Students.SqlQuery("SELECT * FROM dbo.Student").ToList();

         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine("ID: {0}, Name: {1}, \tEnrollment Date {2} ",
               student.ID, name, student.EnrollmentDate.ToString());
         }

         Console.ReadKey();
      }
   }
}

以上代码将检索数据库中的所有学生。

非实体类型的 SQL 查询

可以使用 Database 类上的 SqlQuery 方法创建返回任何类型实例(包括原始类型)的 SQL 查询。例如 -

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         var studentNames = context.Database.SqlQuery
            <string>("SELECT FirstMidName FROM dbo.Student").ToList();

         foreach (var student in studentNames) {
            Console.WriteLine("Name: {0}", student);
         }

         Console.ReadKey();
      }
   }
}

数据库的 SQL 命令

ExecuteSqlCommnad 方法用于向数据库发送非查询命令,例如 Insert、Update 或 Delete 命令。让我们看一下以下代码,其中学生的姓名字段在 ID=1 时被更新。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         //Update command

         int noOfRowUpdated = context.Database.ExecuteSqlCommand("Update 
            student set FirstMidName = 'Ali' where ID = 1");

         context.SaveChanges();

         var student = context.Students.SqlQuery("SELECT * FROM
            dbo.Student where ID = 1").Single();

         string name = student.FirstMidName + " " + student.LastName;

         Console.WriteLine("ID: {0}, Name: {1}, \tEnrollment Date {2} ", 
            student.ID, name, student.EnrollmentDate.ToString());

         Console.ReadKey();
      }
   }
}

以上代码将检索数据库中所有学生的姓名字段。

实体框架 - 枚举支持

在 Entity Framework 中,此功能允许您在域类上定义一个属性,该属性是枚举类型,并将其映射到整数类型的数据库列。然后,Entity Framework 会在查询和保存数据时将数据库值转换为相关的枚举并从相关的枚举转换回来。

  • 当使用具有固定数量响应的属性时,枚举类型具有各种好处。

  • 当您使用枚举时,应用程序的安全性和可靠性都会提高。

  • 枚举使用户犯错的可能性大大降低,并且诸如注入攻击之类的问题根本不存在。

  • 在 Entity Framework 中,枚举可以具有以下基础类型 -

    • 字节
    • Int16
    • Int32
    • Int64
    • SByte
  • 枚举元素的默认基础类型为 int。

  • 默认情况下,第一个枚举器的值为 0,每个后续枚举器的值增加 1。

让我们看一下以下示例,其中我们将创建设计器中的实体,然后添加一些属性。

步骤 1 - 从“文件”→“新建”→“项目”菜单选项创建新项目。

步骤 2 - 在左侧窗格中,选择控制台应用程序。

Creating Entity

步骤 3 - 输入 EFEnumDemo 作为项目名称,然后单击“确定”。

步骤 4 - 右键单击解决方案资源管理器中的项目名称,然后选择“添加”→“新建项”菜单选项。

步骤 5 - 在“模板”窗格中选择 ADO.NET 实体数据模型。

Data Model Template

步骤 6 - 输入 EFEnumModel.edmx 作为文件名,然后单击“添加”。

步骤 7 - 在“实体数据模型向导”页面上,选择“空 EF 设计器模型”。

Model Wizard Page

步骤 8 - 单击“完成”。

步骤 9 - 然后右键单击设计器窗口并选择“添加”→“实体”。

Designer Window Entity

将显示“新建实体”对话框,如下面的图像所示。

New Entity Dialog

步骤 10 - 输入 Department 作为实体名称,输入 DeptID 作为属性名称,将属性类型保留为 Int32,然后单击“确定”。

步骤 11 - 右键单击实体,然后选择“添加新”→“标量属性”。

Scalar Property

步骤 12 - 将新属性重命名为 DeptName。

步骤 13 - 将新属性的类型更改为 Int32(默认情况下,新属性的类型为 String)。

步骤 14 - 要更改类型,请打开“属性”窗口并将“类型”属性更改为 Int32。

Type

步骤 15 - 在 Entity Framework 设计器中,右键单击 Name 属性,选择“转换为枚举”。

Entity Framework Designer

步骤 16 - 在“添加枚举类型”对话框中,输入 DepartmentNames 作为枚举类型名称,将“基础类型”更改为 Int32,然后向类型添加以下成员:物理、化学、计算机和经济学。

Add Enum

步骤 17 - 单击“确定”。

如果切换到“模型浏览器”窗口,您会看到该类型也已添加到“枚举类型”节点中。

Model Browser Window

让我们按照“模型优先”方法章节中提到的所有步骤从模型生成数据库。

步骤 1 - 右键单击实体设计器表面并选择“从模型生成数据库”。

将显示“生成数据库向导”的“选择数据连接”对话框。

步骤 2 - 单击“新建连接”按钮。

Connection Button

步骤 3 - 输入服务器名称和 EnumDemo 作为数据库名称,然后单击“确定”。

步骤 4 - 将弹出一个对话框,询问您是否要创建新数据库,单击“是”。

步骤 5 - 单击“下一步”,创建数据库向导将生成用于创建数据库的数据定义语言 (DDL)。现在单击“完成”。

步骤 6 - 右键单击 T-SQL 编辑器并选择“执行”。

TSql Editor

步骤 7 - 要查看生成的架构,请右键单击 SQL Server 对象资源管理器中的数据库名称,然后选择“刷新”。

您将在数据库中看到 Departments 表。

Departments Table

让我们看一下以下示例,其中一些新的 Department 对象被添加到上下文中并保存。然后检索计算机系。

class Program {

   static void Main(string[] args) {

      using (var context = new EFEnumModelContainer()) {

         context.Departments.Add(new Department { DeptName = DepartmentNames.Physics});
         context.Departments.Add(new Department { DeptName = DepartmentNames.Computer});
         context.Departments.Add(new Department { DeptName = DepartmentNames.Chemistry});
         context.Departments.Add(new Department { DeptName = DepartmentNames.Economics});

         context.SaveChanges();

         var department = (
            from d in context.Departments
            where d.DeptName == DepartmentNames.Computer
            select d
         ).FirstOrDefault();

         Console.WriteLine(
            "Department ID: {0}, Department Name: {1}", 
               department.DeptID, department.DeptName
         );

         Console.ReadKey();
      }
   }
}

执行上述代码时,您将收到以下输出 -

Department ID: 2, Department Name: Computer

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

Entity Framework - 异步查询

异步编程涉及在后台执行操作,以便主线程可以继续执行其自身的操作。这样,主线程可以在后台线程处理手头的任务时保持用户界面响应。

  • Entity Framework 6.0 支持用于查询和保存数据的异步操作。

  • 异步操作可以帮助您的应用程序实现以下目标 -

    • 使您的应用程序对用户交互更具响应性
    • 提高应用程序的整体性能
  • 您可以通过多种方式执行异步操作。但是,async/await 关键字是在 .NET Framework 4.5 中引入的,这使得您的工作变得简单。

  • 您只需遵循以下代码片段所示的 async/await 模式即可。

让我们看一下以下示例(不使用 async/await),其中 DatabaseOperations 方法将新学生保存到数据库,然后从数据库检索所有学生,最后在控制台上打印一些其他消息。

class Program {

   static void Main(string[] args) {
      Console.WriteLine("Database Operations Started");
      DatabaseOperations();
		
      Console.WriteLine();
      Console.WriteLine("Database Operations Completed");
      Console.WriteLine();
      Console.WriteLine("Entity Framework Tutorials");
      Console.ReadKey();
   }

   public static void DatabaseOperations() {

      using (var context = new UniContextEntities()) {

         // Create a new student and save it

         context.Students.Add(new Student {
            FirstMidName = "Akram", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())});

         Console.WriteLine("Calling SaveChanges.");
         context.SaveChanges();
         Console.WriteLine("SaveChanges completed.");

         // Query for all Students ordered by first name

         var students = (from s in context.Students
            orderby s.FirstMidName select s).ToList();

         // Write all students out to Console

         Console.WriteLine();
         Console.WriteLine("All Student:");

         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine(" " + name);
         }
      }
   }
}

执行上述代码时,您将收到以下输出 -

Calling SaveChanges.
SaveChanges completed.
All Student:
Akram Khan
Ali Khan
Ali Alexander
Arturo Anand
Bill Gates
Gytis Barzdukas
Laura  Nornan
Meredith fllonso
Nino Olioetto
Peggy Justice
Yan Li

Entity Framework Tutorials

让我们使用新的 async 和 await 关键字,并对 Program.cs 进行以下更改。

  • 添加 System.Data.Entity 命名空间,它将提供 EF 异步扩展方法。

  • 添加 System.Threading.Tasks 命名空间,它将允许我们使用 Task 类型。

  • DatabaseOperations 更新为标记为 async 并返回 Task

  • 调用 SaveChanges 的 Async 版本并等待其完成。

  • 调用 ToList 的异步版本并等待结果。

class Program {

   static void Main(string[] args) {
      var task = DatabaseOperations();
      Console.WriteLine();
      Console.WriteLine("Entity Framework Tutorials");
      task.Wait();
      Console.ReadKey();
   }

   public static async Task DatabaseOperations() {

      using (var context = new UniContextEntities()) {

         // Create a new blog and save it

         context.Students.Add(new Student {
            FirstMidName = "Salman", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())});

         Console.WriteLine("Calling SaveChanges.");
         await context.SaveChangesAsync();
         Console.WriteLine("SaveChanges completed.");

         // Query for all Students ordered by first name

         var students = await (from s in context.Students 
            orderby s.FirstMidName select s).ToListAsync();

         // Write all students out to Console

         Console.WriteLine();
         Console.WriteLine("All Student:");

         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName; 
            Console.WriteLine(" " + name);
         }
      }
   }
}

执行时,它将产生以下输出。

Calling SaveChanges.
Entity Framework Tutorials
SaveChanges completed.
All Student:
Akram Khan
Ali Khan
Ali Alexander
Arturo Anand
Bill Gates
Gytis Barzdukas
Laura  Nornan
Meredith fllonso
Nino Olioetto
Peggy Justice
Salman Khan
Yan Li

现在代码是异步的,您可以观察到程序的不同执行流程。

  • SaveChanges 开始将新的 Student 推送到数据库,然后 DatabaseOperations 方法返回(即使它尚未执行完成),并且 Main 方法中的程序流程继续。

  • 然后将消息写入控制台。

  • 托管线程在 Wait 调用上阻塞,直到数据库操作完成。完成后,将执行 DatabaseOperations 的其余部分。

  • SaveChanges 完成。

  • 从数据库中检索所有学生并写入控制台。

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

实体框架 - 持久性

现在,Entity Framework 允许您从 Entity Framework 中获益,而无需强制应用程序的每个部分都了解 Entity Framework,从而将实体与基础结构分离。您可以创建专注于其业务规则的类,而无需考虑如何持久化它们(数据存储在哪里以及数据如何在对象之间来回传递)。

创建持久化无感知实体

上一段描述了一种对所使用数据源一无所知的方法。这突出了持久化无感知的本质,即您的类和围绕它们的许多应用程序层不关心数据如何存储。

  • 在 Entity Framework 的 .NET 3.5 版本中,如果您想使用预先存在的类,则需要通过强制它们派生自 EntityObject 来修改它们。

  • 在 .NET 4 中,这不再必要。您无需修改实体即可让它们参与 Entity Framework 操作。

  • 这使我们能够构建采用松耦合和关注点分离的应用程序。

  • 使用这些编码模式,您的类只关心它们自己的工作,并且您的应用程序的许多层(包括 UI)都不依赖于外部逻辑(例如 Entity Framework API),但这些外部 API 能够与我们的实体交互。

使用 Entity Framework 持久化实体时,有两种方式(连接和断开连接)。这两种方式都有其自身的意义。在连接场景中,更改由上下文跟踪,但在断开连接的场景中,我们需要通知上下文实体的状态。

连接场景

连接场景是指从数据库中检索实体并在同一上下文中修改实体。对于连接场景,假设我们有一个 Windows 服务,并且我们正在使用该实体执行一些业务操作,因此我们将打开上下文,循环遍历所有实体,执行我们的业务操作,然后使用最初打开的相同上下文保存更改。

让我们看一下以下示例,其中从数据库中检索学生并更新学生的姓,然后将更改保存到数据库。

class Program {

   static void Main(string[] args) {

      using (var context = new MyContext()) {

         var studentList = context.Students.ToList();

         foreach (var stdnt in studentList) {
            stdnt.FirstMidName = "Edited " + stdnt.FirstMidName;
         }

         context.SaveChanges();

         //// Display all Students from the database

         var students = (from s in context.Students
            orderby s.FirstMidName select s).ToList<Student>();

         Console.WriteLine("Retrieve all Students from the database:");

         foreach (var stdnt in students) {
            string name = stdnt.FirstMidName + " " + stdnt.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
         }

         Console.ReadKey();
      }
   }
}

当编译并执行上述代码时,您将收到以下输出,并且您会看到“Edited”一词附加在姓之前,如下面的输出所示。

Retrieve all Students from the database: 
ID: 1, Name: Edited Edited Alain Bomer 
ID: 2, Name: Edited Edited Mark Upston 

断开连接场景

断开连接场景是指从数据库中检索实体并在不同的上下文中修改实体。假设我们想要在表示层中显示一些数据,并且我们正在使用某个 N 层应用程序,因此最好打开上下文,获取数据,最后关闭上下文。由于我们在此处获取了数据并关闭了上下文,因此我们获取的实体不再被跟踪,这就是断开连接的场景。

让我们看一下以下代码,其中使用 Add 方法将新的断开连接的 Student 实体添加到上下文中。

class Program {

   static void Main(string[] args) {

      var student = new Student {
         ID = 1001, 
         FirstMidName = "Wasim", 
         LastName = "Akram", 
         EnrollmentDate = DateTime.Parse( DateTime.Today.ToString())
      };

      using (var context = new MyContext()) {

         context.Students.Add(student);
         context.SaveChanges();

         //// Display all Students from the database

         var students = (from s in context.Students 
            orderby s.FirstMidName select s).ToList<Student>();

         Console.WriteLine("Retrieve all Students from the database:");

         foreach (var stdnt in students) {
            string name = stdnt.FirstMidName + " " + stdnt.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
         }

         Console.ReadKey();
      }
   }
}

当编译并执行上述代码时,您将收到以下输出。

Retrieve all Students from the database:
ID: 1, Name: Edited Edited Edited Alain Bomer
ID: 2, Name: Edited Edited Edited Mark Upston
ID: 3, Name: Wasim Akram

Entity Framework - 投影查询

LINQ to Entities

理解 LINQ to Entities 最重要的概念之一是它是一种声明式语言。重点在于定义您需要什么信息,而不是如何获取信息。

  • 这意味着您可以将更多时间用于处理数据,而减少用于弄清楚执行诸如访问数据库等任务所需的底层代码的时间。

  • 重要的是要理解,声明式语言实际上并没有消除开发人员的任何控制权,但它有助于开发人员将注意力集中在重要的事情上。

LINQ to Entities 核心关键字

了解用于创建 LINQ 查询的基本关键字非常重要。只需要记住几个关键字,但您可以以各种方式组合它们以获得特定的结果。以下列表包含这些基本关键字,并提供每个关键字的简单描述。

序号 关键字及描述
1

Ascending

指定排序操作从范围的最小(或最低)元素到范围的最高元素进行。这通常是默认设置。例如,在执行字母排序时,排序范围将是从 A 到 Z。

2

By

指定用于实现分组的字段或表达式。字段或表达式定义用于执行分组任务的键。

3

Descending

指定排序操作从范围的最大(或最高)元素到范围的最低元素进行。例如,在执行字母排序时,排序范围将是从 Z 到 A。

4

Equals

用于连接语句的左右子句之间,将主要上下文数据源连接到辅助上下文数据源。equals 关键字左侧的字段或表达式指定主要数据源,而 equals 关键字右侧的字段或表达式指定辅助数据源。

5

From

指定用于获取所需信息的数据源并定义范围变量。此变量与循环中用于迭代的变量具有相同的用途。

6

Group

使用您指定的键值将输出组织成组。使用多个 group 子句创建多级输出组织。group 子句的顺序决定了特定键值在分组顺序中出现的深度。您可以将此关键字与 by 结合使用以创建特定的上下文。

7

In

以多种方式使用。在这种情况下,关键字确定查询使用的上下文数据库源。在使用连接时,in 关键字用于连接使用的每个上下文数据库源。

8

Into

指定一个标识符,您可以将其用作 LINQ 查询子句(如 join、group 和 select)的引用。

9

Join

从两个相关数据源(例如在主/从设置中)创建一个单一数据源。连接可以指定内部连接、组连接或左外部连接,其中内部连接为默认值。您可以在 msdn.microsoft.com 上阅读有关连接的更多信息。

10

Let

定义一个范围变量,您可以使用它在查询表达式中存储子表达式结果。通常,范围变量用于提供额外的枚举输出或提高查询效率(以便无需多次执行特定任务,例如查找字符串的小写值)。

11

On

指定用于实现连接的字段或表达式。字段或表达式定义一个对两个上下文数据源都通用的元素。

12

Orderby

为查询创建排序顺序。您可以添加 ascending 或 descending 关键字来控制排序顺序。使用多个 orderby 子句创建多级排序。orderby 子句的顺序决定了排序表达式处理的顺序,因此使用不同的顺序将导致不同的输出。

13

Where

定义 LINQ 应从数据源中检索什么。您可以使用一个或多个布尔表达式来定义要检索内容的细节。布尔表达式使用 &&(AND)和 ||(OR)运算符彼此分隔。

14

Select

通过指定要返回的信息来确定 LINQ 查询的输出。此语句定义 LINQ 在迭代过程中返回的元素的数据类型。

Projection

投影查询通过仅从数据库中检索特定字段来提高应用程序的效率。

  • 获取数据后,您可能需要根据需要对其进行投影或筛选,以在输出之前塑造数据。

  • 任何 LINQ to Entities 表达式的主要任务是获取数据并将其作为输出提供。

本章的“开发 LINQ to Entities 查询”部分演示了执行此基本任务的技术。

让我们看一下以下代码,其中将检索学生列表。

using (var context = new UniContextEntities()) {

   var studentList = from s in context.Students select s;

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }
}

单个对象

要检索单个学生对象,可以使用 First() 或 FirstOrDefault 可枚举方法,它们返回序列的第一个元素。First 和 FirstOrDefault 之间的区别在于,如果为提供的条件没有结果数据,则 First() 将引发异常,而 FirstOrDefault() 将返回默认值 null,如果为提供的条件没有结果数据。

using (var context = new UniContextEntities()) {

   var student = (from s in context.Students where s.FirstMidName 
      == "Ali" select s).FirstOrDefault<Student>();

   string name = student.FirstMidName + " " + student.LastName;
   Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
}

您还可以使用 Single() 或 SingleOrDefault 获取单个学生对象,它返回序列的单个特定元素。在以下示例中,检索了 ID 为 2 的单个学生。

using (var context = new UniContextEntities()) {

   var student = (from s in context.Students where s.ID 
      == 2 select s).SingleOrDefault<Student>();
   string name = student.FirstMidName + " " + student.LastName;
	
   Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   Console.ReadKey();
}

对象列表

如果要检索姓为 Ali 的学生列表,则可以使用 ToList() 可枚举方法。

using (var context = new UniContextEntities()) {

   var studentList = (from s in context.Students where s.FirstMidName 
      == "Ali" select s).ToList();

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }

   Console.ReadKey();
}

排序

要以任何特定顺序检索数据/列表,可以使用 orderby 关键字。在以下代码片段中,将按升序检索学生列表。

using (var context = new UniContextEntities()) {

   var studentList = (from s in context.Students orderby
      s.FirstMidName ascending select s).ToList();

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }

   Console.ReadKey();
}

标准与投影 Entity Framework 查询

假设您有一个 Student 模型,其中包含 ID、FirstMidName、LastName 和 EnrollmentDate。如果要返回学生列表,则标准查询将返回所有字段。但如果您只想获取包含 ID、FirstMidName 和 LastName 字段的学生列表。此时,您应该使用投影查询。以下是投影查询的简单示例。

using (var context = new UniContextEntities()) {

   var studentList = from s in context.Students
      orderby s.FirstMidName ascending
      where s.FirstMidName == "Ali"

   select new {s.ID, s.FirstMidName, s.LastName};

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }

   Console.ReadKey();
}

上面的投影查询排除了 EnrollmentDate 字段。这将使您的应用程序更快。

Entity Framework - 命令日志记录

在 Entity Framework 6.0 中,引入了一项新功能,称为**日志记录 SQL**。在使用 Entity Framework 时,它会向数据库发送命令或等效的 SQL 查询以执行 CRUD(创建、读取、更新和删除)操作。

  • Entity Framework 的此功能旨在捕获 Entity Framework 内部生成的等效 SQL 查询并将其作为输出提供。

  • 在 Entity Framework 6 之前,每当需要跟踪数据库查询和命令时,开发人员别无选择,只能使用某些第三方跟踪实用程序或数据库跟踪工具。

  • 在 Entity Framework 6 中,此新功能提供了一种简单的方法来记录 Entity Framework 执行的所有操作。

  • Entity Framework 执行的所有活动都使用 DbContext.Database.Log 进行记录。

让我们看一下以下代码,其中将新学生添加到数据库中。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Database.Log = Console.Write;

         // Create a new student and save it

         context.Students.Add(new Student {
            FirstMidName = "Salman", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         context.SaveChanges();
         Console.ReadKey();
      }
   }
}

当执行上述代码时,您将收到以下输出,这实际上是 EF 在上述代码中执行的所有活动的日志。

Opened connection at 10/28/2015 6:27:35 PM +05:00
Started transaction at 10/28/2015 6:27:35 PM +05:00
INSERT [dbo].[Student]([LastName], [FirstMidName], [EnrollmentDate])
VALUES (@0, @1, @2)
SELECT [ID]
FROM [dbo].[Student]
WHERE @@ROWCOUNT > 0 AND [ID] = scope_identity()
-- @0: 'Khan' (Type = String, Size = -1)
-- @1: 'Salman' (Type = String, Size = -1)
-- @2: '10/28/2015 12:00:00 AM' (Type = DateTime)
-- Executing at 10/28/2015 6:27:35 PM +05:00
-- Completed in 5 ms with result: SqlDataReader
Committed transaction at 10/28/2015 6:27:35 PM +05:00
Closed connection at 10/28/2015 6:27:35 PM +05:00

当设置 Log 属性时,将记录以下活动:

  • 所有不同类型命令的 SQL,例如查询,包括作为 SaveChanges 部分生成插入、更新和删除操作的查询

  • 参数

  • 命令是否正在异步执行

  • 指示命令何时开始执行的时间戳

  • 命令成功完成还是失败

  • 结果值的一些指示

  • 执行命令大约花费的时间

日志记录到其他位置

如果您已经有某个日志记录框架并且它定义了日志记录方法,那么您也可以将其记录到其他位置。

让我们看一下以下示例,其中我们有另一个类 MyLogger。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Database.Log = s ⇒ MyLogger.Log("EFLoggingDemo", s);

         // Create a new student and save it

         context.Students.Add(new Student {
            FirstMidName = "Salman", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         context.SaveChanges();
         Console.ReadKey();
      }
   }
}

public class MyLogger {

   public static void Log(string application, string message) {
      Console.WriteLine("Application: {0}, EF Message: {1} ",application, message);
   }
}

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

实体框架 - 命令拦截

在 Entity Framework 6.0 中,还有一个名为**拦截器**或拦截的新功能。拦截代码围绕**拦截接口**的概念构建。例如,IDbCommandInterceptor 接口定义了在 EF 调用 ExecuteNonQuery、ExecuteScalar、ExecuteReader 和相关方法之前调用的方法。

  • 通过使用拦截,实体框架可以真正发挥其优势。使用这种方法,您可以捕获更多瞬态信息,而无需弄乱您的代码。

  • 要实现此目的,您需要创建自己的自定义拦截器并相应地注册它。

  • 一旦创建了实现 IDbCommandInterceptor 接口的类,就可以使用 DbInterception 类将其注册到实体框架。

  • IDbCommandInterceptor 接口有六个方法,您需要实现所有这些方法。以下是这些方法的基本实现。

让我们看一下以下代码,其中实现了 IDbCommandInterceptor 接口。

public class MyCommandInterceptor : IDbCommandInterceptor {

   public static void Log(string comm, string message) {
      Console.WriteLine("Intercepted: {0}, Command Text: {1} ", comm, message);
   }

   public void NonQueryExecuted(DbCommand command, 
      DbCommandInterceptionContext<int> interceptionContext) {
         Log("NonQueryExecuted: ", command.CommandText);
   }

   public void NonQueryExecuting(DbCommand command, 
      DbCommandInterceptionContext<int> interceptionContext) {
         Log("NonQueryExecuting: ", command.CommandText);
   }

   public void ReaderExecuted(DbCommand command, 
      DbCommandInterceptionContext<DbDataReader> interceptionContext) {
         Log("ReaderExecuted: ", command.CommandText);
   }

   public void ReaderExecuting(DbCommand command, 
      DbCommandInterceptionContext<DbDataReader> interceptionContext) {
         Log("ReaderExecuting: ", command.CommandText);
   }

   public void ScalarExecuted(DbCommand command, 
      DbCommandInterceptionContext<object> interceptionContext) {
         Log("ScalarExecuted: ", command.CommandText);
   }

   public void ScalarExecuting(DbCommand command, 
      DbCommandInterceptionContext<object> interceptionContext) {
         Log("ScalarExecuting: ", command.CommandText);
   }

}

注册拦截器

一旦创建了实现一个或多个拦截接口的类,就可以使用 DbInterception 类将其注册到 EF,如下面的代码所示。

DbInterception.Add(new MyCommandInterceptor());

还可以使用 DbConfiguration 基于代码的配置在应用程序域级别注册拦截器,如下面的代码所示。

public class MyDBConfiguration : DbConfiguration {

   public MyDBConfiguration() {
      DbInterception.Add(new MyCommandInterceptor());
   }
}

您还可以使用以下代码配置拦截器配置文件:

<entityFramework>
   <interceptors>
      <interceptor type = "EFInterceptDemo.MyCommandInterceptor, EFInterceptDemo"/>
   </interceptors>
</entityFramework>

实体框架 - 空间数据类型

实体框架 5 中引入了空间类型支持。还包括一组运算符,允许查询分析空间数据。例如,查询可以根据两个地理位置之间的距离进行筛选。

  • 实体框架将允许将新的空间数据类型作为属性公开到您的类中,并将它们映射到数据库中的空间列。

  • 您还可以编写 LINQ 查询,这些查询利用空间运算符根据数据库中执行的空间计算进行筛选、排序和分组。

主要有两种空间数据类型:

  • geography 数据类型存储椭球数据,例如 GPS 纬度和经度坐标。

  • geometry 数据类型表示欧几里得(平面)坐标系。

让我们看一下以下板球场的示例。

步骤 1 - 从“文件”→“新建”→“项目”菜单选项创建新项目。

步骤 2 - 在左侧窗格中,选择控制台应用程序。

Cricket Project

**步骤 3** - 右键单击项目名称,然后选择“管理 NuGet 包…”。

NuGet Project

**步骤 4** - 安装实体框架。

**步骤 5** - 添加对 System.Data.Entity 程序集的引用,并为空间数据类型添加 System.Data.Spatial using 语句。

Add Reference

**步骤 6** - 在 Program.cs 文件中添加以下类。

public class CricketGround {
   public int ID { get; set; }
   public string Name { get; set; }
   public DbGeography Location { get; set; }
}

**步骤 7** - 除了定义实体之外,您还需要定义一个从 DbContext 派生的类,并公开 DbSet<TEntity> 属性。

在 Program.cs 中添加上下文定义。

public partial class CricketGroundContext : DbContext {
   public DbSet<CricketGround> CricketGrounds { get; set; }
}

**步骤 8** - 将以下代码添加到 Main 函数中,这将向上下文添加两个新的 CricketGround 对象。

class Program {

   static void Main(string[] args) {

      using (var context = new CricketGroundContext()) {

         context.CricketGrounds.Add(new CricketGround() {
            Name = "Shalimar Cricket Ground", 
            Location = DbGeography.FromText("POINT(-122.336106 47.605049)"), 
         });

         context.CricketGrounds.Add(new CricketGround() {
            Name = "Marghazar Stadium", Location = DbGeography
               .FromText("POINT(-122.335197 47.646711)"), 
         });

         context.SaveChanges();

         var myLocation = DbGeography.FromText("POINT(-122.296623 47.640405)");

         var cricketGround = (from cg in context.CricketGrounds
            orderby cg.Location.Distance(myLocation) select cg).FirstOrDefault();

         Console.WriteLine("The closest Cricket Ground to you is: {0}.", cricketGround.Name);
      }
   }
}

使用 DbGeography.FromText 方法初始化空间属性。表示为 WellKnownText 的地理点传递给该方法,然后保存数据。之后将检索 CricketGround 对象,其位置最接近指定位置。

执行上述代码时,您将收到以下输出 -

The closest Cricket Ground to you is: Marghazar Stadium

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

实体框架 - 继承

继承使创建更能反映开发人员思维方式的复杂模型成为可能,并且减少了与这些模型交互所需的工作量。与实体一起使用的继承与与类一起使用的继承具有相同的目的,因此开发人员已经了解此功能的工作原理。

让我们看一下以下示例,并通过创建一个新的控制台应用程序项目。

**步骤 1** - 通过右键单击项目名称并选择“添加”→“新建项…”来添加 ADO.NET 实体数据模型。

**步骤 2** - 通过遵循模型优先方法一章中提到的所有步骤,添加一个实体并将其命名为 Person。

**步骤 3** - 添加一些标量属性,如下面的图像所示。

Add Scalar Properties

**步骤 4** - 我们将添加另外两个实体**Student** 和**Teacher**,它们将继承 Person 表的属性。

**步骤 5** - 现在添加 Student 实体,并从“基类型”组合框中选择 Person,如下面的图像所示。

Base Type ComboBox

**步骤 6** - 同样添加 Teacher 实体。

**步骤 7** - 现在将 EnrollmentDate 标量属性添加到 Student 实体,并将 HireDate 属性添加到 Teacher 实体。

EnrollmentDate

**步骤 8** - 让我们继续生成数据库。

**步骤 9** - 右键单击设计图面,然后选择“从模型生成数据库…”。

Generate Database

**步骤 10** - 要创建新数据库,请单击“新建连接…”。将打开以下对话框。单击“确定”。

New Database

**步骤 11** - 单击“完成”。这将在项目中添加 *.edmx.sql 文件。您可以通过打开 .sql 文件在 Visual Studio 中执行 DDL 脚本。现在右键单击并选择“执行”。

**步骤 12** - 转到服务器资源管理器,您将看到数据库已创建并包含三个指定的表。

Database Created Table

**步骤 13** - 您还可以看到以下域类也自动生成了。

public partial class Person {
   public int ID { get; set; }
   public string FirstMidName { get; set; }
   public string LastName { get; set; }
}

public partial class Student : Person {
   public System.DateTime EnrollmentDate { get; set; }
}

public partial class Teacher : Person {
   public System.DateTime HireDate { get; set; }
}

以下是 Context 类。

public partial class InheritanceModelContainer : DbContext {

   public InheritanceModelContainer() : 
      base("name = InheritanceModelContainer") {}

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      throw new UnintentionalCodeFirstException();
   }

   public virtual DbSet<Person> People { get; set; }
}

让我们向数据库添加一些学生和教师,然后从数据库中检索它们。

class Program {

   static void Main(string[] args) {

      using (var context = new InheritanceModelContainer()) {

         var student = new Student {
            FirstMidName = "Meredith", 
            LastName = "Alonso", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(student);

         var student1 = new Student {
            FirstMidName = "Arturo", 
            LastName = "Anand", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(student1);

         var techaer = new Teacher {
            FirstMidName = "Peggy", 
            LastName = "Justice", 
            HireDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(techaer);

         var techaer1 = new Teacher {
            FirstMidName = "Yan", 
            LastName = "Li", 
            HireDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(techaer1);
         context.SaveChanges();
      }
   }
}

学生和教师已添加到数据库中。要检索学生和教师,需要使用**OfType**方法,该方法将返回与指定部门相关的 Student 和 Teacher。

Console.WriteLine("All students in database"); 
Console.WriteLine("");

foreach (var student in context.People.OfType<Student>()) {
   string name = student.FirstMidName + " " + student.LastName;
   Console.WriteLine("ID: {0}, Name: {1}, \tEnrollment Date {2} ", 
      student.ID, name, student.EnrollmentDate.ToString());
}

Console.WriteLine("");
Console.WriteLine("************************************************************ *****");
Console.WriteLine("");
Console.WriteLine("All teachers in database");
Console.WriteLine("");

foreach (var teacher in context.People.OfType<Teacher>()) {
   string name = teacher.FirstMidName + " " + teacher.LastName;
   Console.WriteLine("ID: {0}, Name: {1}, \tHireDate {2} ", 
      teacher.ID, name, teacher.HireDate.ToString()); 
}

Console.WriteLine("");
Console.WriteLine("************************************************************ *****");
Console.ReadKey();

在第一个查询中,当您使用 OfType<Student>() 时,您将无法访问 HireDate,因为 HireDate 属性是 Teacher 实体的一部分,类似地,当您使用 OfType<Teacher>() 时,EnrollmentDate 属性将不可访问。

执行上述代码时,您将收到以下输出 -

All students in database
ID: 1, Name: Meredith Alonso,   Enrollment Date 10/30/2015 12:00:00 AM
ID: 2, Name: Arturo Anand,      Enrollment Date 10/30/2015 12:00:00 AM
*****************************************************************  
All teachers in database
ID: 3, Name: Peggy Justice,     HireDate 10/30/2015 12:00:00 AM
ID: 4, Name: Yan Li,    HireDate 10/30/2015 12:00:00 AM
*****************************************************************

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

实体框架 - 迁移

在实体框架 5 和以前的实体框架版本中,代码在作为 .NET Framework 部分提供的核心库(主要是 System.Data.Entity.dll)和使用 NuGet 分发和提供的其他库(主要是 EntityFramework.dll)之间拆分,如下面的图表所示。

DotNet Framework

在实体框架 6 中,以前是 .NET 框架一部分的核心 API 也作为 NuGet 包的一部分分发。

Core APIs

这是为了允许实体框架开源。但是,结果,每当需要将应用程序从旧版本的实体框架迁移或升级到 EF 6 时,都需要重新构建应用程序。

如果您的应用程序使用 DbContext(在 EF 4.1 及更高版本中提供),则迁移过程很简单。但是,如果您的应用程序是 ObjectContext,则需要稍微多做一些工作。

让我们看一下将现有应用程序升级到 EF6 需要经历的以下步骤。

**步骤 1** - 第一步是将目标设置为 .NET Framework 4.5.2 及更高版本,右键单击您的项目并选择“属性”。

Upgrade EF6

**步骤 2** - 再次右键单击您的项目,然后选择“管理 NuGet 包…”。

Manage NuGet Packages

**步骤 3** - 在“联机”选项卡下选择 EntityFramework 并单击“安装”。确保已删除对 System.Data.Entity.dll 的程序集引用。

当您安装 EF6 NuGet 包时,它应该会自动为您删除项目中对 System.Data.Entity 的任何引用。

**步骤 4** - 如果您有任何使用 EF 设计器创建的模型,那么您还需要更新代码生成模板以生成与 EF6 兼容的代码。

**步骤 5** - 在解决方案资源管理器中,在您的 edmx 文件下,删除现有的代码生成模板,这些模板通常将命名为 <edmx_file_name>.tt 和 <edmx_file_name>.Context.tt。

Edmx

**步骤 6** - 在 EF 设计器中打开您的模型,右键单击设计图面,然后选择“添加代码生成项…”。

**步骤 7** - 添加适当的 EF 6.x 代码生成模板。

Code Generation Template

它还将自动生成与 EF6 兼容的代码。

如果您的应用程序使用 EF 4.1 或更高版本,则无需在代码中更改任何内容,因为 DbContext 和 Code First 类型的命名空间没有更改。

但是,如果您的应用程序使用的是旧版本的实体框架,则以前位于 System.Data.Entity.dll 中的 ObjectContext 等类型已移至新的命名空间。

**步骤 8** - 您需要更新 using 或 Import 指令以针对 EF6 构建。

命名空间更改的一般规则是,System.Data.* 中的任何类型都将移至 System.Data.Entity.Core.*。以下是一些示例:

  • System.Data.EntityException ⇒ System.Data.Entity.Core.EntityException
  • System.Data.Objects.ObjectContext ⇒ System.Data.Entity.Core.Objects.ObjectContext;
  • System.Data.Objects.DataClasses.RelationshipManager ⇒ System.Data.Entity.Core.Objects.DataClasses.RelationshipManager;

某些类型位于Core命名空间中,因为它们不是大多数基于 DbContext 的应用程序直接使用的。

  • System.Data.EntityState ⇒ System.Data.Entity.EntityState
  • System.Data.Objects.DataClasses.EdmFunctionAttribute ⇒ System.Data.Entity.DbFunctionAttribute

您现有的实体框架项目将在实体框架 6.0 中运行,无需进行任何重大更改。

实体框架 - 渴望加载

急切加载是指查询一种类型的实体的过程,同时还将相关实体作为查询的一部分加载。急切加载是通过使用**Include 方法**实现的。

这意味着请求将相关数据与来自数据库的查询结果一起返回。只进行一次到数据源的连接,在初始查询中返回大量数据。

例如,在查询学生时,急切加载他们的注册信息。学生及其注册信息将在单个查询中检索。

让我们看一下以下示例,其中所有学生及其各自的注册信息都通过使用急切加载从数据库中检索。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {
         // Load all students and related enrollments
         var students = context.Students
            .Include(s ⇒ s.Enrollments).ToList();
			
         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);
				
            foreach (var enrollment in student.Enrollments) {
               Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
                  enrollment.EnrollmentID, enrollment.CourseID);
            }
         }

         Console.ReadKey();
      }
   }
}

当编译并执行上述代码时,您将收到以下输出。

ID: 1, Name: Ali Alexander
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041
ID: 2, Name: Meredith Alonso
       Enrollment ID: 4, Course ID: 1045
       Enrollment ID: 5, Course ID: 3141
       Enrollment ID: 6, Course ID: 2021
ID: 3, Name: Arturo Anand
       Enrollment ID: 7, Course ID: 1050
ID: 4, Name: Gytis Barzdukas
       Enrollment ID: 8, Course ID: 1050
       Enrollment ID: 9, Course ID: 4022

以下是可使用的其他一些急切加载查询形式。

// Load one Student and its related enrollments

var student1 = context.Students
   .Where(s ⇒ s.FirstMidName == "Ali")
   .Include(s ⇒ s.Enrollments).FirstOrDefault();

// Load all Students and related enrollments
// using a string to specify the relationship

var studentList = context.Students
   .Include("Enrollments").ToList();

// Load one Student and its related enrollments
// using a string to specify the relationship

var student = context.Students
   .Where(s ⇒ s.FirstMidName == "Salman")
   .Include("Enrollments").FirstOrDefault();

多级

还可以急切加载多级相关实体。以下查询显示了 Student、Enrollments 和 Course 的示例。

// Load all Students, all related enrollments, and all related courses

var studentList = context.Students
   .Include(s ⇒ s.Enrollments.Select(c ⇒ c.Course)).ToList();

// Load all Students, all related enrollments, and all related courses
// using a string to specify the relationships

var students = context.Students
   .Include("Enrollments.Course").ToList();

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

实体框架 - 延迟加载

延迟加载是指在第一次访问引用实体/实体的属性时自动从数据库加载实体或实体集合的过程。延迟加载意味着延迟加载相关数据,直到您明确请求它。

  • 当使用 POCO 实体类型时,延迟加载是通过创建派生代理类型的实例,然后覆盖虚拟属性以添加加载挂钩来实现的。

  • 延迟加载几乎是默认设置。

  • 如果使用默认配置,并且在查询中没有显式地告诉 Entity Framework 你想要除了延迟加载之外的其他东西,那么你将获得延迟加载。

  • 例如,当使用 Student 实体类时,相关的 Enrollments 将在第一次访问 Enrollments 导航属性时加载。

  • 导航属性应定义为 public,virtual。如果属性未定义为 virtual,则 Context **不会**执行延迟加载。

下面是一个包含 Enrollments 导航属性的 Student 类。

public partial class Student {

   public Student() {
      this.Enrollments = new HashSet<Enrollment>();
   }
	
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public System.DateTime EnrollmentDate { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

让我们来看一个简单的例子,其中首先从数据库中加载学生列表,然后在你需要的时候加载特定学生的报名信息。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         //Loading students only
         IList<Student> students = context.Students.ToList<Student>();

         foreach (var student in students) {

            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);
	
            foreach (var enrollment in student.Enrollments) {
               Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
                  enrollment.EnrollmentID, enrollment.CourseID);
            }
         }

         Console.ReadKey();
      }
   }
}	

当编译并执行上述代码时,您将收到以下输出。

ID: 1, Name: Ali Alexander
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041
ID: 2, Name: Meredith Alonso
       Enrollment ID: 4, Course ID: 1045
       Enrollment ID: 5, Course ID: 3141
       Enrollment ID: 6, Course ID: 2021
ID: 3, Name: Arturo Anand
       Enrollment ID: 7, Course ID: 1050
ID: 4, Name: Gytis Barzdukas
       Enrollment ID: 8, Course ID: 1050
       Enrollment ID: 9, Course ID: 4022
ID: 5, Name: Yan Li
       Enrollment ID: 10, Course ID: 4041
ID: 6, Name: Peggy Justice
       Enrollment ID: 11, Course ID: 1045
ID: 7, Name: Laura Norman
       Enrollment ID: 12, Course ID: 3141

关闭延迟加载

延迟加载和序列化并不兼容,如果不注意,你可能会因为启用了延迟加载而最终查询整个数据库。在序列化实体之前关闭延迟加载是一个好习惯。

关闭特定导航属性的延迟加载

可以通过将 Enrollments 属性设置为非 virtual 来关闭 Enrollments 集合的延迟加载,如下例所示。

public partial class Student { 

   public Student() { 
      this.Enrollments = new HashSet<Enrollment>(); 
   }
	
   public int ID { get; set; } 
   public string LastName { get; set; } 
   public string FirstMidName { get; set; } 
   public System.DateTime EnrollmentDate { get; set; }
   public ICollection<Enrollment> Enrollments { get; set; } 
}

关闭所有实体的延迟加载

可以通过将 Configuration 属性上的标志设置为 false 来关闭上下文中所有实体的延迟加载,如下例所示。

public partial class UniContextEntities : DbContext { 

   public UniContextEntities(): base("name=UniContextEntities") {
      this.Configuration.LazyLoadingEnabled = false;
   }
	
   protected override void OnModelCreating(DbModelBuilder modelBuilder) { 
      throw new UnintentionalCodeFirstException(); 
   } 
}

关闭延迟加载后,现在再次运行上面的示例,你将看到 Enrollments 没有加载,并且只检索了学生数据。

ID: 1, Name: Ali Alexander
ID: 2, Name: Meredith Alons
ID: 3, Name: Arturo Anand
ID: 4, Name: Gytis Barzduka
ID: 5, Name: Yan Li
ID: 6, Name: Peggy Justice
ID: 7, Name: Laura Norman
ID: 8, Name: Nino Olivetto

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

实体框架 - 显式加载

禁用延迟加载后,仍然可以延迟加载相关实体,但必须通过显式调用来完成。

  • 与延迟加载不同,关于何时运行查询没有歧义或混淆的可能性。

  • 为此,你可以在相关实体的条目上使用 Load 方法。

  • 对于一对多关系,在集合上调用 Load 方法。

  • 对于一对一关系,在引用上调用 Load 方法。

让我们来看下面的例子,其中延迟加载被禁用,然后检索名为 Ali 的学生。

然后将学生信息写入控制台。如果查看代码,报名信息也被写入,但 Enrollments 实体尚未加载,因此 foreach 循环不会执行。

之后,显式加载 Enrollments 实体,现在学生信息和报名信息将写入控制台窗口。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Configuration.LazyLoadingEnabled = false;

         var student = (from s in context.Students where s.FirstMidName == 
            "Ali" select s).FirstOrDefault<Student>();

         string name = student.FirstMidName + " " + student.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
               enrollment.EnrollmentID, enrollment.CourseID);
         }

         Console.WriteLine();
         Console.WriteLine("Explicitly loaded Enrollments");
         Console.WriteLine();

         context.Entry(student).Collection(s ⇒ s.Enrollments).Load();
         Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
               enrollment.EnrollmentID, enrollment.CourseID);
         }

         Console.ReadKey();
      }
   }
}

执行上述示例时,你将收到以下输出。首先只显示学生信息,然后显式加载 Enrollments 实体后,显示学生及其相关报名信息。

ID: 1, Name: Ali Alexander
Explicitly loaded Enrollments
ID: 1, Name: Ali Alexander
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041

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

实体框架 - 验证

在本章中,让我们学习可以在 ADO.NET Entity Framework 中使用的验证技术来验证模型数据。Entity Framework 提供了各种各样的验证功能,可以实现到用户界面以进行客户端验证,也可以用于服务器端验证。

  • 在 Entity Framework 中,数据验证是应用程序中捕获错误数据解决方案的一部分。

  • Entity Framework 在将数据写入数据库之前默认会验证所有数据,并使用各种数据验证方法。

  • 但是,Entity Framework 在用户界面数据验证之后出现。因此,在这种情况下,需要实体验证来处理 EF 引发的任何异常并显示通用消息。

  • 有一些数据验证技术可以改进错误检查以及如何将错误消息传递回用户。

DbContext 有一个可重写的方法,称为 ValidateEntity。当你调用 SaveChanges 时,Entity Framework 将为其缓存中状态不是 Unchanged 的每个实体调用此方法。你可以将验证逻辑直接放在这里,如下例中针对 Student 实体所示。

public partial class UniContextEntities : DbContext {

   protected override System.Data.Entity.Validation
      .DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, 
      System.Collections.Generic.IDictionary<object, object> items) {

         if (entityEntry.Entity is Student) {

            if (entityEntry.CurrentValues.GetValue<string>("FirstMidName") == "") {

               var list = new List<System.Data.Entity
                  .Validation.DbValidationError>();

               list.Add(new System.Data.Entity.Validation
                  .DbValidationError("FirstMidName", "FirstMidName is required"));

               return new System.Data.Entity.Validation
                  .DbEntityValidationResult(entityEntry, list);
            }
         }

         if (entityEntry.CurrentValues.GetValue<string>("LastName") == "") {

            var list = new List<System.Data.Entity
               .Validation.DbValidationError>();

            list.Add(new System.Data.Entity.Validation
               .DbValidationError("LastName", "LastName is required"));

            return new System.Data.Entity.Validation
               .DbEntityValidationResult(entityEntry, list);
         }

         return base.ValidateEntity(entityEntry, items);
   }
}

在上面的 ValidateEntity 方法中,检查 Student 实体的 FirstMidName 和 LastName 属性,如果这些属性中的任何一个为空字符串,则将返回错误消息。

让我们来看一个简单的例子,其中创建一个新学生,但学生的 FirstMidName 为空字符串,如下面的代码所示。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         Console.WriteLine("Adding new Student to the database");
         Console.WriteLine();

         try {

            context.Students.Add(new Student() {
               FirstMidName = "",
               LastName = "Upston"
            });

            context.SaveChanges();
         } catch (DbEntityValidationException dbValidationEx) {

            foreach (DbEntityValidationResult entityErr in 
               dbValidationEx.EntityValidationErrors) {

               foreach (DbValidationError error in entityErr.ValidationErrors) {
                  Console.WriteLine("Error: {0}",error.ErrorMessage);
               }
            }
         }

         Console.ReadKey();
      }
   }
}

编译并执行上述示例时,你将在控制台窗口中收到以下错误消息。

Adding new Student to the database  
Error: FirstMidName is required 

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

实体框架 - 跟踪更改

Entity Framework 提供了跟踪对实体及其关系所做的更改的功能,因此当调用上下文的 SaveChanges 方法时,将在数据库上进行正确的更新。这是 Entity Framework 的一个关键特性。

  • 更改跟踪在向实体集合添加新记录、修改或删除现有实体时跟踪更改。

  • 然后 DbContext 级别保留所有更改。

  • 如果在销毁 DbContext 对象之前未保存这些跟踪更改,则会丢失这些更改。

  • DbChangeTracker 类提供有关上下文当前跟踪的所有实体的信息。

  • 要让上下文跟踪任何实体,该实体必须具有主键属性。

在 Entity Framework 中,更改跟踪默认启用。你还可以通过将 DbContext 的 AutoDetectChangesEnabled 属性设置为 false 来禁用更改跟踪。如果此属性设置为 true,则 Entity Framework 会维护实体的状态。

using (var context = new UniContextEntities()) {
   context.Configuration.AutoDetectChangesEnabled = true;
}

让我们来看下面的例子,其中从数据库中检索学生及其报名信息。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Configuration.AutoDetectChangesEnabled = true;
         Console.WriteLine("Retrieve Student");

         var student = (from s in context.Students where s.FirstMidName == 
            "Ali" select s).FirstOrDefault<Student>();

         string name = student.FirstMidName + " " + student.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);
         Console.WriteLine();
         Console.WriteLine("Retrieve all related enrollments");

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
               enrollment.EnrollmentID, enrollment.CourseID);
         }

         Console.WriteLine();

         Console.WriteLine("Context tracking changes of {0} entity.", 
            context.ChangeTracker.Entries().Count());

         var entries = context.ChangeTracker.Entries();

         foreach (var entry in entries) {
            Console.WriteLine("Entity Name: {0}", entry.Entity.GetType().Name);
            Console.WriteLine("Status: {0}", entry.State);
         }

         Console.ReadKey();
      }
   }
}

编译并执行上述示例时,你将收到以下输出。

Retrieve Student 
ID: 1, Name: Ali Alexander
Retrieve all related enrollments
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041
Context tracking changes of 4 entity.
Entity Name: Student
Status: Unchanged
Entity Name: Enrollment
Status: Unchanged
Entity Name: Enrollment
Status: Unchanged
Entity Name: Enrollment
Status: Unchanged

你可以看到,所有数据都只从数据库中检索,因此所有实体的状态都是未更改的。

现在让我们来看另一个简单的例子,我们将向数据库中添加一个报名信息并删除一个学生。以下是添加新报名信息和删除一个学生的代码。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Configuration.AutoDetectChangesEnabled = true;

         Enrollment enr = new Enrollment() { 
            StudentID = 1, CourseID = 3141 
         };

         Console.WriteLine("Adding New Enrollment");
         context.Enrollments.Add(enr);
         Console.WriteLine("Delete Student");

         var student = (from s in context.Students where s.ID == 
            23 select s).SingleOrDefault<Student>();

         context.Students.Remove(student);
         Console.WriteLine("");

         Console.WriteLine("Context tracking changes of {0} entity.", 
            context.ChangeTracker.Entries().Count());
         var entries = context.ChangeTracker.Entries();

         foreach (var entry in entries) {
            Console.WriteLine("Entity Name: {0}", entry.Entity.GetType().Name);
            Console.WriteLine("Status: {0}", entry.State);
         }

         Console.ReadKey();
      }
   }
}

编译并执行上述示例时,你将收到以下输出。

Adding New Enrollment
Delete Student
Context tracking changes of 2 entity.
Entity Name: Enrollment
Status: Added
Entity Name: Student
Status: Deleted

你现在可以看到,报名实体的状态设置为已添加,学生实体的状态设置为已删除,因为已添加了新的报名信息,并且从数据库中删除了一个学生。

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

实体框架 - 彩色实体

在 Entity Framework 中,彩色实体主要与更改设计器中实体的颜色有关,以便开发人员可以轻松识别 Visual Studio 设计器中相关的实体组。此功能首次在 Entity Framework 5.0 中引入。

  • 此功能与性能方面无关。

  • 当你的项目规模很大并且在一个 edmx 文件中有多个实体时,此功能非常有助于将实体分离到不同的模块中。

如果你正在使用 edmx 文件并在设计器中打开它,要更改颜色,请在设计窗口上选择一个实体。然后右键单击并选择属性。

Edmx File Designer Edmx File

在属性窗口中,选择填充颜色属性。

Color Property Window

使用有效的颜色名称(例如 Green)或有效的 RGB(255, 128, 128)指定颜色,或者也可以从颜色拾取器中选择。

Color

要一次更改多个实体的颜色,请选择多个实体并使用属性窗口更改所有实体的填充颜色。

Multiple Entities

你还可以通过选择以下任一选项来更改属性的格式:

  • 显示名称
  • 显示名称和类型

默认情况下,选择显示名称选项。要更改属性格式,请右键单击设计器窗口。

Property Format

选择标量属性格式→显示名称和类型。

Scalar Property Format

你现在可以看到类型也与名称一起显示。

Entity Framework - Code First 方法

实体框架提供三种创建实体模型的方法,每种方法都有其自身的优缺点。

  • 代码优先 (Code First)
  • 数据库优先 (Database First)
  • 模型优先 (Model First)

在本章中,我们将简要介绍代码优先方法。一些开发人员喜欢使用代码中的设计器,而另一些开发人员则宁愿只使用他们的代码。对于这些开发人员,实体框架提供了一种称为代码优先的建模工作流。

  • 代码优先建模工作流的目标数据库是不存在的,代码优先将创建它。

  • 如果你的数据库为空,然后 Code First 将向其中添加新表,它也可以使用。

  • 代码优先允许您使用 C# 或 VB.Net 类定义模型。

  • 可以使用类和属性上的特性或使用流畅的 API 选择性地执行其他配置。

Code First Approach

为什么选择代码优先?

  • 代码优先实际上是由一组拼图块组成的。首先是您的领域类。

  • 领域类与实体框架无关。它们只是您的业务领域的项目。

  • 然后,实体框架有一个上下文来管理这些类与数据库之间的交互。

  • 上下文不特定于代码优先。它是实体框架的一个特性。

  • 代码优先添加了一个模型构建器,它检查上下文正在管理的类,然后使用一组规则或约定来确定这些类和关系如何描述模型,以及该模型应如何映射到您的数据库。

  • 所有这些都在运行时发生。您将永远看不到此模型,它仅存在于内存中。

  • Code First 还可以使用该模型创建数据库(如果需要)。

  • 如果模型发生更改,它还可以更新数据库,使用称为代码优先迁移的功能。

环境设置

要开始使用 EF Code First 方法,你需要在你的系统上安装以下工具。

  • Visual Studio 2013(.net framework 4.5.2)或更高版本。
  • MS SQL Server 2012 或更高版本。
  • 通过 NuGet 包安装 Entity Framework。

通过 NuGet 包安装 EF

**步骤 1** - 首先,从文件→新建→项目…创建控制台应用程序。

**步骤 2** - 从左侧窗格中选择 Windows,从模板窗格中选择控制台应用程序。

Installing EF

**步骤 3** - 输入 EFCodeFirstDemo 作为名称,然后选择确定。

**步骤 4** - 在解决方案资源管理器中右键单击你的项目,然后选择管理 NuGet 包…

NuGet Package Manager

这将打开 NuGet 包管理器,并搜索 EntityFramework。这将搜索与 Entity Framework 相关的所有包。

**步骤 5** - 选择 EntityFramework 并单击安装。或者从工具菜单中单击 NuGet 包管理器,然后单击包管理器控制台。在包管理器控制台窗口中,输入以下命令:Install-Package EntityFramework。

Installed Entity Framework6

安装完成后,你将在输出窗口中看到以下消息“已成功将 'EntityFramework 6.1.2' 安装到 EFCodeFirstDemo”。

安装后,EntityFramework.dll 将包含在你的项目中,如下面的图像所示。

Entity Framework dll

现在你可以开始使用 Code First 方法了。

实体框架 - 第一个示例

让我们使用类定义一个非常简单的模型。我们只是在 Program.cs 文件中定义它们,但在实际应用程序中,你将把你的类拆分到单独的文件中,并可能拆分到单独的项目中。以下是我们将使用 Code First 方法创建的数据模型。

Model Using Classes

创建模型

使用以下代码为 Student 类在 Program.cs 文件中添加以下三个类。

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; }
}
  • ID 属性将成为与该类对应的数据库表的 PRIMARY KEY 列。

  • Enrollments 属性是一个导航属性。导航属性保存与该实体相关的其他实体。

  • 在这种情况下,Student 实体的 Enrollments 属性将保存与该 Student 实体相关的所有 Enrollment 实体。

  • 导航属性通常定义为 virtual,以便它们可以利用 Entity Framework 的某些功能,例如延迟加载。

  • 如果导航属性可以保存多个实体(如多对多或一对多关系),则其类型必须是列表,其中可以添加、删除和更新条目,例如 ICollection。

以下是 Course 类的实现。

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

Enrollments 属性是一个导航属性。Course 实体可以与任意数量的 Enrollment 实体相关联。

以下是 Enrollment 类和枚举的实现。

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; }
}
  • EnrollmentID 属性将是 PRIMARY KEY。

  • Grade 属性是一个枚举。Grade 类型声明后面的问号表示 Grade 属性是可为空的。

  • 空等级与零等级不同。空表示等级未知或尚未分配。

  • StudentID 和 CourseID 属性是外键,相应的导航属性是 Student 和 Course。

  • Enrollment 实体与一个 Student 和一个 Course 实体相关联,因此该属性只能保存单个 Student 和 Course 实体。

创建数据库上下文

协调给定数据模型的 Entity Framework 功能的主要类是数据库上下文类,它允许查询和保存数据。你可以通过从 DbContext 类派生并为模型中的每个类公开一个类型化的 DbSet 来创建此类。以下是 MyContext 类的实现,它派生自 DbContext 类。

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

以下是 Program.cs 文件中的完整代码。

using System.ComponentModel.DataAnnotations.Schema;
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; }
   }

}

以上代码是我们开始存储和检索数据所需的所有内容。让我们添加一些数据,然后检索它。以下是 main 方法中的代码。

static void Main(string[] args) {

   using (var context = new MyContext()) {
      // Create and save a new Students
      Console.WriteLine("Adding new students");

      var student = new Student {
         FirstMidName = "Alain", LastName = "Bomer", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      };

      context.Students.Add(student);
		
      var student1 = new Student {
         FirstMidName = "Mark", LastName = "Upston", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      };

      context.Students.Add(student1);
      context.SaveChanges();

      // Display all Students from the database
      var students = (from s in context.Students 
         orderby s.FirstMidName select s).ToList<Student>();

      Console.WriteLine("Retrieve all Students from the database:");

      foreach (var stdnt in students) {
         string name = stdnt.FirstMidName + " " + stdnt.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
      }
		
      Console.WriteLine("Press any key to exit...");
      Console.ReadKey();
   }
}

执行上述代码时,你将收到以下输出。

Adding new students
Retrieve all Students from the database:
ID: 1, Name: Alain Bomer
ID: 2, Name: Mark Upston
Press any key to exit...

现在想到的问题是,数据在哪里以及我们添加了一些数据然后从数据库中检索到的数据库在哪里。按照约定,DbContext 为你创建了一个数据库。

  • 如果本地 SQL Express 实例可用,则 Code First 会在该实例上创建数据库。

  • 如果 SQL Express 不可用,则 Code First 将尝试使用 LocalDb。

  • 数据库以派生上下文的完全限定名称命名。

在我们的例子中,SQL Express 实例可用,数据库名称为 EFCodeFirstDemo.MyContext,如下面的图片所示。

SQL Express Instance
  • 这些仅仅是默认约定,还有多种方法可以更改 Code First 使用的数据库。

  • 如上图所示,它创建了 Students、Courses 和 Enrollments 表,并且每个表都包含具有适当数据类型和长度的列。

  • 列名和数据类型也与相应域类的属性匹配。

数据库初始化

在上面的示例中,我们看到了 Code First 自动创建数据库,但是如果您想更改数据库和服务器的名称,让我们看看 Code First 在初始化数据库时如何决定数据库名称和服务器。请查看下图。

Database Initialization

您可以通过以下方式定义上下文类的基本构造函数。

  • 无参数
  • 数据库名称
  • 连接字符串名称

无参数

如果您在上下文类的基本构造函数中不指定任何参数,如上例所示,则实体框架将在您的本地 SQLEXPRESS 服务器上创建一个名为 {命名空间}.{上下文类名} 的数据库。

在上面的示例中,自动创建的数据库名称为 EFCodeFirstDemo.MyContext。如果您查看名称,您会发现 EFCodeFirstDemo 是命名空间,MyContext 是上下文类名,如下面的代码所示。

public class MyContext : DbContext {
   public MyContext() : base() {}

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

数据库名称

如果您在上下文类的基本构造函数中将数据库名称作为参数传递,则 Code First 将再次自动创建数据库,但这次名称将是在本地 SQLEXPRESS 数据库服务器上的基本构造函数中作为参数传递的名称。

在下面的代码中,MyContextDB 指定为基本构造函数中的参数。如果运行您的应用程序,则名为 MyContextDB 的数据库将在您的本地 SQL 服务器中创建。

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

连接字符串名称

这是一种简单的方法,可以告诉 DbContext 使用除 SQL Express 或 LocalDb 之外的数据库服务器。您可以选择将连接字符串放在 app.config 文件中。

  • 如果连接字符串的名称与您的上下文名称匹配(无论是否使用命名空间限定),则 DbContext 在使用无参数构造函数时将找到它。

  • 如果连接字符串名称与您的上下文名称不同,则可以通过将连接字符串名称传递给 DbContext 构造函数来告诉 DbContext 在 Code First 模式下使用此连接。

public class MyContext : DbContext {
   public MyContext() : base("name = MyContextDB") {}
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}
  • 在上面的代码中,上下文类连接字符串的代码片段指定为基本构造函数中的参数。

  • 连接字符串名称必须以“name=”开头,否则将将其视为数据库名称。

  • 此表单明确表明您期望在配置文件中找到连接字符串。如果找不到具有给定名称的连接字符串,则会抛出异常。

<connectionStrings>
   <add name = "MyContextDB"
      connectionString = "Data Source =.;Initial Catalog = EFMyContextDB;Integrated Security = true"
      providerName = "System.Data.SqlClient"/>
</connectionStrings>
  • app.config 中连接字符串中的数据库名称为 **EFMyContextDB**。CodeFirst 将创建一个新的 **EFMyContextDB** 数据库或使用本地 SQL Server 中现有的 **EFMyContextDB** 数据库。

域类

到目前为止,我们只是让 EF 使用其默认约定发现模型,但有时我们的类不遵循这些约定,我们需要能够执行进一步的配置。但是您可以通过配置域类来为 EF 提供所需的信息来覆盖这些约定。有两种选项可以配置您的域类 -

  • 数据注释
  • Fluent API

数据注释

DataAnnotations 用于配置您的类,这将突出显示最常用的配置。DataAnnotations 也被许多 .NET 应用程序理解,例如 ASP.NET MVC,这些应用程序允许这些应用程序利用相同的注释进行客户端验证。

以下是学生类中使用的数据注释。

public class Enrollment {

   [Key]
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }

   [ForeignKey("CourseID")]
   public virtual Course Course { get; set; }

   [ForeignKey("ID")]
   public virtual Student Student { get; set; }
}

Fluent API

大多数模型配置可以使用简单的数据注释来完成。Fluent API 是一种指定模型配置的高级方法,它涵盖了数据注释可以执行的所有操作,此外还有一些数据注释无法实现的更高级的配置。数据注释和 Fluent API 可以一起使用。

要访问 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; }
}

实体框架 - 数据注解

DataAnnotations 用于配置类,这将突出显示最常用的配置。DataAnnotations 也被许多 .NET 应用程序理解,例如 ASP.NET MVC,这些应用程序允许这些应用程序利用相同的注释进行客户端验证。DataAnnotation 属性覆盖默认的 CodeFirst 约定。

**System.ComponentModel.DataAnnotations** 包含以下影响列的可空性或大小的属性。

  • Key
  • Timestamp
  • ConcurrencyCheck
  • Required
  • MinLength
  • MaxLength
  • StringLength

**System.ComponentModel.DataAnnotations.Schema** 命名空间包含以下影响数据库模式的属性。

  • Table
  • Column
  • Index
  • ForeignKey
  • NotMapped
  • InverseProperty

Key

实体框架依赖于每个实体都具有一个键值,它用于跟踪实体。Code First 依赖的约定之一是它如何暗示每个 Code First 类中的哪个属性是键。

  • 约定是查找名为“Id”的属性或一个将类名和“Id”组合在一起的属性,例如“StudentId”。

  • 该属性将映射到数据库中的主键列。

  • Student、Course 和 Enrollment 类遵循此约定。

现在假设 Student 类使用 StdntID 而不是 ID。当 Code First 未找到与此约定匹配的属性时,它将抛出异常,因为实体框架要求您必须具有键属性。您可以使用键注释来指定哪个属性用作 EntityKey。

让我们看看以下 Student 类的代码,其中包含 StdntID,但它不遵循默认的 Code First 约定。因此,为了处理这个问题,添加了一个 Key 属性,这将使其成为主键。

public class Student {

   [Key]
   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; }
}

当您运行应用程序并在 SQL Server 资源管理器中查看数据库时,您会看到主键现在是 Students 表中的 StdntID。

Primary Key

实体框架也支持组合键。**组合键** 也是主键,由多个属性组成。例如,您有一个 DrivingLicense 类,其主键是 LicenseNumber 和 IssuingCountry 的组合。

public class DrivingLicense {

   [Key, Column(Order = 1)]
   public int LicenseNumber { get; set; }
   [Key, Column(Order = 2)]
   public string IssuingCountry { get; set; }
   public DateTime Issued { get; set; }
   public DateTime Expires { get; set; }
}

当您有组合键时,实体框架要求您定义键属性的顺序。您可以使用 Column 注释来指定顺序。

Column Annotation

Timestamp

Code First 将 Timestamp 属性与 ConcurrencyCheck 属性相同对待,但它还将确保 Code First 生成的数据库字段不可为空。

  • 更常见的是使用 rowversion 或 timestamp 字段进行并发检查。

  • 与其使用 ConcurrencyCheck 注释,不如使用更具体的 TimeStamp 注释,只要属性的类型是字节数组即可。

  • 在给定的类中,您只能有一个 timestamp 属性。

让我们看一个简单的例子,将 TimeStamp 属性添加到 Course 类中 -

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
   [Timestamp]
   public byte[] TStamp { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如上例所示,Timestamp 属性应用于 Course 类的 Byte[] 属性。因此,Code First 将在 Courses 表中创建一个名为 TStamp 的时间戳列。

ConcurrencyCheck

ConcurrencyCheck 注释允许您标记一个或多个属性,以便在用户编辑或删除实体时在数据库中用于并发检查。如果您一直在使用 EF 设计器,这与将属性的 ConcurrencyMode 设置为 Fixed 相一致。

让我们看一个 ConcurrencyCheck 如何工作的简单示例,将其添加到 Course 类中的 Title 属性中。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   public string Title { get; set; }
   public int Credits { get; set; }
   [Timestamp, DataType("timestamp")]
   public byte[] TimeStamp { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

在上面的 Course 类中,ConcurrencyCheck 属性应用于现有的 Title 属性。现在,Code First 将在更新命令中包含 Title 列以检查乐观并发,如下面的代码所示。

exec sp_executesql N'UPDATE [dbo].[Courses]
   SET [Title] = @0
   WHERE (([CourseID] = @1) AND ([Title] = @2))
   ',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'Maths',@1=1,@2=N'Calculus'
go

Required 注释

Required 注释告诉 EF 特定的属性是必需的。让我们看看以下 Student 类,其中 Required id 添加到 FirstMidName 属性中。Required 属性将强制 EF 确保该属性包含数据。

public class Student {

   [Key]
   public int StdntID { get; set; }

   [Required]
   public string LastName { get; set; }

   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如上例所示,Required 属性应用于 FirstMidName 和 LastName。因此,Code First 将在 Students 表中创建 NOT NULL FirstMidName 和 LastName 列,如下面的图片所示。

Not Null

MaxLength

MaxLength 属性允许您指定其他属性验证。它可以应用于域类的字符串或数组类型属性。EF Code First 将根据 MaxLength 属性中指定的内容设置列的大小。

让我们看看以下 Course 类,其中 MaxLength(24) 属性应用于 Title 属性。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   [MaxLength(24)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

当您运行上述应用程序时,Code First 将在 CourseId 表中创建一个名为 nvarchar(24) 的列 Title,如下面的图片所示。

nvarchar Column

当用户设置包含超过 24 个字符的 Title 时,EF 将抛出 EntityValidationError。

MinLength

MinLength 属性也允许您指定其他属性验证,就像您使用 MaxLength 一样。MinLength 属性也可以与 MaxLength 属性一起使用,如下面的代码所示。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   [MaxLength(24) , MinLength(5)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如果将 Title 属性的值设置为小于 MinLength 属性中指定的长度或大于 MaxLength 属性中指定的长度,则 EF 将抛出 EntityValidationError。

StringLength

StringLength 也允许您指定其他属性验证,如 MaxLength。唯一的区别是 StringLength 属性只能应用于域类的字符串类型属性。

public class Course {

   public int CourseID { get; set; }
   [StringLength (24)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

实体框架还验证 StringLength 属性的属性值。如果用户设置包含超过 24 个字符的 Title,则 EF 将抛出 EntityValidationError。

Table

默认的 Code First 约定创建类似于类名的表名。如果您让 Code First 创建数据库,并且还想更改它正在创建的表的名称。然后 -

  • 您可以将 Code First 与现有数据库一起使用。但并非总是类名与数据库中表名匹配的情况。

  • Table 属性覆盖此默认约定。

  • EF Code First 将为给定的域类创建具有 Table 属性中指定名称的表。

让我们看下面的例子,其中类名为 Student,根据约定,Code First 假设这将映射到名为 Students 的表。如果不是这种情况,您可以使用 Table 属性指定表名,如下面的代码所示。

[Table("StudentsInfo")]
public class Student {

   [Key]
   public int StdntID { get; set; }
   [Required]
   public string LastName { get; set; }
   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

现在您可以看到 Table 属性将表指定为 StudentsInfo。生成表时,您将看到表名为 StudentsInfo,如下面的图片所示。

StudentsInfo

您不仅可以指定表名,还可以使用 Table 属性指定表的模式,如下面的代码所示。

[Table("StudentsInfo", Schema = "Admin")] 
public class Student {

   [Key]
   public int StdntID { get; set; }
   [Required]
   public string LastName { get; set; }
   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以在上面的示例中看到,表使用 admin 模式指定。现在 Code First 将在 Admin 模式下创建 StudentsInfo 表,如下面的图片所示。

Admin Schema

Column

它与 Table 属性相同,但 Table 属性会覆盖表的行为,而 Column 属性会覆盖列的行为。默认的 Code First 约定会创建一个与属性名称类似的列名称。如果您让 Code First 创建数据库,并且还希望更改表中列的名称。然后 -

  • Column 属性会覆盖默认约定。

  • EF Code First 将为给定属性的 Column 属性中指定的名称创建一个列。

让我们看一下下面的示例,其中属性名为 FirstMidName,按照约定,Code First 假设这将映射到名为 FirstMidName 的列。

如果不是这种情况,您可以使用 Column 属性指定列的名称,如下面的代码所示。

public class Student {

   public int ID { get; set; }
   public string LastName { get; set; }
   [Column("FirstName")]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 Column 属性将列指定为 FirstName。生成表时,您将看到列名称 FirstName,如下面的图像所示。

FirstName

Index

Index 属性是在 Entity Framework 6.1 中引入的。如果您使用的是早期版本,则本节中的信息不适用。

  • 您可以使用 IndexAttribute 在一个或多个列上创建索引。

  • 将属性添加到一个或多个属性将导致 EF 在创建数据库时在数据库中创建相应的索引。

  • 在大多数情况下,索引可以使数据检索更快更高效。但是,在表或视图上加载过多的索引可能会对其他操作(例如插入或更新)的性能产生不利影响。

  • 索引是 Entity Framework 中的新功能,您可以通过减少从数据库查询数据所需的时间来提高 Code First 应用程序的性能。

  • 您可以使用 Index 属性向数据库添加索引,并覆盖默认的 Unique 和 Clustered 设置以获得最适合您方案的索引。

  • 默认情况下,索引将命名为 IX_<property name>

让我们看一下下面的代码,其中在 Course 类中为 Credits 添加了 Index 属性。

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

您可以看到 Index 属性已应用于 Credits 属性。生成表时,您将在索引中看到 IX_Credits。

IX Credits

默认情况下,索引是非唯一的,但您可以使用IsUnique命名参数指定索引应该是唯一的。以下示例介绍了一个唯一的索引,如下面的代码所示。

public class Course {
   public int CourseID { get; set; }
   [Index(IsUnique = true)]
	
   public string Title { get; set; }
   [Index]
	
   public int Credits { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

ForeignKey

Code First 约定将处理模型中最常见的关联,但有些情况下它需要帮助。例如,通过更改 Student 类中主键属性的名称,导致其与 Enrollment 类之间的关联出现问题。

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 {
   [Key]
   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; }
}

在生成数据库时,Code First 会看到 Enrollment 类中的 StudentID 属性,并根据约定(它与类名加“ID”匹配)将其识别为对 Student 类的外键。但是,Student 类中没有 StudentID 属性,但 Student 类中是 StdntID 属性。

解决此问题的方案是在 Enrollment 中创建一个导航属性,并使用 ForeignKey DataAnnotation 来帮助 Code First 了解如何在两个类之间建立关联,如下面的代码所示。

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; }
   [ForeignKey("StudentID")]
	
   public virtual Student Student { get; set; }
}

您现在可以看到 ForeignKey 属性已应用于导航属性。

ForeignKey Attribute

NotMapped

根据 Code First 的默认约定,每个支持的数据类型的属性(包括 getter 和 setter)都将在数据库中表示。但这并非始终适用于您的应用程序。NotMapped 属性会覆盖此默认约定。例如,您可能在 Student 类中有一个名为 FatherName 的属性,但不需要存储它。您可以将 NotMapped 属性应用于您不想在数据库中创建列的 FatherName 属性,如下面的代码所示。

public class Student {
   [Key]
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
	
   public DateTime EnrollmentDate { get; set; }
   [NotMapped]

   public int FatherName { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 NotMapped 属性已应用于 FatherName 属性。生成表时,您将看到数据库中不会创建 FatherName 列,但它存在于 Student 类中。

NotMapped Attribute

Code First 不会为没有 getter 或 setter 的属性创建列,如下面的 Student 类 Address 和 Age 属性示例所示。

InverseProperty

当您在类之间有多个关联时,使用 InverseProperty。在 Enrollment 类中,您可能希望跟踪谁注册了 Current Course 和 Previous Course。让我们为 Enrollment 类添加两个导航属性。

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 CurrCourse { get; set; }
   public virtual Course PrevCourse { get; set; }
   public virtual Student Student { get; set; }
}

类似地,您还需要在这些属性引用的 Course 类中添加。Course 类具有返回 Enrollment 类的导航属性,其中包含所有当前和以前的注册信息。

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]

   public int Credits { get; set; }
   public virtual ICollection<Enrollment> CurrEnrollments { get; set; }
   public virtual ICollection<Enrollment> PrevEnrollments { get; set; }
}

如果外键属性未包含在特定类中,则 Code First 会创建{Class Name}_{Primary Key}外键列,如上面的类所示。生成数据库时,您将看到以下外键。

Foreign Keys

如您所见,Code First 无法自行匹配两个类中的属性。Enrollments 的数据库表应该有一个 CurrCourse 的外键和一个 PrevCourse 的外键,但 Code First 将创建四个外键属性,即

  • CurrCourse _CourseID
  • PrevCourse _CourseID
  • Course_CourseID,以及
  • Course_CourseID1

要解决这些问题,您可以使用 InverseProperty 注释来指定属性的对齐方式。

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]

   public int Credits { get; set; }
   [InverseProperty("CurrCourse")]

   public virtual ICollection<Enrollment> CurrEnrollments { get; set; }
   [InverseProperty("PrevCourse")]

   public virtual ICollection<Enrollment> PrevEnrollments { get; set; }
}

如您所见,InverseProperty 属性在上面的 Course 类中通过指定它属于 Enrollment 类的哪个引用属性来应用。现在,Code First 将生成一个数据库,并在 Enrollments 表中仅创建两个外键列,如下面的图像所示。

Foreign Key Columns

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

实体框架 - Fluent API

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

  • Fluent API 是配置域类的另一种方法。

  • 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) 的方法称为 Code First。

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

序号 方法名称和说明
1

ComplexType<TComplexType>

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

2

Entity<TEntityType>

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

3

HasKey<TKey>

配置此实体类型的 primary key 属性。

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 支持的主要映射类型 -

  • 实体映射
  • 属性映射

实体映射

实体映射只是一些简单的映射,将影响 Entity Framework 对类如何映射到数据库的理解。所有这些我们都在数据注释中讨论过,在这里我们将看到如何使用 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

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

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

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

然后要显式设置属性为 primary key,您可以使用 HasKey 方法,如下面的代码所示 -

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

配置列

在 Entity Framework 中,默认情况下,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 属性

在下面的示例中,课程标题属性的长度不能超过 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 属性

在下面的示例中,课程标题属性是必需的,因此使用 IsRequired 方法创建 NotNull 列。类似地,学生入学日期是可选的,因此我们将使用 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 类来定义模型。默认情况下,Entity Framework 使用 Code First 约定将您的类映射到数据库模式。

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

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

  • 其中一些可以在数据注释中使用,您甚至可以使用 Fluent API 应用一些更复杂的配置。

配置一对一关系

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

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

  • 在一对一关系中,primary key 还会充当外键,并且两个表都没有单独的外键列。

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

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

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

如您在上面的代码中看到的,Key 和 ForeignKey 属性用于 StudentLogIn 类中的 ID 属性,以便将其标记为主键以及外键。

要使用 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
}

在大多数情况下,Entity Framework 可以推断出关系中哪个类型是依赖类型,哪个类型是主体类型。但是,当关系的两端都必需或两端都可选时,Entity Framework 无法识别依赖项和主体。当关系的两端都必需时,您可以使用 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 时,都必须将 Student 与 Enrollment 实体一起分配。为了处理这种情况,我们需要使用 HasOptional 方法而不是 HasRequired 方法。

配置多对多关系

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

  • 您可以通过定义一个第三个表(称为连接表)来创建这种关系,该表的 primary key 由表 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; }
}

要配置 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

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

实体框架 - 种子数据库

在 Entity Framework 中,Seed 引入于 EF 4.1,并与数据库初始化程序一起使用。**Seed 方法** 的基本思想是将数据初始化到由 Code First 创建或由 Migrations 演变的数据库中。这些数据通常是测试数据,但也可能是参考数据,例如已知学生、课程等的列表。初始化数据时,它执行以下操作:

  • 检查目标数据库是否存在。
  • 如果存在,则将当前 Code First 模型与数据库元数据中存储的模型进行比较。
  • 如果当前模型与数据库中的模型不匹配,则删除数据库。
  • 如果数据库已删除或最初不存在,则创建数据库。
  • 如果创建了数据库,则调用初始化程序 Seed 方法。

Seed 方法将数据库上下文对象作为输入参数,方法中的代码使用该对象将新实体添加到数据库中。要将数据播种到数据库中,您需要覆盖 Seed 方法。让我们看一下下面的示例,其中一些默认数据在内部类中初始化到数据库中。

private class UniDBInitializer<T> : DropCreateDatabaseAlways<MyContext> {

   protected override void Seed(MyContext context) {

      IList<Student> students = new List<Student>();

      students.Add(new Student() {
         FirstMidName = "Andrew", 
         LastName = "Peters", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      });

      students.Add(new Student() {
         FirstMidName = "Brice", 
         LastName = "Lambson", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      });

      students.Add(new Student() {
         FirstMidName = "Rowan", 
         LastName = "Miller", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      });

      foreach (Student student in students)
      context.Students.Add(student);
      base.Seed(context);
   }
}

在上面的代码中,初始化了 student 表。您需要在上下文类中设置此 DB 初始化程序类,如下面的代码所示。

public MyContext() : base("name=MyContextDB") {
   Database.SetInitializer<MyContext>(new UniDBInitializer<MyContext>());
}

以下是 MyContext 类的完整类实现,它还包含 DB 初始化程序类。

public class MyContext : DbContext {

   public MyContext() : base("name=MyContextDB") {
      Database.SetInitializer<MyContext>(new UniDBInitializer<MyContext>());
   }

   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
	
   private class UniDBInitializer<T> : DropCreateDatabaseAlways<MyContext> {

      protected override void Seed(MyContext context) {

         IList<Student> students = new List<Student>();
			
         students.Add(new Student() {
            FirstMidName = "Andrew", 
            LastName = "Peters", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString()) 
         });

         students.Add(new Student() {
            FirstMidName = "Brice", 
            LastName = "Lambson", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         students.Add(new Student() {
            FirstMidName = "Rowan", 
            LastName = "Miller", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         foreach (Student student in students)
         context.Students.Add(student);
         base.Seed(context);
      }
   } 
}

编译并执行上述示例后,您可以在数据库中看到数据,如下面的图像所示。

Data In Database

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

Entity Framework - Code First 迁移

Entity Framework 4.3 包含一个新的 Code First 迁移功能,允许您随着时间的推移随着模型的变化而逐步演变数据库模式。对于大多数开发人员来说,这比 4.1 和 4.2 版本中数据库初始化程序选项有了很大的改进,这些选项要求您在模型更改时手动更新数据库或删除并重新创建数据库。

  • 在 Entity Framework 4.3 之前,如果您已经在数据库中拥有数据(种子数据除外)或现有的存储过程、触发器等,这些策略将删除整个数据库并重新创建它,因此您将丢失数据和其他数据库对象。

  • 使用迁移,它将在您的模型更改时自动更新数据库模式,而不会丢失任何现有数据或其他数据库对象。

  • 它使用一个名为 MigrateDatabaseToLatestVersion 的新数据库初始化程序。

迁移有两种类型:

  • 自动迁移
  • 基于代码的迁移

自动迁移

自动迁移首次引入于 Entity Framework 4.3。在自动迁移中,您无需在代码文件中手动处理数据库迁移。例如,对于每个更改,您还需要更改域类。但是,使用自动迁移,您只需在程序包管理器控制台中运行一个命令即可完成此操作。

让我们看一下自动迁移的分步过程。

当您使用 Code First 方法时,您的应用程序没有数据库。

在此示例中,我们将从 3 个基本类开始,例如 Student、Course 和 Enrollment,如下面的代码所示。

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; }
   [Index]
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

以下是上下文类。

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

在运行应用程序之前,您需要启用自动迁移。

**步骤 1** - 从“工具”→“NuGet 包管理器”→“程序包管理器控制台”中打开程序包管理器控制台。

**步骤 2** - 要启用自动迁移,请在程序包管理器控制台中运行以下命令。

PM> enable-migrations -EnableAutomaticMigrations:$true
Command

**步骤 3** - 命令成功运行后,它会在项目的 Migration 文件夹中创建一个内部密封的 Configuration 类,如下面的代码所示。

namespace EFCodeFirstDemo.Migrations {

   using System;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;
	
   internal sealed class Configuration : DbMigrationsConfiguration<EFCodeFirstDemo.MyContext> {

      public Configuration() {
         AutomaticMigrationsEnabled = true;
         ContextKey = "EFCodeFirstDemo.MyContext";
      }

      protected override void Seed(EFCodeFirstDemo.MyContext context) {

         //  This method will be called after migrating to the latest version.
         //  You can use the DbSet<T>.AddOrUpdate() helper extension method
         //  to avoid creating duplicate seed data. E.g.

         //  context.People.AddOrUpdate(
            //  p ⇒ p.FullName, 
            //  new Person { FullName = "Andrew Peters" }, 
            //  new Person { FullName = "Brice Lambson" }, 
            //  new Person { FullName = "Rowan Miller" }
         //  );
      }
   }
}

**步骤 4** - 使用新的 DB 初始化策略 MigrateDatabaseToLatestVersion 在上下文类中设置数据库初始化程序。

public class MyContext : DbContext {

   public MyContext() : base("MyContextDB") {
      Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, 
         EFCodeFirstDemo.Migrations.Configuration>("MyContextDB"));
   }

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

}

**步骤 5** - 您已设置自动迁移。当您执行应用程序时,当您更改模型时,它将自动处理迁移。

Migration

**步骤 6** - 如您所见,一个系统表 __MigrationHistory 也与其他表一起在您的数据库中创建。在 __MigrationHistory 中,自动迁移维护数据库更改的历史记录。

**步骤 7** - 当您将另一个实体类作为域类添加并执行应用程序时,它将在您的数据库中创建该表。让我们添加以下 StudentLogIn 类。

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

**步骤 8** - 不要忘记在您的上下文类中为上述类添加 DBSet,如下面的代码所示。

public virtual DbSet<StudentLogIn> StudentsLogIn { get; set; }

**步骤 9** - 再次运行您的应用程序,您将看到 StudentsLogIn 表已添加到您的数据库中。

StudentsLogIn

上述针对自动迁移提到的步骤仅适用于您的实体。例如,添加另一个实体类或删除现有实体类,它将成功迁移。但是,如果您向实体类添加或删除任何属性,则它将引发异常。

步骤 10 − 要处理属性迁移,您需要在配置类构造函数中设置 AutomaticMigrationDataLossAllowed = true。

public Configuration() {
   AutomaticMigrationsEnabled = true;
   AutomaticMigrationDataLossAllowed = true;
   ContextKey = "EFCodeFirstDemo.MyContext";
}

基于代码的迁移

当您开发一个新的应用程序时,您的数据模型会频繁更改,并且每次模型更改时,它都会与数据库不同步。您已将 Entity Framework 配置为每次更改数据模型时自动删除并重新创建数据库。基于代码的迁移在您希望对迁移有更多控制权时非常有用。

  • 当您添加、删除或更改实体类或更改您的 DbContext 类时,下次运行应用程序时,它会自动删除您现有的数据库,创建一个与模型匹配的新数据库,并使用测试数据填充它。

  • Code First 迁移功能通过启用 Code First 更新数据库模式而不是删除和重新创建数据库来解决此问题。要部署应用程序,您必须启用迁移。

以下是迁移数据库中更改的基本规则:

  • 启用迁移
  • 添加迁移
  • 更新数据库

让我们看一下以下基于代码的迁移的分步过程。

当您使用 Code First 方法时,您的应用程序没有数据库。

在本例中,我们将使用我们 3 个基本类(如 Student、Course 和 Enrollment)重新开始,如下面的代码所示。

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; }
   [Index]
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

以下是上下文类。

public class MyContext : DbContext {

   public MyContext() : base("MyContextDB") {
      Database.SetInitializer(new MigrateDatabaseToLatestVersion<
         MyContext, EFCodeFirstDemo.Migrations.Configuration>("MyContextDB"));
   }

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

}

步骤 1 − 在运行应用程序之前,您需要启用迁移。

步骤 2 − 从“工具”→“NuGet 包管理器”→“包管理器控制台”打开包管理器控制台。

步骤 3 − 迁移已启用,现在通过执行以下命令在您的应用程序中添加迁移。

PM> add-migration "UniDB Schema"

步骤 4 − 当命令成功执行后,您将看到在 Migration 文件夹中创建了一个新文件,其名称为传递给命令的参数,并在其前面添加了时间戳前缀,如下面的图像所示。

TimeStamp Prefix

步骤 5 − 您可以使用“update-database”命令创建或更新数据库。

PM> Update-Database -Verbose

“-Verbose”标志指定在控制台中显示应用于目标数据库的 SQL 语句。

步骤 6 − 让我们在 Student 类中再添加一个属性“Age”,然后执行更新语句。

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

}

当您执行 PM → Update-Database –Verbose 时,如果命令成功执行,您将看到新列 Age 已添加到您的数据库中。

New Column Age.

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

Entity Framework - 多个 DbContext

在本章中,我们将学习如何在应用程序中存在多个 DbContext 类时将更改迁移到数据库。

  • 多个 DbContext 最初在 Entity Framework 6.0 中引入。
  • 多个上下文类可能属于单个数据库或两个不同的数据库。

在我们的示例中,我们将为同一个数据库定义两个上下文类。在以下代码中,有两个用于 Student 和 Teacher 的 DbContext 类。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
}

public class MyStudentContext : DbContext {
   public MyStudentContext() : base("UniContextDB") {}
   public virtual DbSet<Student> Students { get; set; }
}

public class Teacher {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime HireDate { get; set; }
}

public class MyTeacherContext : DbContext {
   public MyTeacherContext() : base("UniContextDB") {}
   public virtual DbSet<Teacher> Teachers { get; set; }
}

如您在上面的代码中看到的,有两个名为“Student”和“Teacher”的模型。每个模型都与特定的对应上下文类相关联,即 Student 与 MyStudentContext 相关联,Teacher 与 MyTeacherContext 相关联。

以下是在项目中存在多个上下文类时迁移数据库更改的基本规则。

  • enable-migrations -ContextTypeName <DbContext-Name-with-Namespaces> MigrationsDirectory:<Migrations-Directory-Name>

  • Add-Migration -configuration <DbContext-Migrations-Configuration-Class-withNamespaces> <Migrations-Name>

  • Update-Database -configuration <DbContext-Migrations-Configuration-Class-withNamespaces> -Verbose

让我们通过在包管理器控制台中执行以下命令来为 MyStudentContext 启用迁移。

PM→ enable-migrations -ContextTypeName:EFCodeFirstDemo.MyStudentContext
Package Manager Console

执行后,我们将模型添加到迁移历史记录中,为此,我们必须在同一个控制台中触发 add-migration 命令。

PM→ add-migration -configuration EFCodeFirstDemo.Migrations.Configuration Initial

现在让我们在数据库中的 Students 和 Teachers 表中添加一些数据。

static void Main(string[] args) {

   using (var context = new MyStudentContext()) {
	
      //// Create and save a new Students
      Console.WriteLine("Adding new students");

      var student = new Student {
         FirstMidName = "Alain", 
         LastName = "Bomer", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 24
      };

      context.Students.Add(student);

      var student1 = new Student {
         FirstMidName = "Mark",
         LastName = "Upston", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 30
      };

      context.Students.Add(student1);
      context.SaveChanges();
      // Display all Students from the database
      var students = (from s in context.Students orderby s.FirstMidName
         select s).ToList<Student>();
		
      Console.WriteLine("Retrieve all Students from the database:");

      foreach (var stdnt in students) {
         string name = stdnt.FirstMidName + " " + stdnt.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
      }

      Console.WriteLine("Press any key to exit...");
      Console.ReadKey();
   }

   using (var context = new MyTeacherContext()) {

      //// Create and save a new Teachers
      Console.WriteLine("Adding new teachers");

      var student = new Teacher {
         FirstMidName = "Alain", 
         LastName = "Bomer", 
         HireDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 24
      };

      context.Teachers.Add(student);

      var student1 = new Teacher {
         FirstMidName = "Mark", 
         LastName = "Upston", 
         HireDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 30
      };

      context.Teachers.Add(student1);
      context.SaveChanges();
  
      // Display all Teachers from the database
      var teachers = (from t in context.Teachers orderby t.FirstMidName
         select t).ToList<Teacher>();
		
      Console.WriteLine("Retrieve all teachers from the database:");

      foreach (var teacher in teachers) {
         string name = teacher.FirstMidName + " " + teacher.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", teacher.ID, name);
      }

      Console.WriteLine("Press any key to exit...");
      Console.ReadKey();
   }
}

执行上述代码后,您将看到为两个不同的模型创建了两个不同的表,如下面的图像所示。

Executed Code

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

Entity Framework - 嵌套实体类型

在 Entity Framework 6 之前,Entity Framework 无法识别嵌套在其他实体或复杂类型中的实体或复杂类型。当 Entity Framework 生成模型时,嵌套类型会消失。

让我们来看一个简单的示例,其中我们有三个实体 Student、Course 和 Enrollment 的基本模型。

  • 让我们添加一个属性 Identity,它是一个 Person 类型。Person 是另一个实体,包含 BirthDate 和 FatherName 属性。

  • 在 Entity Framework 术语中,因为它没有标识并且是实体的一部分,所以它是 Entity Framework 复杂类型,并且我们实际上从 Entity Framework 的第一个版本开始就支持复杂类型。

  • Person 类型没有像以下代码所示那样嵌套。

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

public class Person {

   public Person(string fatherName, DateTime birthDate) {
      FatherName = fatherName;
      BirthDate = birthDate;
   }
	
   public string FatherName { get; set; }
   public DateTime BirthDate { get; set; }
}

Entity Framework 将知道如何在以前版本中使用它时持久化 Person 类型。

通过使用 Entity Framework Power Tool,我们将看到 Entity Framework 如何解释模型。右键单击 Program.cs 文件并选择 Entity Framework → 查看实体数据模型(只读)

Framework Power Tool

现在您将看到 Identity 属性在 Student 类中定义。

Identity Property

如果此 Person 类不会被任何其他实体使用,那么我们可以将其嵌套在 Student 类中,但是此较早版本的 Entity Framework 无法识别嵌套类型。

在旧版本中,您重新生成模型,不仅无法识别该类型,而且由于该类型不存在,该属性也不存在,因此 Entity Framework 根本不会持久化 Person 类型。

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

   public class Person {

      public Person(string fatherName, DateTime birthDate) {
         FatherName = fatherName;
         BirthDate = birthDate;
      }

      public string FatherName { get; set; }
      public DateTime BirthDate { get; set; }
   }
}

使用 Entity Framework 6,嵌套实体和复杂类型将被识别。在上面的代码中,您可以看到 Person 嵌套在 Student 类中。

当您使用 Entity Framework Power Tool 显示 Entity Framework 如何解释模型时,这次有一个真正的 Identity 属性和 Person 复杂类型。因此,Entity Framework 将持久化该数据。

Nested Entity Type

现在您可以看到 Identity 是一个嵌套实体类型,这在 Entity Framework 6 之前不受支持。

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

广告