实体框架 – 快速指南

实体框架 – 快速指南


实体框架 – 概述

什么是实体框架?

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

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

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

  • ORM 还有助于跟踪对这些对象的更改,并且在收到指示时,它还会为您将这些更改保存回数据库。

为什么是实体框架?

实体框架是一个 ORM,ORM 旨在通过减少保留应用程序中使用的数据的冗余任务来提高开发人员的生产力。

  • Entity Framework 可以生成读取或写入数据库中数据所需的数据库命令,并为您执行。

  • 如果您要进行查询,则可以使用 LINQ to entity 表达对域对象的查询。

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

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

典型的 ORM

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

EF 运行时元数据

  • 实体框架是 Microsoft 为新应用程序推荐的数据访问技术。

  • ADO.NET 似乎直接指的是数据集和数据表的技术。

  • 实体框架是进行所有前瞻性投资的地方,多年来一直如此。

  • Microsoft 建议您通过 ADO.NET 或 LINQ to SQL 使用实体框架进行所有新开发。

概念模型

对于习惯于以数据库为中心的开发的开发人员来说,Entity Framework 的最大转变是它让您可以专注于您的业务领域。您希望您的应用程序在不受数据库功能限制的情况下做什么?

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

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

  • 您可以使用可视化设计器来定义您的概念模型,然后可以生成您最终将在应用程序中使用的类。

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

概念模型

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

特征

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

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

实体框架 – 架构

实体框架的架构,自下而上,包括以下内容 –

数据提供者

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

它通过命令树将 LINQ 等常见 SQL 语言转换为原生 SQL 表达式,并针对特定的 DBMS 系统执行它。

实体客户端

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

实体数据模型

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

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

  • 映射层是在与实际关系和在逻辑层中定义的表的概念层所限定的实体和关系映射的XML文件。

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

对象服务

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

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

  • 它是Entity Framework的ORM层,将数据结果表示给实体的对象实例。

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

实体框架 – 环境设置

Entity Framework 6 有哪些新功能?

Framework 有一个复杂的 API,可让您对从建模到运行时行为的所有内容进行精细控制。Entity Framework 5 的一部分位于 .NET 内部。它的另一部分位于使用 NuGet 分发的附加程序集中。

  • Entity Framework 的核心功能内置于 .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 结合使用时,您才能获得它们。

框架 6

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

实体框架 6

这意味着 –

  • 所有实体框架都位于这个由 NuGet 分发的程序集中

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

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

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

  • Visual Studio 2013 或以上
  • SQL Server 2012 或以上
  • 来自 NuGet 包的实体框架更新

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

安装

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

Visual Studio 安装程序

步骤 2 – 单击安装按钮,它将开始安装过程。

安装过程

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

设置完成

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

视觉工作室

第 5 步– 完成所有操作后,您将看到 Visual Studio 的主窗口。

主窗口

让我们从 File → New → Project 创建一个新项目

新项目

步骤 1 – 选择控制台应用程序并单击确定按钮。

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

控制台应用程序

Step 3 – 如上图所示,选择 Manage NuGet Packages,这将在 Visual Studio 中打开以下窗口。

视觉工作室 1

第 4 步– 搜索实体框架并按安装按钮安装最新版本。

预习

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

输出窗口

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

实体框架 – 数据库设置

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

  • 学生
  • 课程
  • 注册

数据库

每当使用术语数据库时,我们都会直接想到一件事,那就是具有某种关系的不同类型的表。表之间存在三种关系,不同表之间的关系取决于相关列的定义方式。

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

一对多关系

一对多关系是最常见的关系类型。在这种关系中,A表中的一行可以在B表中有很多匹配的行,而B表中的一行在A表中只能有一个匹配行。例如,在上图中,Student和Enrollment表中只有一个一对多的关系,每个学生可能有多个注册,但每个注册只属于一个学生。

多对多关系

在多对多关系中,表 A 中的一行在表 B 中可以有许多匹配的行,反之亦然。您可以通过定义第三个表(称为联结表)来创建这种关系,该表的主键由表 A 和表 B 中的外键组成。例如,Student 和 Course 表具有由以下定义的多对多关系从这些表中的每一个到 Enrollment 表的一对多关系。

一对一关系

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

这种类型的关系并不常见,因为以这种方式相关的大多数信息都是一体表。您可以使用一对一的关系 –

  • 将一个包含许多列的表格分开。
  • 出于安全原因隔离表的一部分。
  • 存储时间很短并且可以通过简单地删除表轻松删除的数据。
  • 存储仅适用于主表子集的信息。

实体框架 – 数据模型

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

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

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

存储模式模型

存储模型也称为存储架构定义层 (SSDL),表示后端数据存储的示意图。

电火花加工

概念模型

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

映射模型

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

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

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

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

  • Edmx 文件包含实体框架元数据工件。

模式定义语言

ADO.NET 实体框架使用称为架构定义语言 (SDL) 的基于 XML 的数据定义语言来定义 EDM 架构。

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

  • 定义原始值和名称映射的枚举也被视为简单类型。

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

  • 复杂类型是从其他类型的聚合中创建的。这些类型的属性集合定义了一个实体类型。

数据模型主要有三个关键概念来描述数据结构 –

  • 实体类型
  • 关联类型
  • 财产

实体类型

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

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

  • 实体代表特定对象,例如特定学生或注册。

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

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

实体类型

关联类型

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

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

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

  • 关联端多重性的值可以是一 (1)、零或一 (0..1) 或多 (*)。

  • 关联一端的实体可以通过导航属性访问,或者如果它们在实体类型上公开,则可以通过外键访问。

财产

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

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

实体框架 – DbContext

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

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

负责将数据作为对象进行交互的主要类是 System.Data.Entity.DbContext。DbContext API 不作为 .NET Framework 的一部分发布。为了更加灵活和频繁地向 Code First 和 DbContext API 发布新功能,实体框架团队通过 Microsoft 的 NuGet 分发功能分发了 EntityFramework.dll。

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

  • 称为库包管理器的 Visual Studio 扩展提供了一种将适当的程序集从 Web 提取到您的项目中的简单方法。

数据库上下文

  • DbContext API 主要旨在简化您与实体框架的交互。

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

  • 在以前版本的实体框架中,发现和编码这些任务通常很复杂。

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

定义 DbContext 派生类

推荐的使用上下文的方法是定义一个派生自 DbContext 的类,并公开表示上下文中指定实体集合的 DbSet 属性。如果您正在使用 EF 设计器,则会为您生成上下文。如果您使用 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)中都很有用且简单。

查询

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

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

添加新实体

使用实体框架添加新对象就像构造对象的新实例并使用 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();

      }
   }
}

删除现有实体

要使用实体框架删除实体,请使用 DbSet 上的 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();
   }
}

实体框架 – 类型

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

  • POCO实体
  • 动态代理

POCO实体

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

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

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

  • 您可以使用 POCO 模板从概念模型生成不考虑持久性的实体类型。

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

概念实体模型

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

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

设计器窗口

步骤 2 – 选择添加代码生成项…

代码生成

步骤 3 – 选择 EF 6.x DbContext Generator,写入名称,然后单击添加按钮。

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

解决方案资源管理器

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

产生

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

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

   }
}

为实体模型中的课程和注册表生成了类似的类。

动态代理

创建 POCO 实体类型的实例时,实体框架通常会创建动态生成的派生类型的实例,作为实体的代理。IT也可以说它是一个运行时代理类,就像POCO实体的包装类。

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

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

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

如果您希望实体框架支持相关对象的延迟加载并跟踪 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) 是一列或列组合,用于在两个表中的数据之间建立和强制建立链接。下图包含三个表。

  • 学生
  • 课程
  • 注册

关系型数据库

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

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

一对多关系

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

  • 在这种关系中,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 中有许多匹配的行,反之亦然。

  • 您可以通过定义第三个表(称为联结表)来创建这种关系,其主键由表 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 中最多只能有一个匹配行,反之亦然。

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

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

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

  • 将一个包含许多列的表格分开。
  • 出于安全原因隔离表的一部分。
  • 存储时间很短并且可以通过简单地删除表轻松删除的数据。
  • 存储仅适用于主表子集的信息。

下面的代码是添加另一个类名 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 时,上下文生命周期是一个非常重要的决定。

  • 上下文就像实体缓存一样执行,因此这意味着它保存对所有加载实体的引用,这些实体的内存消耗可能会增长得非常快,也可能导致内存泄漏。

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

数据工作流

实体生命周期

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

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

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

  • 已修改:实体已被修改。

  • 不变:实体没有被修改。

  • 分离:未跟踪实体。

实体生命周期中的状态变化

有时实体的状态由上下文自动设置,但也可以由开发人员手动修改。尽管从一种状态切换到另一种状态的所有组合都是可能的,但其中一些是没有意义的。例如,将实体添加已删除状态,反之亦然。

让我们讨论不同的状态。

不变的状态

  • 当实体未更改时,它绑定到上下文但尚未修改。

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

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

  • 上下文无法跟踪对其未引用的对象的更改,因此当它们被附加时,它假定它们是未更改的。

分离状态

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

  • 即使您在上下文的 using 块内实例化实体也是如此。

  • Detached 甚至是禁用跟踪时从数据库中检索到的实体的状态。

  • 当实体分离时,它不会绑定到上下文,因此不会跟踪其状态。

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

  • 因为没有上下文跟踪它,所以它对实体框架没有意义。

添加状态

  • 当实体处于已添加状态时,您几乎没有选择。实际上,您只能将其与上下文分离。

  • 自然地,即使您修改了某些属性,状态仍然是已添加,因为将其移动到已修改、未更改或已删除是没有意义的。

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

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

添加状态

修改状态

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

  • 实体进入 Modified 状态后,可以移动到 Detached 或 Deleted 状态,但即使手动恢复原始值也无法回滚到 Unchanged 状态。

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

删除状态

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

  • 这是限制性最强的状态,因为从该状态更改为除 Detached 之外的任何其他值毫无意义。

使用的语句,如果你想,上下文控制在该块结束时设置的所有资源。当您使用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 应用程序中,我们处理的请求非常短,但包含所有服务器事务,因此它们是上下文存在的适当持续时间。

桌面应用程序

  • 对于桌面应用程序,如 Win Forms/WPF 等,每个表单/对话框/页面都使用上下文。

  • 由于我们不想将上下文作为我们应用程序的单例,我们将在从一种形式移动到另一种形式时处理它。

  • 通过这种方式,我们将获得很多上下文的能力,并且不会受到长时间运行的上下文的影响。

实体框架 – 代码优先方法

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

  • 代码优先
  • 数据库优先
  • 模特第一

在本章中,我们将简要描述代码优先的方法。一些开发人员更喜欢在代码中使用设计器,而另一些开发人员则更喜欢使用他们的代码。对于这些开发人员,Entity Framework 有一个称为 Code First 的建模工作流。

  • Code First 建模工作流针对不存在的数据库,Code First 将创建它。

  • 如果您有一个空数据库,那么它也可以使用,然后 Code First 也会添加新表。

  • Code First 允许您使用 C# 或 VB.Net 类定义模型。

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

代码优先方法

为什么代码优先?

  • Code First 实际上是由一组拼图组成的。首先是您的域类。

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

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

  • 上下文并非特定于 Code First。这是一个实体框架功能。

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

  • 所有这些都发生在运行时。你永远不会看到这个模型,它只是在记忆中。

  • 如果需要,Code First 可以使用该模型创建数据库。

  • 如果模型发生变化,它还可以使用称为 Code First Migrations 的功能更新数据库。

实体框架 – 模型优先方法

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

  • 当您开始一个数据库还不存在的新项目时,Model First 非常适合。

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

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

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

以下是使用 Model First 方法创建新控制台项目的简单示例。

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

控制台项目

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

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

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

模型

将打开以下对话框。

添新

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

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

模型向导对话框

步骤 7 – 选择 Empty EF Designer 模型并单击下一步按钮。实体框架设计器打开时带有一个空白模型。现在我们可以开始向模型添加实体、属性和关联。

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

设计面

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

添加新实体

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

实体对话框

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

学生

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

新实体

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

标量属性

步骤 13 – 按照上述所有步骤添加另外两个实体课程和注册,并添加一些标量属性,如以下步骤所示。

视觉设计师

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

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

添加关联

第 16 步– 使关系的一端指向具有多重性的 Student,而另一端指向具有多重性的 Enrollment。

一的多重性

第 17 步– 这意味着一个学生有很多注册并且注册属于一个学生。

步骤 18 – 确保选中将外键属性添加到“发布”实体框并单击确定。

第 19 步– 同样,在“课程”和“注册”之间再添加一个关联。

课程与招生

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

数据模型

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

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

来自模型的数据库

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

生成数据库

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

连接属性

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

服务器和数据库

步骤 5 – 单击下一步。

服务器和数据库 1

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

DDL 脚本

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

连接到数据库

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

成功执行

步骤 9 – 转到服务器资源管理器,您将看到使用指定的三个表创建数据库。

数据库已创建

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

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

英孚设计师

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

对话框打开

第 2 步– 在中间窗格中选择 EF 6.x DbContext Generator,然后在 Name 字段中输入 ModelFirstDemoModel。

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

模板生成

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

另一个模板处理所有类型的学生、课程等。以下是学生类,它是从实体模型自动生成的。

CSharp代码

以下是 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 个表的数据库,如下图所示。

新控制台项目

第 1 步– 让我们使用 DatabaseFirstDemo 名称创建一个新的控制台项目。

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

创建模型

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

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

模型内容

步骤 5 – 从数据库中选择 EF Designer,然后单击下一步按钮。

实体模型向导

步骤 6 – 选择现有数据库并单击下一步。

现有数据库

步骤 7 – 选择实体框架 6.x,然后单击下一步。

实体框架

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

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

POCO 课程

现在让我们通过在 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...

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

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

实体框架 – DEV 方法

在本章中,让我们专注于使用 Designer 或 Database First 或仅使用 Code First 构建模型。以下是一些指导方针,可帮助您决定选择哪种建模工作流程。

  • 我们已经看到了 Code First 建模、Database First 建模和 Model First 建模工作流的示例。

  • Database First 和 Model First 工作流使用 Designer,但一个从数据库开始创建模型,另一个从模型开始创建数据库。

设计师模型

  • 对于那些不想使用 Visual Designer 加代码生成的开发人员,Entity Framework 有一个完全不同的工作流,称为 Code First。

  • Code First 的典型工作流程非常适合您甚至没有数据库的全新应用程序。您定义类和代码,然后让 Code First 确定您的数据库应该是什么样子。

  • 也可以使用数据库启动 Code First,这使得 Code First 有点矛盾。但是有一个工具可以让您将数据库逆向工程到类中,这是在编码方面领先一步的好方法。

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

  • 如果您更喜欢在生成的代码中使用可视化设计器,那么您需要选择涉及 EF 设计器的工作流之一。如果您的数据库已经存在,那么 Database First 就是您的路径。

  • 如果您想在没有数据库的全新项目上使用可视化设计器,那么您将需要使用模型优先。

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

  • 如果您有现有的类,那么最好的办法是将它们与 Code First 结合使用。

实体框架 – 数据库操作

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

  • 其中两个,Database First 和 Model First,依赖于结合代码生成的实体框架设计器。

  • 第三个,Code First,让你跳过视觉设计师,只写你自己的代码。

  • 无论您选择哪条路径,最终都会得到域类,并且一个或多个实体框架 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; }
   }
}

创建操作

使用实体框架添加新对象就像构造对象的新实例并使用 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();
}

删除操作

要使用实体框架删除实体,请使用 DbSet 上的 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();
}

实体框架 – 并发

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

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

  • 在这种情况下,并发不是问题。更有可能的是,事情并没有那么简单,而且没有什么灵丹妙药可以一次性解决所有情况。

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

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

课程表

转到设计器并右键单击设计器窗口并选择从数据库更新模型…

设计师

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

课程实体

右键单击新创建的列 VersionNo 并选择 Properties 并将 ConcurrencyMode 更改为 Fixed,如下图所示。

新创建的列

Course.VersionNo 的 ConcurrencyMode 设置为 Fixed,任何时候更新 Course,Update 命令都会使用它的 EntityKey 和它的 VersionNo 属性查找课程。

我们来看一个简单的场景。两个用户同时检索同一门课程,用户 1 将该课程的标题更改为 Maths 并在用户 2 之前保存更改。稍后,当用户 2 更改在用户 1 保存更改之前检索到的该课程的标题时,即案例用户 2 将获得并发异常“用户 2:发生乐观并发异常”

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

实体框架 – 交易

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

  • 这对你来说都是透明的,你永远不需要处理它。

  • 这个事务只持续足够长的时间来执行操作然后完成。

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

实体框架 6 提供以下内容 –

Database.BeginTransaction()

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

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

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

Database.UseTransaction()

  • 它允许 DbContext 使用在实体框架之外启动的事务。

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

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 – 创建一个新的控制台应用程序项目。

应用项目

步骤 2 – 右键单击​​解决方案资源管理器中的项目,然后选择添加 → 新项目。

项目解决方案浏览器

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

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

添加按钮

步骤 5 – 从数据库中选择 EF Designer,然后单击下一步按钮。

实体模型向导

步骤 6 – 选择现有数据库并单击下一步。

现有数据库

步骤 7 – 选择实体框架 6.x,然后单击下一步。

实体框架下一个

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

表格视图

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

在解决方案资源管理器中,您可以看到 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; }
}

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

实体框架 – 存储过程

实体框架允许您使用实体数据模型中的存储过程来代替或结合其自动命令生成。

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

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

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

  • 或者,您可能只想明确控制在存储中执行的内容,而更喜欢创建存储过程。

以下示例从 File → New → Project 创建一个新项目。

程序新建项目

步骤 1 – 从中间窗格中选择控制台应用程序,然后在名称字段中输入 StoredProceduresDemo。

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

Step 3 – 选择 New Query 并在 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 – 右键单击​​编辑器并选择执行。

编辑

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

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

服务器数据库

Step 7 – 选择 New Query 并在 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 – 右键单击​​编辑器并选择执行。

执行

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

存储过程已创建

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

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

模板窗格

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

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

模型内容

步骤 14 – 选择您的数据库并单击下一步。

数据库 1

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

数据库对象

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

步骤 17 – 选择查看 → 其他窗口 → 实体数据模型浏览器,然后右键单击函数导入下的 GetStudentGrades 并选择编辑。

实体浏览器

它将产生以下对话框。

实体浏览器对话框

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

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

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

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

实体框架 – 断开连接的实体

在本章中,让我们看看如何对上下文未跟踪的实体进行更改。上下文未跟踪的实体称为“断开连接”实体。

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

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

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

  • 客户端应用程序然后在将其返回到服务器以进行持久化之前操作该数据。

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

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

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

更改为实体

让我们看一下下面的代码,其中添加了 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 后,代码使用 DbContext.Entry 方法访问实体框架具有的关于新 Student 的更改跟踪信息。

  • 根据这个变化跟踪信息,State 属性用于写出实体的当前状态。

  • 然后,对从新学生引用的每个新创建的注册重复此过程。如果您运行该应用程序,您将收到以下输出 –

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...

实体框架 – 表值函数

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

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

  • 它首先在实体框架版本 5 中引入。

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

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

让我们看看以下从 File → New → Project 创建新项目的示例。

创建项目

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

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

资源管理器数据库

Step 3 – 选择 New Query 并在 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 – 右键单击​​编辑器并选择执行。

选择执行

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

添加表

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

CREATE FUNCTION [dbo].[GetStudentGradesForCourse]

(@CourseID INT)

RETURNS TABLE

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

步骤 7 – 右键单击​​编辑器并选择执行。

编辑器选择

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

函数创建

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

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

实体模板窗格

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

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

内容对话框

步骤 12 – 选择您的数据库,然后单击下一步。

选择数据库

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

对象对话框

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

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

选择视图

您将看到以下对话框。

对话

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

让我们看一下以下 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

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

现有实体上的 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 方法用于向数据库发送非查询命令,例如插入、更新或删除命令。我们看下面的代码,其中学生的名字被更新为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();
      }
   }
}

上面的代码将从数据库中检索所有学生的名字。

实体框架 – 枚举支持

在实体框架中,此功能将允许您在枚举类型的域类上定义属性,并将其映射到整数类型的数据库列。然后,实体框架将在查询和保存数据时将数据库值与相关枚举相互转换。

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

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

  • 枚举使用户更难犯错误,并且不存在注入攻击等问题。

  • 在实体框架中,枚举可以具有以下基础类型 –

    • 字节
    • 整数16
    • 整数32
    • 64位
    • 字节
  • 枚举元素的默认基础类型是 int。

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

让我们看一下下面的例子,我们将在设计器中创建一个实体,然后添加一些属性。

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

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

创建实体

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

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

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

数据模型模板

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

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

模型向导页面

步骤 8 – 单击完成

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

设计器窗口实体

“新建实体”对话框如下图所示。

新建实体对话框

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

步骤 11 – 右键单击​​实体并选择添加新 → 标量属性。

标量属性

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

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

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

类型

步骤 15 – 在实体框架设计器中,右键单击 Name 属性,选择 Convert to enum。

实体框架设计器

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

添加枚举

步骤 17 – 单击确定。

如果您切换到 Model Browser 窗口,您将看到该类型也被添加到 Enum Types 节点。

模型浏览器窗口

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

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

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

步骤 2 – 单击新建连接按钮。

连接按钮

步骤 3 – 输入数据库的服务器名称和 EnumDemo,然后单击确定。

步骤 4 – 将弹出询问您是否要创建新数据库的对话框,单击是。

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

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

TSql编辑器

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

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

部门表

让我们看一下以下示例,其中向上下文添加并保存了一些新的 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 6.0 支持用于查询和保存数据的异步操作。

  • 异步操作可以通过以下方式帮助您的应用程序 –

    • 使您的应用程序对用户交互更敏感
    • 提高应用程序的整体性能
  • 您可以通过多种方式执行异步操作。但是 .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 的异步版本并等待其完成。

  • 调用 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 完成。

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

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

实体框架 – 持久性

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

创建持久的无知实体

前一段描述了一种方法,该方法对其消耗的数据来源没有深入了解。这突出了持久性无知的本质,即当您的类和我们周围的许多应用程序层不关心数据如何存储时。

  • 在 .NET 3.5 版本的实体框架中,如果您想使用预先存在的类,您需要通过强制它们从 EntityObject 派生来修改它们。

  • 在 .NET 4 中,这不再是必要的。您不必为了让它们参与实体框架操作而修改您的实体。

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

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

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

连接场景

连接场景是从数据库中检索实体并在同一上下文中进行修改。对于连接的场景,让我们假设我们有一个 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 word 附加在名字之前,如下面的输出所示。

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

实体框架 – 投影查询

LINQ 到实体

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

  • 这意味着您可以花更多的时间处理数据,而花更少的时间试图找出执行任务(例如访问数据库)所需的底层代码。

  • 重要的是要了解声明式语言实际上并没有从开发人员那里删除任何控制,但它可以帮助开发人员将注意力集中在重要的事情上。

LINQ to Entities 基本关键字

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

Sr. No. 关键字和描述
1

Ascending

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

2

By

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

3

Descending

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

4

Equals

用于连接语句的 left 和 right 子句之间,以将主上下文数据源连接到辅助上下文数据源。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

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

13

Where

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

14

Select

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

投影

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

  • 获得数据后,您可能希望根据需要对其进行投影或过滤,以便在输出之前对数据进行整形。

  • 任何 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,如果没有结果数据。在下面的代码片段中,将检索列表中第一个名字为 Ali 的学生。

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

标准与投影实体框架查询

假设您有一个包含 ID、FirstMidName、LastName 和 EnrollmentDate 的 Student 模型。如果要返回学生列表,标准查询将返回所有字段。但是,如果您只想获取包含 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 6.0 中,引入了一项新功能,称为Logging SQL在使用实体框架时,它向数据库发送命令或等效的 SQL 查询以执行 CRUD(创建、读取、更新和删除)操作。

  • Entity Framework 的这个特性是捕获Entity Framework 内部生成的等效SQL 查询并将其作为输出提供。

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

  • 在 Entity Framework 6 中,此新功能通过记录 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 纬度和经度坐标。

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

让我们来看看以下板球场的例子。

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

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

板球项目

步骤 3 – 右键单击​​项目名称并选择管理 NuGet 包…

NuGet 项目

第 4 步– 安装实体框架。

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

添加参考

步骤 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 – 添加一些标量属性,如下图所示。

添加标量属性

第 4 步– 我们将添加另外两个实体StudentTeacher,它们将继承 Person 表的属性。

步骤 5 – 现在添加学生实体并从基本类型组合框中选择人员,如下图所示。

基本类型组合框

步骤 6 – 同样添加教师实体。

第 7 步– 现在将 EnrollmentDate 标量属性添加到学生实体,将 HireDate 属性添加到教师实体。

注册日期

第 8 步– 让我们继续生成数据库。

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

生成数据库

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

新建数据库

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

步骤 12 – 转到服务器资源管理器,您将看到数据库是使用指定的三个表创建的。

数据库创建表

步骤 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();
      }
   }
}

学生和教师被添加到数据库中。NTO检索的学生和老师,OfType方法需要使用,这将返回学生和教师与指定部门。

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 属性是教师实体的一部分,同样,当您使用 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
*****************************************************************

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

实体框架 – 迁移

在 Entity Framework 5 和以前版本的 Entity Framework 中,代码在作为 .NET Framework 一部分提供的核心库(主要是 System.Data.Entity.dll)之间拆分,并且附加库(主要是 EntityFramework.dll)被分发和使用 NuGet 提供,如下图所示。

点网框架

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

核心 API

这是允许 Entity Framework 开源的必要条件。但是,因此,每当需要将应用程序从旧版本的实体框架迁移或升级到 EF 6 时,都需要重新构建应用程序。

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

让我们来看看将现有应用程序升级到 EF6 需要执行的以下步骤。

步骤 1 – 第一步是针对 .NET Framework 4.5.2 和更高版本,右键单击您的项目并选择属性。

升级EF6

第 2 步– 再次右键单击您的项目并选择管理 NuGet 包…

管理 NuGet 包

步骤 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。

埃德姆克斯

第 6 步– 在 EF 设计器中打开您的模型,右键单击设计图面并选择添加代码生成项…

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

代码生成模板

它还将自动生成 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。实体异常
  • 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

您现有的 Entity Framework 项目将在 Entity Framework 6.0 中运行,无需任何重大更改。

实体框架 – 预加载

Eager loading 是一个过程,其中对一种类型的实体的查询也加载相关实体作为查询的一部分。快速加载是通过使用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();

多层次

也可以急切地加载多个级别的相关实体。以下查询显示了学生、注册和课程的示例。

// 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 实体类型时,延迟加载是通过创建派生代理类型的实例,然后覆盖虚拟属性以添加加载钩子来实现的。

  • 延迟加载几乎是默认设置。

  • 如果您保留默认配置,并且没有在查询中明确告诉实体框架您想要延迟加载以外的其他内容,那么您将获得延迟加载。

  • 例如,当使用 Student 实体类时,将在第一次访问 Enrollments 导航属性时加载相关的 Enrollments。

  • 导航属性应定义为公共、虚拟。如果属性未定义为虚拟,则上下文不会进行延迟加载。

以下是包含 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 属性设为非虚拟来关闭 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 方法。

  • 对于一对多关系,请调用 Collection 上的 Load 方法。

  • 对于一对一的关系,请在 Reference 上调用 Load 方法。

我们来看看下面的例子,其中禁用了延迟加载,然后检索了名字为 Ali 的学生。

然后将学生信息写入控制台。如果您查看代码,也会写入注册信息,但尚未加载注册实体,因此不会执行 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();
      }
   }
}

执行上述示例后,您将收到以下输出。首先只显示学生信息,在显式加载注册实体后,显示学生及其相关注册信息。

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 中用于验证模型数据的验证技术。实体框架提供了种类繁多的验证功能,这些功能可以实现到用户界面以进行客户端验证,也可以用于服务器端验证。

  • 在实体框架中,数据验证是用于在应用程序中捕获不良数据的解决方案的一部分。

  • 默认情况下,实体框架会在所有数据写入数据库之前对其进行验证,使用范围广泛的数据验证方法。

  • 但是,实体框架是在用户界面数据验证之后出现的。因此,在这种情况下,需要实体验证来处理 EF 引发的任何异常并显示通用消息。

  • 有一些数据验证技术可以改进您的错误检查以及如何将错误消息传回给用户。

DbContext 有一个称为 ValidateEntity 的 Overridable 方法。当您调用 SaveChanges 时,Entity Framework 将为其缓存中状态不是 Unchanged 的​​每个实体调用此方法。您可以将验证逻辑直接放在此处,如下面的学生实体示例所示。

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 

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

实体框架 – 跟踪更改

实体框架提供了跟踪对实体及其关系所做的更改的能力,因此在调用上下文的 SaveChanges 方法时会在数据库上进行正确的更新。这是实体框架的一个关键特性。

  • 更改跟踪在向实体集合添加新记录、修改或删除现有实体时跟踪更改。

  • 然后所有更改都由 DbContext 级别保留。

  • 如果在销毁 DbContext 对象之前未保存这些跟踪更改,则会丢失这些更改。

  • DbChangeTracker 类为您提供有关上下文正在跟踪的当前实体的所有信息。

  • 要通过上下文跟踪任何实体,它必须具有主键属性。

在实体框架中,默认情况下启用更改跟踪。您还可以通过将 DbContext 的 AutoDetectChangesEnabled 属性设置为 false 来禁用更改跟踪。如果此属性设置为 true,则实体框架会维护实体的状态。

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 中,Colored Entity 主要是在设计器中改变实体的颜色,以便开发人员在 Visual Studio 设计器中轻松识别相关的实体组。此功能最初是在 Entity Framework 5.0 中引入的。

  • 此功能与性能方面无关。

  • 当你有一个大型项目并且在一个 edmx 文件中有很多实体时,这个功能非常有助于将你的实体分离到不同的模块中。

如果您正在使用 edmx 文件并且已在设计器中打开它,要更改颜色,请在设计窗口中选择一个实体。然后右键单击并选择属性。

Edmx 文件设计器
edmx 文件

在“属性”窗口中,选择“填充颜色”属性。

颜色属性窗口

使用有效的颜色名称指定颜色,例如绿色或有效的 RGB(255、128、128),或者您也可以从颜色选择器中进行选择。

颜色

要一次性更改多个实体的颜色,请选择多个实体并使用属性窗口更改所有实体的填充颜色。

多个实体

您还可以通过选择以下任何选项来更改属性的格式 –

  • 显示名称
  • 显示名称和类型

默认情况下,显示名称选项处于选中状态。要更改属性格式,请右键单击设计器窗口。

属性格式

选择标量属性格式 → 显示名称和类型。

标量属性格式

您现在可以看到类型也与名称一起显示。

实体框架 – 代码优先方法

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

  • 代码优先
  • 数据库优先
  • 模特第一

在本章中,我们将简要描述代码优先的方法。一些开发人员更喜欢在代码中使用设计器,而另一些开发人员则更喜欢使用他们的代码。对于这些开发人员,Entity Framework 有一个称为 Code First 的建模工作流。

  • Code First 建模工作流针对不存在的数据库,Code First 将创建它。

  • 如果您有一个空数据库,然后 Code First 将向其中添加新表,也可以使用它。

  • Code First 允许您使用 C# 或 VB.Net 类定义模型。

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

代码优先方法

为什么代码优先?

  • Code First 实际上是由一组拼图组成的。首先是您的域类。

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

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

  • 上下文并非特定于 Code First。这是一个实体框架功能。

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

  • 所有这些都发生在运行时。你永远不会看到这个模型,它只是在记忆中。

  • 如果您愿意,Code First 还可以使用该模型来创建数据库。

  • 如果模型发生变化,它还可以使用称为 Code First Migrations 的功能更新数据库。

环境设置

要开始使用 EF Code First 方法,您需要在系统上安装以下工具。

  • Visual Studio 2013 (.net framework 4.5.2) 或更高版本。
  • MS SQL Server 2012 或更高版本。
  • 通过 NuGet 包的实体框架。

通过 NuGet 包安装 EF

第 1 步– 首先,从文件 → 新建 → 项目…创建控制台应用程序

步骤 2 – 从左窗格中选择 Windows,从模板窗格中选择控制台应用程序。

安装 EF

步骤 3 – 输入 EFCodeFirstDemo 作为名称并选择 OK。

第 4 步– 在解决方案资源管理器中右键单击您的项目,然后选择管理 NuGet 包…

NuGet 包管理器

这将打开 NuGet 包管理器,并搜索 EntityFramework。这将搜索与实体框架相关的所有包。

第 5 步– 选择 EntityFramework 并单击安装。或者从工具菜单中单击 NuGet 包管理器,然后单击包管理器控制台。在包管理器控制台窗口中,输入以下命令:Install-Package EntityFramework。

已安装的实体框架6

安装完成后,您将在输出窗口中看到以下消息“Successfully installed ‘EntityFramework 6.1.2’ to EFCodeFirstDemo”。

安装后,EntityFramework.dll 将包含在您的项目中,如下图所示。

实体框架dll

现在您已准备好开始使用 Code First 方法。

实体框架 – 第一个示例

让我们使用类定义一个非常简单的模型。我们只是在 Program.cs 文件中定义它们,但在实际应用程序中,您会将类拆分为单独的文件,并且可能是一个单独的项目。以下是我们将使用 Code First 方法创建的数据模型。

使用类建模

创建模型

使用以下学生类代码在 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 属性将成为该类对应的数据库表的主键列。

  • Enrollments 属性是一个导航属性。导航属性包含与该实体相关的其他实体。

  • 在这种情况下,Student 实体的 Enrollments 属性将包含与该 Student 实体相关的所有 Enrollment 实体。

  • 导航属性通常定义为虚拟的,以便它们可以利用某些实体框架功能,例如延迟加载。

  • 如果导航属性可以包含多个实体(如在多对多或一对多关系中),则其类型必须是可以在其中添加、删除和更新条目的列表,例如 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 属性将是主键。

  • Grade 属性是一个枚举。Grade 类型声明后的问号表示 Grade 属性可为空。

  • 为空的成绩与零成绩不同。Null 表示等级未知或尚未分配。

  • StudentID 和 CourseID 属性是外键,对应的导航属性是 Student 和 Course。

  • 一个 Enrollment 实体与一个 Student 和一个 Course 实体相关联,因此该属性只能包含一个 Student 和 Course 实体。

创建数据库上下文

协调给定数据模型的实体框架功能的主要类是允许查询和保存数据的数据库上下文类。您可以通过从 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 实例

  • 这些只是默认约定,有多种方法可以更改 Code First 使用的数据库。

  • 正如您在上图中所看到的,它创建了学生、课程和注册表,每个表都包含具有适当数据类型和长度的列。

  • 列名和数据类型也与相应域类的属性匹配。

数据库初始化

在上面的例子中,我们看到了 Code First 自动创建了一个数据库,但是如果你想更改数据库和服务器的名称,让我们看看 Code First 在初始化数据库时如何决定数据库名称和服务器。看看下面的图表。

数据库初始化

您可以通过以下方式定义上下文类的基构造函数。

  • 无参数
  • 数据库名称
  • 连接字符串名称

无参数

如果如上例所示指定上下文类的基本构造函数而没有任何参数,则实体框架将在本地 SQLEXPRESS 服务器中创建一个名为 {Namespace}.{Context class name} 的数据库。

在上面的示例中,自动创建的数据库名为 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 被指定为基本构造函数中的参数。如果运行您的应用程序,则会在您的本地 SQL 服务器中创建名为 MyContextDB 的数据库。

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 中连接字符串中的数据库名称是EFMyContextDBCodeFirst 将创建一个新的EFMyContextDB数据库或使用本地 SQL Server 上现有的EFMyContextDB数据库。

领域类

到目前为止,我们只是让 EF 使用其默认约定发现模型,但有时我们的类不遵循约定,我们需要能够执行进一步的配置。但是您可以通过配置域类来为 EF 提供所需的信息来覆盖这些约定。有两个选项可以配置您的域类 –

  • 数据注释
  • 流利的API

数据注释

DataAnnotations 用于配置您的类,这些类将突出显示最常用的配置。许多 .NET 应用程序(例如 ASP.NET MVC)也可以理解 DataAnnotations,这些应用程序允许这些应用程序利用相同的注释进行客户端验证。

以下是学生课堂中使用的数据注释。

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

流利的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 用于配置类,这些类将突出显示最常用的配置。许多 .NET 应用程序也可以理解 DataAnnotations,例如 ASP.NET MVC,它允许这些应用程序利用相同的注释进行客户端验证。DataAnnotation 属性覆盖默认的 CodeFirst 约定。

System.ComponentModel.DataAnnotations包括以下影响列的可为空性或大小的属性。

  • 钥匙
  • 时间戳
  • 并发检查
  • 必需的
  • 最小长度
  • 最长长度
  • 字符串长度

System.ComponentModel.DataAnnotations.Schema命名空间包括以下影响数据库架构的属性。

  • 桌子
  • 柱子
  • 指数
  • 外键
  • 未映射
  • 反性质

钥匙

实体框架依赖于每个具有用于跟踪实体的键值的实体。Code First 依赖的约定之一是它如何暗示哪个属性是每个 Code First 类中的键。

  • 约定是查找名为“Id”的属性或将类名和“Id”组合在一起的属性,例如“StudentId”。

  • 该属性将映射到数据库中的主键列。

  • Student、Course 和 Enrollment 类都遵循此约定。

现在让我们假设 Student 类使用名称 StdntID 而不是 ID。当 Code First 未找到与此约定匹配的属性时,它将抛出异常,因为 Entity Framework 要求您必须具有键属性。您可以使用键注释来指定将哪个属性用作 EntityKey。

让我们看一下包含 StdntID 的 Student 类的以下代码,但它不遵循默认的 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 Explorer 中查看您的数据库时,您将看到主键现在是 Students 表中的 StdntID。

首要的关键

实体框架还支持复合键。复合键也是由多个属性组成的主键。例如,您有一个 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 注释来指定顺序。

列注释

时间戳

Code First 会将 Timestamp 属性与 ConcurrencyCheck 属性相同,但它还会确保代码首先生成的数据库字段不可为空。

  • 更常见的是使用 rowversion 或时间戳字段进行并发检查。

  • 除了使用 ConcurrencyCheck 注释,您还可以使用更具体的 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 将TStamp在 Courses 表中创建一个时间戳列

并发检查

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 注释告诉 EF 需要一个特定的属性。让我们看一下下面的 Student 类,其中将 Required id 添加到 FirstMidName 属性。必需属性将强制 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 列,如下图所示。

不为空

最长长度

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 列

当用户设置包含超过 24 个字符的 Title 时,EF 将抛出 EntityValidationError。

最小长度

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 还允许您指定其他属性验证,如 MaxLength。唯一的区别是 StringLength 属性只能应用于 Domain 类的字符串类型属性。

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 个字符,则 EF 将抛出 EntityValidationError。

桌子

默认代码优先约定创建一个类似于类名的表名。如果您让 Code First 创建数据库,并且还想更改它正在创建的表的名称。然后 –

  • 您可以将 Code First 与现有数据库一起使用。但是,类的名称并不总是与数据库中表的名称相匹配。

  • 表属性覆盖此默认约定。

  • 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,如下图所示。

学生信息

您不仅可以指定表名,还可以使用 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 表,如下图所示。

管理架构

柱子

它也与 Table 属性相同,但 Table 属性会覆盖表行为,而 Column 属性会覆盖列行为。默认代码优先约定创建一个类似于属性名称的列名称。如果您让 Code First 创建数据库,并且还想更改表中列的名称。然后 –

  • 列属性覆盖默认约定。

  • 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,如下图所示。

名

指数

Index 属性是在 Entity Framework 6.1 中引入的。如果您使用的是早期版本,则本节中的信息不适用。

  • 您可以使用 IndexAttribute 在一个或多个列上创建索引。

  • 将属性添加到一个或多个属性会导致 EF 在创建数据库时在数据库中创建相应的索引。

  • 在大多数情况下,索引使数据检索更快更有效。但是,使用索引重载表或视图可能会令人不快地影响其他操作(例如插入或更新)的性能。

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

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

  • 默认情况下,索引将命名为 IX_<property name>

让我们看一下下面的代码,其中为 Credits 在 Course 类中添加了 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学分

默认情况下,索引是非唯一的,但您可以使用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; }
}

外键

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属性,但是StdntID属性是Student类。

对此的解决方案是在 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 属性应用于导航属性。

外键属性

未映射

默认情况下,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 类中。

未映射属性

Code First 不会为没有 getter 或 setter 的属性创建列,如以下 Student 类的 Address 和 Age 属性示例所示。

反性质

当类之间有多个关系时使用 InverseProperty。在 Enrollment 类中,您可能想要跟踪谁注册了当前课程和以前的课程。让我们为 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} 外键列,如果外键属性未包含在特定类中,如上述类所示。生成数据库后,您将看到以下外键。

外键

如您所见,Code first 无法单独匹配两个类中的属性。Enrollments 的数据库表应该有一个用于 CurrCourse 的外键和一个用于 PrevCourse 的外键,但是 Code First 将创建四个外键属性,即

  • CurrCourse _CourseID
  • 上一个课程_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; }
}

正如您所看到的,通过指定它所属的 Enrollment 类的引用属性,在上述 Course 类中应用了 InverseProperty 属性。现在,Code First 将生成一个数据库并在 Enrollments 表中仅创建两个外键列,如下图所示。

外键列

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

实体框架 – 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) 的以代码为中心的方法被称为代码优先。

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

Sr. No. 方法名称和描述
1

ComplexType<TComplexType>

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

2

Entity<TEntityType>

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

3

HasKey<TKey>

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

4

HasMany<TTargetEntity>

从此实体类型配置多关系。

5

HasOptional<TTargetEntity>

从此实体类型配置可选关系。实体类型的实例将能够保存到数据库中而无需指定此关系。数据库中的外键可以为空。

6

HasRequired<TTargetEntity>

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

7

Ignore<TProperty>

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

8

Property<T>

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

9

ToTable(String)

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

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

  • 实体映射
  • 属性映射

实体映射

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

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

  • 第一件事是覆盖 OnModelCreating 方法,该方法使模型构建器可以使用。

默认架构

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

让我们看一下应用了管理架构的以下示例。

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 将使用上下文类(例如 Courses、Enrollments 和 Students)中的 DbSet 属性名称创建数据库表。但是,如果您想要不同的表名,则可以覆盖此约定,并可以提供与 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 方法

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

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

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

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

  • StudentData -包含学生FirstMidName和姓。

  • StudentEnrollmentInfo – 包含 EnrollmentDate。

生成数据库后,您会在数据库中看到以下表格,如下图所示。

实体拆分

属性映射

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

配置主键

主键的默认约定是 –

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

如果您的班级不遵循主键的默认约定,如以下 Student 班级代码所示 –

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

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

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

配置列

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

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

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

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

配置 MaxLength 属性

在以下示例中,课程标题属性不应超过 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 列。同样,Student EnrollmentDate 是可选的,因此我们将使用 IsOptional 方法允许此列中的空值,如以下代码所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

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

配置关系

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

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

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

  • 其中一些在数据注释中可用,您可以使用 Fluent API 应用一些更复杂的注释。

配置一对一关系

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

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

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

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

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

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

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

正如您在上面的代码中看到的,Key 和 ForeignKey 属性用于 StudentLogIn 类中的 ID 属性,以便将其标记为 Primary Key 和 Foreign Key。

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

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

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

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

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

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

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

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

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

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

生成数据库后,您将看到创建的关系如下图所示。

建立关系

配置一对多关系

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

  • 在这种关系中,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 方法

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

配置多对多关系

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

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

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

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

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

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

要配置 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 列创建的,如下图所示。

连接表

如果要指定连接表名称和表中列的名称,则需要使用 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");
   }); 
}

可以看到生成数据库的时候,表名和列名都是按照上面的代码创建的。

连接表

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

实体框架 – 种子数据库

在实体框架中,种子是在 EF 4.1 中引入的,并与数据库初始值设定项一起使用。种子方法的一般思想是将数据初始化到由 Code First 创建或由迁移演变的数据库中。这些数据通常是测试数据,但也可能是参考数据,例如已知学生、课程等的列表。当数据初始化时,它会执行以下操作 –

  • 检查目标数据库是否已经存在。
  • 如果是,则将当前 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);
   }
}

在上面的代码中,学生表被初始化。您需要在上下文类中设置此 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);
      }
   } 
}

编译并执行上述示例后,您可以看到数据库中的数据如下图所示。

数据库中的数据

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

实体框架 – 代码优先迁移

Entity Framework 4.3 包含一个新的 Code First 迁移功能,该功能允许您随着模型的变化而逐步改进数据库架构。对于大多数开发人员来说,这比 4.1 和 4.2 版本中的数据库初始化程序选项有了很大的改进,这些选项要求您在模型更改时手动更新数据库或删除并重新创建它。

  • 在 Entity Framework 4.3 之前,如果您的数据库中已经有数据(种子数据除外)或现有的存储过程、触发器等,这些策略用于删除整个数据库并重新创建它,因此您将丢失数据和其他数据库对象。

  • 通过迁移,它会在您的模型更改时自动更新数据库架构,而不会丢失任何现有数据或其他数据库对象。

  • 它使用名为 MigrateDatabaseToLatestVersion 的新数据库初始值设定项。

有两种迁移 –

  • 自动迁移
  • 基于代码的迁移

自动迁移

自动迁移首先在实体框架 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

命令

步骤 3 – 命令成功运行后,它会在项目的迁移文件夹中创建一个内部密封的配置类,如下面的代码所示。

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 步– 使用新的数据库初始化策略 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 步– 您已设置自动迁移。当您执行应用程序时,当您更改模型时,它会自动处理迁移。

移民

第 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 表已添加到您的数据库中。

学生登录

上述针对自动迁移的步骤仅适用于您的实体。例如,要添加另一个实体类或删除现有实体类,它将成功迁移。但是,如果您向实体类添加或删除任何属性,则会引发异常。

第 10 步– 要处理属性迁移,您需要在配置类构造函数中设置 AutomaticMigrationDataLossAllowed = true。

public Configuration() {
   AutomaticMigrationsEnabled = true;
   AutomaticMigrationDataLossAllowed = true;
   ContextKey = "EFCodeFirstDemo.MyContext";
}

基于代码的迁移

当您开发一个新应用程序时,您的数据模型会频繁更改,并且每次模型更改时,它都会与数据库不同步。您已将实体框架配置为每次更改数据模型时自动删除并重新创建数据库。当您希望对迁移进行更多控制时,基于代码的迁移非常有用。

  • 当您添加、删除或更改实体类或更改 DbContext 类时,下次运行应用程序时,它会自动删除您现有的数据库,创建一个与模型匹配的新数据库,并用测试数据为其播种。

  • 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 文件夹中创建了一个新文件,其中包含您传递给命令的参数名称,并带有时间戳前缀,如下图所示。

时间戳前缀

第 5 步– 您可以使用“update-database”命令创建或更新数据库。

PM> Update-Database -Verbose

“-Verbose”标志指定在控制台中显示应用于目标数据库的 SQL 语句。

第 6 步– 让我们在学生类中再添加一个属性“年龄”,然后执行更新语句。

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。

新列时代。

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

实体框架 – 多个 DbContext

在本章中,我们将学习当应用程序中有多个 DbContext 类时如何将更改迁移到数据库中。

  • 多 DbContext 最早是在 Entity Framework 6.0 中引入的。
  • 多个上下文类可能属于单个数据库或两个不同的数据库。

在我们的示例中,我们将为同一个数据库定义两个 Context 类。在以下代码中,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 相关联。

当同一个项目中有多个 Context 类时,这是迁移数据库更改的基本规则。

  • 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

包管理器控制台

执行后,我们将在迁移历史记录中添加模型,为此,我们必须在同一控制台中触发 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();
   }
}

执行上述代码时,您将看到为两个不同的模型创建了两个不同的表,如下图所示。

执行代码

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

实体框架 – 嵌套实体类型

在 Entity Framework 6 之前,Entity Framework 无法识别嵌套在其他实体或复杂类型中的实体或复杂类型。当实体框架生成模型时,嵌套类型就消失了。

让我们看一个简单的例子,在这个例子中,我们的基本模型包含三个实体:Student、Course 和 Enrollment。

  • 让我们添加一个属性 Identity,它是一个 Person 类型。Person 是另一个实体,包含 BirthDate 和 FatherName 属性。

  • 在实体框架术语中,因为它没有身份并且是实体的一部分,所以它是实体框架复杂类型,而且我们实际上从实体框架的第一个版本开始就支持复杂类型。

  • 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 文件并选择实体框架 → 查看实体数据模型(只读)

框架动力工具

现在您将看到在 Student 类中定义了 Identity 属性。

身份属性

如果这个 Person 类不会被任何其他实体使用,那么我们可以将它嵌套在 Student 类中,但是这个早期版本的实体框架不承认嵌套类型。

在旧版本中,您再次生成模型,不仅无法识别类型,而且因为它不存在,属性也不存在,因此实体框架根本不会持久化 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; }
   }
}

使用实体框架 6,可以识别嵌套实体和复杂类型。在上面的代码中,可以看到 Person 嵌套在 Student 类中。

当您使用 Entity Framework Power Tool 展示这次 Entity Framework 如何解释模型时,有真正的 Identity 属性和 Person 复杂类型。因此实体框架将保留该数据。

嵌套实体类型

现在你可以看到 Identity 是一个嵌套的实体类型,在 Entity Framework 6 之前是不支持的。

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

觉得文章有用?

点个广告表达一下你的爱意吧 !😁