简单演示 Entity Framework - 释放野兽






4.96/5 (27投票s)
简单演示 Entity Framework - 一个解释 Code First 的教程!
Content
- 引言
- Entity Framework 简介
- Code First 驱动方法
- 我们将要开发的那个项目
- 项目 - 领域类快速预览
- 创建 DbContext 类
- 与数据库交互
- 解释 Code First 创建的数据库
- 让我们来理解 Code First Migrations
- 与数据库交互 - 在 Discussion 数据库中插入记录
- 与数据库交互 - 更新和查询 Discussion 数据库
- 与数据库交互 - 删除 Discussion 数据库中的记录
- 与数据库交互 - 从 Discussion 数据库中检索数据记录图
- 最终想法
引言
在本文中,我们将深入了解 Entity Framework 6,特别是 Code First 方法。
关于 EF 6
"Entity Framework" 主要有三种形式:
- 数据库优先 (Database First)
- 模型优先 (Model First)
- 最后,也是 EF 的亮点... Code First - 这是我们将在本文中涵盖的。
目标是展示我能想到的 EF 6 Code First 的每个方面。请放心,我会尽力涵盖所有细节,以便让您在阅读本文的过程中尽可能舒适。
希望在完成本文后,我们会对微软有更多的欣赏。:)
顺便说一下,我写过几篇关于 WCF Web 服务的文章,请随时根据您的方便查阅。
我将遵循 KISS 原则:“保持简单,愚蠢。”
那么,让我们开始吧...
在继续之前,假设您已安装、配置并完美运行 Visual Studio 2013(或更早版本)和 SQL Server 2012。如果尚未安装,现在是时候搜索 Visual Studio Community Edition 来开始了。
Entity Framework 简介
概述
Entity Framework 是一个对象关系映射器("ORM")。ORM 提供了包括提高开发人员生产力在内的诸多好处(EF 很好地负责从数据库拉取数据和向数据库写入数据)- EF 可以生成基本的命令来读取和写入数据库数据,从而改善了许多开发者的生活。
查询是使用您在应用程序中声明的领域对象,通过一个叫做“LINQ to Entities”的东西编写的,从数据库检索到的结果会自动转换为我们在软件应用程序中定义的领域对象。基本上,生活变得容易多了。
理论上,像 Entity Framework 这样的 ORM 让我们能够更专注于业务方面的事情,而不是过于担心数据库。在 EF 中,我们关注的是我们领域中的概念模型。
现在,这是一个应用程序中对象模型,而不是用于持久化应用程序数据的数据库模型。
现在,关键点来了,根据不同情况,我们的概念模型可能与数据库模型一致,另一方面,我们也可能遇到概念模型与数据库模型不一致的情况。
是的,EF 就是如此灵活且随和。
一旦我们有了概念模型,我们就可以使用 Entity Framework 的一个名为 Code First 的功能,它可以为我们创建数据库表等内容,并将其放置在我们选择的数据库中。
总的来说,Entity Framework 能够轻松优雅地在我们定义的模型和所需的数据库之间进行映射,以实现数据的持久化。所以我们针对概念模型编写查询,Entity Framework 会确保组合必要的查询来读取/写入和更新数据库中的数据。
您问 EF 是如何做到这一点的?
您使用 LINQ to Entities 编写查询 - Entity Framework 将它们分解为 Command Trees,然后提供程序将这些 Command Trees 转换为目标数据库的适当 SQL 查询。
EF 工作流程
注意* 在本文中,我们只涵盖 Code-First 方法。
- 我们从定义概念模型——领域类开始。
- 针对我们在上一步中定义的模型编写查询(LINQ to Entities)
- Entity Framework 将(上一步中定义的)查询分解为 Command Trees
- 然后 EF 提供程序将这些 Command Trees 转换为目标数据库的适当 SQL 查询
- EF 确定并执行 SQL
- EF 将结果转换为我们在第一步中定义的领域类对象。
以下是关于 EF 您可能感兴趣的更多详细信息:
- Entity Framework 是一项开源计划。
- Entity Framework 与 .NET 发布周期无关。
- Entity Framework 可与多种数据库一起使用(提供许多第三方提供程序)。
- Entity Framework 可以创建参数化查询。这取决于您如何指定 LINQ 查询。
- Entity Framework 能够跟踪我们概念模型中的内存更改。一旦调用 SaveChanges() 方法,EF 将执行适当的 Insert/Update 或 Delete 查询来反映已进行的内存更改。
- Entity Framework 灵活且提供精细控制(如有必要)。
- Entity Framework 通过自动缓存查询提供更好的查询性能。
因此,在我们开始之前,请记住这一点 - “如果你做容易的事,你的生活会很艰难;如果你做艰难的事,你的生活会很轻松。”
该开始行动了…
Code First 驱动方法
在我们开始之前需要记住的事情是...
- 我们没有可视化设计器。
- 我们没有自动代码生成。
- 数据库 - 可能存在,也可能不存在。(我们很快就会讲到)
- 最后一点,我们“热爱代码”,讨厌其他两种方法 - 数据库优先和模型优先。
那么,Code First 方法包含什么?
- 领域类
这些领域类与 Entity Framework 无关。它们只是代表您的业务领域项。 - Entity Framework
然后我们声明 Context 类。这个 Context 类管理着我们的领域类与数据库之间的交互。值得一提的是,Context 类并非 Code First 方法独有,它是 Entity Framework 的一个特性,在 Model First 和 Database First 方法中也用于编排数据库交互。
特殊之处在于 - Code First 添加并使用 DbContext 类作为模型构建器,它会检查 Context 类和它所管理的领域类,以及我们通过 - Code First Data Annotations 或 Code First Fluent API 或两者兼而有之定义的一组规则,来确定我们的领域类及其关系如何描述数据库模型,以及如何精确地映射到数据库。 - 我们还可以选择初始化数据库和/或使用 Code First Migrations。初始化将帮助我们从一开始就用一些数据填充数据库。Migrations 将帮助我们更新数据库模式,如果我们的领域类模型发生任何更改。
所以,让我们开始吧,本文中我们将做的事情如下:
- 我们将创建默认的 Code First 模型并从中创建一个数据库。
- 管理和理解领域模型的变化如何反映/影响数据库。
- 以声明式(Data Annotations)和命令式(Fluent API)方式管理配置。
- 最后,我们将把现有数据库反向生成为 Code First 模型。
我们将要开发的那个项目
我们将为讨论版(Discussion Board)开发数据库模型。
我了解到,讨论对于促进一个不断发展的进步社会至关重要。所以,现在是时候举办一个讨论会了,2016 年美国大选也近在眼前。
让我们构建后端来促进上述讨论。
我们提出的“讨论版”应用程序将需要以下表 - 旁边我已说明了创建这些表的目的。
表名 | 创建该表的目的 |
类别 | 讨论所属的类别。一些有效的例子是 - 技术、政治、科学、体育以及无数其他。 |
主题 | 讨论的主题。一些有效的例子是 - 在“政治”类别下,为什么“伊丽莎白·沃伦”不竞选 2016 年总统?注意* 一个主题也可以关联到多个类别。例如 - 盖茨先生会购买一个篮球队吗?或者他会继续做出有益于人类的明智投资吗?上述主题可能符合两个类别 - 1. 技术和 2. 体育。 |
注释 | 关于一个主题的评论。一些有效的例子是 - 关于上面提到的主题:我不知道为什么“伊丽莎白·沃伦”不竞选总统。她会是我投票的乐趣。 |
成员 | 存储会员详细信息的表。例如会员名字、姓氏、昵称等。 |
联系方式 | 存储会员联系方式的表。例如 Twitter 别名、Facebook 个人资料 ID 等。别担心,我们支持匿名,如果我们正在开发一个功能齐全的应用程序,我们将允许完全匿名。这些详细信息仅用于充实我们的模拟数据库。 |
兴趣 | 存储会员兴趣的表。例如,如果会员的兴趣包括科学、体育等,我们将在该表中记录该信息。 |
特别说明请您注意... 类别和主题表将共享一个多对多*关系。 | |
项目 - 领域类快速预览
创建一个类库项目,其中将包含上述所有领域类的定义。注意* 我已经创建了一个名为:“CodeFirst.DiscussionBoard”的 Visual Studio 解决方案。我们将向此解决方案添加项目。
创建的项目
注意* 删除以下类,我们不需要它。
现在我们将开始构建实际项目。向项目中添加以下类文件:
我们将从添加一个名为“Category”的类开始。
- 步骤 1
- 第二步
添加的类...
现在添加以下类 - 重复上述两个步骤。
- “Topic”
- “Comment”
- “Member”
- “ContactDetail”
- “Interest”
已添加类...
现在我们将向上述定义的类添加内容。我们将为上述类添加属性,这些属性以后将映射到数据库表列。例如 - Category 类将映射到 Category 表,其中的任何属性将映射到 Category 表将拥有的列。
我将继续为上述类添加所需的属性。这些属性相当简单,我将在代码中添加注释来解释任何可能有点棘手的地方。
在添加完上述创建的类中的详细信息(属性)后的屏幕截图。
- Category 类
- Topic 类
- Comment 类
- Member 类
- ContactDetail 类
- Interest 类
需要注意的一点是,我们的类库项目:“DiscussionBoard.DomainClasses”将不会引用 Entity Framework。这只是代表我们业务领域的普通类。
我们将开始添加将在 Category 和 Interest 类中使用的枚举类型。下面是新的枚举以及修改后的 Category 和 Interest 类。
修改后的 Category 类属性
修改后的 Interest 类属性
最后说明 - 上述类中存在导航属性 - 这些属性有助于在 Entity Framework 中更好地管理关系。它们还有助于实现所谓的延迟加载(我们稍后会看到)virtual 关键字* 在这里很有帮助。
导航属性的示例
// Snippet from Comment.cs // The Topic this Comment is related too public int TopicId { get; set; } // Navigation Property to Topic public virtual Topic Topic { get; set; }
注意* 我们有 TopicId,但仍然有一个类型为 Topic 的名为 Topic 的虚拟属性。
与其他类类似,其他类也有导航属性。我们拥有这些导航属性的原因如上所述。
现在我们已经准备好了简单的领域模型,接下来... 我们将创建 DbContext 类。
创建 DbContext 类
因此,在上面部分,我们创建了代表业务领域的简单 POCO 类。
我们的领域类完全没有引用 Entity Framework。
当然,接下来自然会问:我们将如何与 Entity Framework 交互?我们将通过引入一个数据层来实现,该数据层将引用 Entity Framework。我们还将把我们的数据层指向上面创建的业务领域类。
Needless to add - 数据层将管理我们的业务领域类与 Entity Framework 相关的方面。这包括 - 但不限于 - 管理更改跟踪、从数据库获取数据等等。
在我们的解决方案中创建一个新项目
注意* 删除 Class1.cs 并添加一个名为“DiscussionBoardContext”的新类。
已添加项目 -
现在让我们将 Entity Framework 添加到解决方案中,并将我们新创建的项目DiscussionBoard.DataLayer 指向使用它。
添加 EF 到解决方案的步骤
取消选中 - Domain Classes Project。并在提示时接受许可条款和条件。
现在我们的解决方案中有了最新的稳定版 EF(截至撰写时为 6.0 版本)。
现在的项目结构。我们的 Data Layer 项目已添加 app.config 和 package.config 文件。此外,我们的 Data Layer 项目在其引用下现在有了 Entity Framework DLL。
app.config 示例文件(我项目中的文件有所不同 - 我将在下面提供)。它包含一些默认配置。注意* LocalDbConnectionFactory 是 SQL Server 2012 的一部分,并且是您将使用的默认开发数据库,而不是 SQL Express,它会随 Visual Studio 2012(及更高版本)版本一起安装。如果您运行的是 VS 的先前版本,它将自动默认为 SQL Express,所以完全不用担心。
但是,我运行的是 SQL Server 2014,对我来说它默认是 SQL Express。我没有安装 LocalDb,所以它默认是 SQL Express。我的 App.config 文件有以下配置设置:
package.config 文件 - 这里没什么复杂的。
现在是时候引入 DbContext 了。
我们将在 Data Layer 的 DiscussionBoardContext 类中继承 EF 的 DbContext 类。
已添加 DbContext,现在我们的 DiscussionBoardContext 类将能够执行所有与 Entity Framework 相关的任务,因为它现在继承自 DbContext。
现在我们希望 DiscussionBoardContext 管理我们的业务领域类。我们将通过使用 DbSet 来实现。注意* 我将留给您去搜索 DbSet 是什么?它相当直接。不过这里有一些关于 DbSet 的提示(这应该涵盖最重要的部分):
- DbSet:管理操作(集合操作),例如:Add、Attach、Remove、Find。
- DbSet:是单个实体类型的集合(通常映射到一个业务领域类)。
- DbSet:与 DbContext 一起用于查询数据库。
- DbSet:是 System.Data.Entity 的成员。
示例
public DbSet<Student> Students { get; set; }
Students 的 DbSet 将用于管理 Student(s) 对象在数据库中的实例、查询和更新。
一旦我们在 Data Layer 项目中使用 DbSet,您将更好地理解它。
在引入 DbSet 之前,我们需要首先在我们的 Data Layer 项目中引用我们的 Domain Classes Project。(请遵循下面的屏幕截图)。
是时候添加我们的业务领域类的 DbSet 了。这是一个简单的过程,请遵循下面的屏幕截图。
出现的一个有趣细节是,我没有为 ContactDetail 领域类创建 DbSet。这是因为我确信我不会一对一地与 ContactDetail 交互。也就是说,我不会显式地查询 ContactDetail。由于 ContactDetail 与 Member 共享一对一或一对零关系,因此我只想在获取 Member 记录时检索它们。EF Code First 将支持这一点,因为它通过我们的业务领域模型了解这种关系。即使我们没有为 ContactDetail 设置 DbSet,我们仍然能够获取 Member 的 ContactDetail。
旁注* 我们所有的领域都在一个 Context 类中。对于我们这里简单的应用程序来说,这会奏效。对于复杂的应用程序,您可能需要查找称为领域驱动设计(Domain Driven Design)的东西。
Entity Framework 依赖于描述概念模型 Schema(CMS)、数据库结构 Schema(DSS) 以及(CMS 和 DSS 之间的)映射的元数据来与数据库进行交互。
对于 Code-First,Entity Framework 在运行时即时构建模型元数据。它根据 Context 所管理的 DbSet 以及领域类本身中定义的(由 DbSet 管理的)关系和属性来推断这些数据。
我们可以使用一个工具(一个强大的工具)来创建一个只读的可视化模型,以便我们更好地理解 Entity Framework 如何解释我们的领域模型。
这将帮助我们构建想要的模型,并消除 Code-First 即时生成的模型元数据的猜测。因为我们可以提前看到它将如何翻译我们的领域模型。
接下来让我们看看这个强大的工具。
安装推荐的强大工具(请遵循下面的屏幕截图)。
现在我们已经安装了工具,让我们从 Context 类生成数据库模型。
右键单击 DiscussionContext.cs 并按照下面的屏幕截图操作。
哎呀,出错。查看 VS 底部... 我们看到以下内容:
查看输出窗口,我们看到以下错误:错误是关于:System.IO.FileNotFoundException。
现在,我们收到上述错误是因为当前的启动项目是:DiscussionBoard.DomainClasses,而 DiscussionBoard.DomainClasses 项目没有 App.config 文件。
所以,让我们将 DiscussionBoard.DataLayer 项目设置为启动项目,并且 DiscussionBoard.DataLayer 项目有一个 App.config 文件(其中包含必要的 DB 连接字符串等)。
DiscussionBoard.DataLayer 项目已设置为启动项目并拥有 App.config 文件。
现在让我们再尝试生成 DB 模型...
啊... 更多的错误。
我必须补充一点,这个工具也会验证我们的模型,在构建数据库之前验证我们的模型是个好主意 - 它可以更容易地识别错误/问题,并且在早期阶段 - 所以修复问题也更便宜。
这次工具抱怨的是:错误类型是:System.Reflection.TargetInvocationException。
简而言之,验证失败是因为我们的领域类 ContactDetail 没有声明主键。
Needless to add Entity Framework 有一个规则,即每个实体都必须有一个 Key,因此出现了验证错误,因为该工具未能为 ContactDetail 找到一个。EF 查找 KEY 时遵循的约定是,每个实体都必须有一个名为 选项 1: Id 或 选项 2: 名为 entityName 并以 Id 结尾的属性,如 EntityNameId。ContactDetail 实体的有效示例将是以下内容...
选项 1 示例
// In ContactDetail Class public class ContactDetail { // Key Property Option 1 public int Id { get; set; } }
或 选项 2 示例
// In ContactDetail Class public class ContactDetail { // Key Property Option 2 - entityName suffixed by Id public int ContactDetailId { get; set; } }
闪回 - ContactDetail 领域类
由于我们没有使用上述指定约定(通过选项 1 和选项 2 示例演示)在 ContactDetail 中声明 KEY,因此我们收到了验证错误。
现在 Code First 提供了两种方法来提供配置信息,以供 EF 无法使用约定正确推断(如我们在 ContactDetail 中的情况)。
我们可以通过 2 种方法来解决这个问题:
- 数据注解
- Fluent API
现在我们将通过 Data Annotations 来解决这个问题。
在 ContactDetail 类中使用 Data Annotations 来声明主键。
我们只需要使用 KEY 注解将一个属性标记为 KEY...
[Key] 注解下的红色波浪线表示一切都不好。我们将需要将所需的命名空间添加到我们的 ContactDetail 类中。
请遵循下面屏幕截图所示的步骤。
在 Framework.. 下选择 System.ComponentModel.DataAnnotations。
完成后... 让我们回到 ContactDetail 并添加所需的命名空间。
已添加命名空间,一切正常。
现在让我们再次运行该工具...
该死,还是有错误。
我们快到了...
这次验证失败是因为 - Entity Framework 的一个令人困惑的约定。
现在抱怨的是由于这个令人困惑的约定:简单来说:当我们在
- 一对一关系
- 一对零或一关系
Code First 需要了解哪个是关系中的主端,哪个是依赖端。这将帮助 Code First 确定数据库中的哪个表将具有指向另一个表的外键。
所以,让我们再次使用 Data Annotations 来清楚地指定 Member 表和 ContactDetail 表之间的这种一对一或一对零或一关系。
我们可以通过指定 Foreign Key 注解来做到这一点。
请遵循下面的屏幕截图:(ContactDetail 类的屏幕截图)
同样,Foreign Key 注解存在于 System.ComponentModel.DataAnnotations.Schema 命名空间中。让我们添加所需的命名空间。
现在的 ContactDetail 类
注意* 需要注意的一个重要事项是,仅仅指定 Foreign Key 属性是不够的。我们需要指定 Foreign Key 指向什么?通过使用导航属性,我们可以做到这一点。因此,该属性指向 Member 导航属性(它不是属性类型,而是屏幕截图上方黄色高亮显示的属性名称)。
哇.. 终于,让我们再次运行该工具...
瞧,我们看到了(只读的)模型显示。辛苦总算有回报了...
既然我们已经验证了模型... 让我们进入下一步...
数据库初始化
Code-First 在运行时并不真正关心数据库... 但当它到达那个点时,它会执行以下两件事:
- 步骤 1 - 定位数据库(在这里使用了 App.config 中的连接字符串)。
- 步骤 2 - 必要时创建数据库。
这是它的工作原理,第一个 DbContext 类尝试与数据库交互(在应用程序运行时),Code First 将执行“数据库初始化”。
它首先查找连接字符串,一旦确定(App.Config 在这里有帮助),它就会查找数据库 - 默认情况下,Code First 查找的数据库名称将是 DbContext 类的完全限定名称。
在我们的情况下,这将是:命名空间加上 DbContext 类名称 - 如下面高亮显示:
下一步 Code First 将检查数据库名称为:“DiscussionBoard.DataLayer.DiscussionBoardContext”的数据库是否存在。当然,下一步是 - 如果数据库不存在,它将创建它。Needless to add Code First 将使用模型构建器创建的元数据来确定数据库的 Schema。一旦数据库存在,Code First 将继续执行 Context 被指示执行的任何操作,例如 - 插入记录等。
当然,我知道您现在很好奇想看看默认行为的实际运行。
您的等待结束了 - 让我们开始吧...
请注意* 在本文中,我们将使用一个控制台应用程序项目来进行数据库交互。
我将在未来撰写一篇关于 MVC 的文章,其中将使用我们的 DiscussionBoard 数据库。
在其中。一旦这篇文章上线,我还需要大约 45 天左右才能完成提议的 MVC。
文章(我只在下班后的晚上写,所以需要时间。希望您能理解)。
为此,我们需要创建一个控制台应用程序项目,然后引用我们的数据层项目...
请遵循下面的步骤...
现在我们将引用 Entity Framework 和 DiscussionBoard.DataLayer 项目到您新创建的控制台应用程序项目中。
引用 DiscussionBoard.DataLayer 项目
已添加引用
引用 Entity Framework
选择项目后,在“选择的项目”对话框中按“确定”。
现在我们有了引用。
我们在 DiscussionBoard.ConsoleApplication 中有 App.Config(包含必要的连接字符串和其他信息),我们可以从 DiscussionBoard.DataLayer 项目中删除 App.Config,因为我们将从我们新创建的控制台应用程序项目:DiscussionBoard.ConsoleApplication 进行所有交互。
当提示出现时,选择“确定”.
删除 DiscussionBoard.DataLayer 项目中的 App.Config 后。
注意* 请也将 DiscussionBoard.DomainClasses 项目的引用添加到我们的控制台应用程序中。
现在让我们在 DiscussionBoard.ConsoleApplication 项目的 Program.cs 类中添加一些代码。我已在代码中添加注释来解释代码的作用...
此时在运行时需要注意的一点是 -
// Snippet from Program.cs Class
var members = context.Members.ToList();
Code First 意识到它必须做些什么,它会去查找连接字符串,并检查数据库是否存在。所以,这里需要注意的重要一点是,Code First 在初始化 DiscussionBoardContext 类时不会立即采取行动。
现在,Needless to add,当我们此时运行我们的控制台应用程序时,不会返回任何内容,因为到目前为止数据库尚不存在。
无论如何,让我们运行它... 确保启动项目设置为 DiscussionBoard.ConsoleApplication。按 F5。
我们唯一得到的是一个空白黑屏。
让我们深入了解并理解实际发生了什么。打开 SQL Server Object Explorer。
注意* 如果您没有 SQL Server Object Explorer,那么您需要安装 SQL Server Data Tools,可以从这个链接下载。请选择正确的版本并安装它。
找到 SQL Server Object Explorer 后,进入 SQL Express 数据库。
如果您的 SQL Express 数据库丢失,您可以通过执行以下操作添加它:
选择数据库并连接到它。
连接后 - 找到 Code First 创建的数据库,如下所示。
现在,在下一节中,我将尝试解释 Code First 为我们创建的数据库。
解释 Code First 创建的数据库
打开数据库 - 见下文。我们将从数据库的名称开始 - 即:“DiscussionBoard.DataLayer.DiscussionBoardContext”。如上一节所述 - 默认数据库名称将是 Context 类的完全限定名称。
让我们看看表,那里有些有趣的东西。
在此处提及一点很重要,即是我们代码的模型决定了数据库的结构。仔细查看数据库 Schema。
所以,我们的数据库看起来就像我们在 Data Layer 项目中创建的模型(还记得使用 Power Tools 生成的模型吗?)。
Needless to add,类(在 Domain Class 项目中)在这里被表示为表,它们的属性作为列。
关系是使用 Domain Classes 项目中的 Data Annotations 定义的。
有趣的表是 ContactDetail 和 TopicsCategory。
让我们先看看 ContactDetail。
MemberId 列是表中的主键,同时也是外键。这是我们在 ContactDetail 类中指定 Data Annotations 的结果。如果您愿意,可以快速回顾一下 ContactDetail 类。
让我们看看 TopicsCategory 表。您可能已经在想,我们没有定义一个名为这个表名的类,那么这个表是如何存在的。
嗯,我们在 Domain Classes 项目中定义了 Category 和 Topic 类之间的多对多*关系。
Entity Framework 和数据库处理这种关系的方式是使用一个JOIN Table。在数据库中。TopicsCategory 就是那个JOIN Table。
所以,在我们的应用程序中,每当我们使用 Category 和 Topic 之间指定的多对多*关系时,我们全能的 Entity Framework 都会将其转换为 SQL 语句,这些语句将使用这个 JOIN Table。使数据库更容易理解我们试图做什么 - 例如获取/插入/更新/删除等。
让我们来理解 CodeFirst Migrations
现在,我们有了模型和数据库,如果我们决定更改它,会发生什么?
也就是说,如果我们决定向 Comment 表添加一个新列。
要做到这一点,我们需要在 Domain Class 项目(DiscussionBoard.DomainClasses)中的 COMMENT 类中添加一个新列。
让我们向 Comment 类添加一个名为 - NumberOfLikes 的新列。
构建项目并再次运行 Console Application - 我们将检查会发生什么 - 记住我们更改了模型。
糟糕... 添加新列破坏了我们的项目。自我解释的异常 - 高亮部分解释了问题所在。
我们之所以得到这个异常,是因为数据库初始化的默认行为不允许模型更改。
是时候修复这个因我们创建数据库后更新模型而出现的问题了。在现实世界中,这是非常可能的情况 - 我们将不得不更新数据库模式。重要的是要解决这个异常。
敲鼓声 - Code First Migrations 登场。来解决上述问题。
我们需要告诉 Code First,与其使用数据库初始化,不如使用其Migrations 功能。
Entity Framework 的数据库 Migrations API 负责管理和同步数据库模式,因为我们的领域模型会发生变化/演变。Migrations - 通过跟踪模型和数据库之间的更改来实现管理(它们通过管理数据库模式的哈希值来实现)。
我们将从首先开始:我们将让 Code First 知道我们希望使用 Migrations。其次:我们将使用 Migrations 来创建我们的初始数据库(所以,如果您已有的数据库,请随意删除。可以从 SQL Server Management Studio 进行。另外,请注释掉 DiscussionBoard.DomainClasses 项目中的 Comment 类中的 NumberOfLikes 属性。)。
所以,让我们开始工作,让 Code First 了解我们使用 Migrations 的意图。
打开 Package Manager Console。
请确保您已选择默认项目为 "DiscussionBoard.DataLayer"。
是时候玩一些简单的命令了。在 Package Manager 控制台中键入“enable-migrations”,我们可以为该命令传递一些参数,但我们将坚持使用默认值。
所以,这个 enable-migrations 命令将首先检查数据库是否存在,然后它将启用 migrations。如下图所示...
运行 enable-migrations 命令对我们的 DiscussionBoard.DataLayer 项目做了一些更改。我们有一个新创建的文件夹:Migrations,这个文件夹有一个名为:Configuration.cs 的类。
文件夹
Configuration 类默认代码。这个类特定于 Migrations(继承自 DbMigrationsConfiguration),并且它正在为我们的上下文类:DiscussionBoardContext 设置配置(如下面高亮显示)。我们将保持 Configuration.cs 中的参数 "AutomaticMigrationsEnabled" 为 false。Seed 方法的重要性在于它在每次 Migration 运行之前执行。所以,如果有人想在每次 Migration 进行时将一些默认数据推送到数据库,他们就会利用 Seed 方法。注意 - Seed 方法中已经提供了一些示例代码来添加默认初始数据。
Code First Database Migrations 过程基本上是 3 个步骤:
- 定义或对模型进行更改(定义(初始数据库创建)/更改(修改现有数据库模式)都使用我们在 DiscussionBoard.DomainClasses 项目中的代码模型)。
- 创建 Migration 文件:这是表示模型当前状态的文件。此文件用于识别需要对数据库进行的更改,以使其与我们的模型保持一致。
- 将上述创建的 Migration 文件应用于数据库。无需进一步解释 :P
现在,在继续之前,请确保您已删除(如果仍然存在)项目的已创建数据库,并确保 Comment.cs 类中的 NumberOfLikes 属性已被注释掉。
所以,开始了。
打开 Package Manager Console,选择 DiscussionBoard.DataLayer 项目。
键入命令:add-migration InitialMigration20151014
请遵循下面的屏幕截图。
在这里,命令 add-migration - migration 是 EF 创建的类,用于描述数据库中将需要的更改。add-migration 命令后的参数只是迁移的名称。我选择了 InitialMigration20151014 这个名称。但您可以使用更具描述性的名称等。
首先,上述命令检查我是否之前有任何迁移,但没有。所以,它将基于这一事实来构建迁移文件/类。这是非常初始的迁移,甚至数据库尚不存在。所以,EF 正在迁移的是什么都没有,因此它需要从头开始创建一切。
请查看执行上述命令添加的类以更好地理解。
文件
查看 Migrations 文件夹下的 Class - InitialMigration20151014.cs。
由于这是第一个迁移,InitialMigration20151014.cs 类已填充了 create table 和其他相关方法。注意* 这段代码将被翻译成适当的 SQL 代码... 是的,Migration API 就是这样构建的。:)
因此,到目前为止,我们已经完成了第 1 步和第 2 步。第 1 步是定义模型,我们在前面的部分已经完成。第 2 步是创建 Migration 文件,我们通过命令:add-migration InitialMigration20151014 完成了。
现在,是时候执行第 3 步 - 应用 Migration 文件了。
命令 update-database 将默认更新我们的数据库模型到最新的迁移。您可能已经注意到迁移文件上的时间戳 - Entity Framework 通过它来识别哪个迁移是最新的。
现在,有许多参数可以与 update-database 命令一起使用,但我想让您浏览这两个命令,因为我认为它们在开发人员的日常工作中非常重要。
注意* 在继续之前,请暂时将 App.Config 文件从 DiscussionBoard.ConsoleApplication 项目移动到 DiscussionBoard.DataLayer 项目。因为我们将为我们的 DiscussionBoard.DataLayer 项目在 Package Manager Console 中执行 Migration 命令。
- update-database 命令带 -script 参数。这将生成一个 SQL 脚本,可以将其传递给 DBA,以便他/她可以应用模型更改到数据库。
用法:在 Package Manager Console 中输入 - update-database -script
已生成一个名为:SqlQuery_1.sql 的脚本文件。现在,这个文件可以交给 DBA 进行进一步处理。
我将跳过这个文件,因为我将为这个 CP 项目使用第二个参数。资源不足,人员不足,所以负担不起为这个 CP 项目雇佣 DBA :P
**请确保您已将 App.config 文件重新添加到 DiscussionBoard.DataLayer 项目中,以便此命令能够成功运行。**
- update-database 命令带 -verbose 参数。这将更新数据库,并在 Package Manager Console 中提供更新过程的详细信息。
对我们来说,它也将创建数据库,因为我们在开始这个 migrations 文章部分之前已经删除了我们的数据库。
用法:在 Package Manager Console 中输入 - update-database -verbose
-verbose 实际操作 第 1 部分
-verbose 实际操作 第 2 部分 - 因为我们没有数据库,所以运行 Seed 方法。
**请确保您已将 App.config 文件重新添加到 DiscussionBoard.DataLayer 项目中,以便此命令能够成功运行。**
现在,借助 Migrations 的强大功能,我们的数据库创建完成了。
现在,我们将取消注释/重新添加 Comment.cs 类(DiscussionBoard.DomainClasses 项目)中的 NumberOfLikes Property。
所以,我们这次有一个模型更改。我们如何更新数据库以匹配我们最新的代码数据模型... 下一步将详细讨论。
利用 Migrations 的强大功能来传递我们的领域模型更改 - 将 NumberOfLikes 属性添加到数据库。这将反映为 - 一个名为 NumberOfLikes 的新列将被添加到 Comment 表。
在开始之前,NumberOfLikes 列目前尚不存在于 Comment 表中。
注意*:另外值得一提的是,数据库中的 dbo.MigrationHistory 表是 Entity Framework 用来存储哈希值的地方,每次我们添加一个迁移时,Entity Framework 都会根据存储在 dbo.MigrationHistory 中的哈希值来计算数据库模式与代码模型之间的差异。
现在让我们开始 Migrations 过程 -
- 添加新迁移。
为 DiscussionBoard.DataLayer 项目打开 Package Manager Console,然后输入以下命令。AddedNumberOfLikesPropertyToCommentClass 只是迁移的描述。
命令结果
迁移代码类 - 简单:只包含新添加列的定义,用于更新数据库模式。
看,多么方便 - 我们可以拥有一个已存在的数据库,数据库中可以有数据,但我们仍然可以轻松地向该数据库的表中添加列。预先存在的索引、触发器等都不会受到任何影响。太棒了。
- 是时候更新数据库 Schema 了。
为 DiscussionBoard.DataLayer 项目打开 Package Manager Console,然后输入以下命令。
命令结果
注意* 突出显示的部分是存储在 dbo.MigrationHistory 表中的哈希值。
新的列已添加。
在下一节中,我们将看到如何与我们的 DiscussionBoard 数据库进行交互。
与数据库交互 - 在 Discussion 数据库中插入记录
是时候与我们的数据库进行交互了。
我们将使用项目:DiscussionBoard.ConsoleApplication 进行交互。
所以,请跟随我。目前我们的数据库中没有任何讨论数据...
在接下来的几节中,我们将向控制台应用程序添加以下方法。
- Insert Categories 方法实现
所以在下面的代码片段中,我们正在做的是:
Using 块中的 DiscussionBoardContext Context 具有所有必需的 DbSet。DbSet 使我们能够与定义的每种类型进行交互。
在这种情况下,我们将处理 Category 类型的 DbSet。正如在以下代码行中所见:context.Categories.Add(definedCategory);
此外,我们希望将 Category添加到 DbSet。现在 Context已经知道我们要使用 Category 的 DbSet,并且我们使用了 ADD 方法这一事实让 Context 清楚这些 Category 项目需要插入到数据库中。简单。正如在以下代码行中所见:context.Categories.Add(definedCategory);
最后,我们调用 Context 的 SaveChanges() 方法。这将为我们执行 Insert 语句。注意* SaveChanges() 方法将执行 Context 正在跟踪的所有内容。对我们来说,它只跟踪这些新的 Categories 对象,所以它将为每个对象触发一个 Insert 查询。代码行:context.SaveChanges();
在我们运行代码之前,我想向您介绍 EF6 中引入的新日志记录功能。我们所做的只是通过我们的 Context 实例,我们将它的 Log 属性设置为 Console.WriteLine 函数。EF 将在控制台窗口中为我们输出日志。现在控制台窗口将显示 EF 为完成工作所做的所有活动。这是一个不错的/有用的功能,尤其是在开发过程中。代码行:context.Database.Log = Console.WriteLine;
项目:DiscussionBoard.ConsoleApplication 中的 Program.cs 类的代码片段。
注意* 在我们运行之前,请确保您已取消注释Project: DiscussionBoard.ConsoleApplication Class Program.cs Method Main 中对Method InsertCatergories() 的调用,并注释掉该方法和类中的Method GetMembers()。
运行后 - Console Project... 结果(我在 Management Studio 2014 中执行了一个查询)。
日志输出:(欢迎您四处看看 - 查看 EF 的连接、EF 开始的事务等)
查询结果
在我们继续之前 - 请在项目:DiscussionBoard.ConsoleApplication Class Program.cs Method Main 中添加以下代码。我们正在禁用 EF 数据库初始化。当我们使用 Discussion Board Context 时。Database.SetInitializer(new NullDatabaseInitializer<DiscussionBoardContext>());
现在,EF 将只执行与我们指定的 Context 相关的命令。
注意* 这是一个重要的方面,在生产环境中,数据库初始化最好是关闭的。现在您会注意到,EF 只在控制台窗口(日志报告的位置)中触发与我们指定的上下文相关的查询。这有利于性能。
所以,现在如果您重新运行上面的代码 - 您将在控制台中看到 EF 只执行了一个 Insert 命令,而没有其他任何操作,不像以前那样有大量的交互。对性能有利。
注意* 您可能想看看 Context 类的 AddRange 方法。它接受 IEnumerable 并使处理列表变得更容易。无需 Foreach 块。
在继续之前,请对InsertCatergories() Method 进行以下更改。我们进行这些更改,以避免在数据库中保存重复项。我们也可以选择 DropCreateDatabase 作为我们的数据库初始化方案,但我将其留给您探索,因为它相当简单。此外,我想让本文尽可能直观。
_getInitialCategories() 方法... 非常简单。
- Insert Topics 方法实现
既然我们已经开始实现InsertCatergories() Method,
在本节中,我将重点介绍检索记录和插入 Topics 记录。
所以,查询 Categories 表并将 Topics 插入到 Category 对应的 Topics 表中。
InsertTopics() Method
上面表格中的代码是自我解释的,注释也很好地阐明了代码。
让我们运行项目并插入一些 Topics。
在按 F5 之前,请确保取消注释 InsertTopics() Method。
调试执行...
该方法即将被调用...
预期中的 2 条 Categories 记录已被找到...
保存 Topic 记录 - 在将其链接/附加到 2 个已获取的 Categories 之后。
哦哦... 这里有些不对劲... 已经触发了 2 条 Insert Statements 来插入 Categories。
但我们已经有了那些 Categories...
让我们看看数据库...
所以记录 2 和 3 被重新插入。枚举 2 代表体育,3 代表技术。
这不是我们想要的,因此在 Method InsertTopics() 中,我们将 Topic 链接到了已获取的 2 个 Categories。
好吧,我们尝试在这里保存一个实体图(Entity Graph),但未能清晰地表达我们对图的意图。EF 将 Topic 和附加的 Categories 解释为要添加。当我们说context.Topics.Add(topic); 时,EF 将其解释为 Topic 记录及其附加的所有内容都将被添加。
此外,我们用于检索 Categories 的上下文与我们用于插入 Topic 的上下文不同。所以,当前上下文不知道这些 Categories 是从数据库中拉取的(EF 也忽略了这些 Categories 的 Id 列大于零的事实。是的,它就是这样做的)。
我们需要找到一种解决办法。使用 Repository Pattern 和 Unit of Work Pattern 通过使用接口等有许多优雅的解决方案,但我将把这个留给您去解决。目前,我将使用一个简单的方法来解决这个问题。
插入图(Inserting Graphs)。
我们可以通过显式管理 Change Tracker 来解决这个问题。
现在的代码..在InsertTopics() Method 中..
现在,在我们可以测试它之前 - 请遵循以下步骤。
否则您会收到异常 - 登录失败等。
完全删除数据库。
仅为本次运行注释掉以下代码...
是时候构建并运行项目了。
选择 Run without Debugging(不调试运行)。
当控制台窗口弹出时,按任意键继续 ReadKey()。
显然,您需要这样做大约 5 次左右,针对 5 条 Category 记录。
数据库看起来不错。
好了,现在是时候表演了。
取消注释下面的高亮代码。
重新构建并调试代码。
您可以随时在此阶段查看 Context 等。
控制台日志看起来不错。没有记录插入到 Categories 表中。
仅为确保... 查询数据库。
Categories 表
Topics 表
TopicCategories 表(由 Entity Framework 管理和使用)。
一切看起来都不错。
- Insert Members 方法实现
现在,这将是一个相当简单的方法。
我将提供代码片段,让您自行尝试。
另外,我还添加了一些 Topics 的检查代码,以便如果记录已存在,则不会触发插入。
代码...
我另外添加的一些验证代码,用于在触发插入之前进行检查。
现在的 Main 方法看起来像这样...
我的测试运行结果
日志文件看起来还可以。每个 Member Insert 语句后面都有一个 ContactDetail Insert 语句。
数据库查询
欢迎您自行调试和测试 InsertMembers 方法。
将您遇到的任何问题或麻烦留在评论中。
- Insert Interests 方法实现
像好人一样,我们将从验证方法开始。
现在的 Main 方法看起来像这样...
现在是 InsertInterests 方法本身。
我的测试运行结果
日志文件看起来还可以。如预期的那样,只为 Interest 记录触发了 Insert 语句。
数据库查询
欢迎您自行调试和测试 InsertInterests 方法。将您遇到的任何问题或麻烦留在评论中。
- Insert ContactDetails 方法实现
插入 Member 记录时将需要此方法,因此将不需要此方法。请参阅InsertMembers 方法。
另外,ContactDetail 记录不能独立存在。它与 Member 表共享一个零对一关系。
- Insert Comments 方法实现
像好人一样,我们将从验证方法开始。
现在的 Main 方法看起来像这样...
现在是 InsertComments 方法本身。
我的测试运行结果
日志文件看起来还可以。如预期的那样,只为 Comments 记录触发了 Insert 语句。
数据库查询
欢迎您自行调试和测试 InsertComments 方法。将您遇到的任何问题或麻烦留在评论中。
与数据库交互 - 更新和查询 Discussion 数据库
更新数据库...
我们将更新上一节中添加的 Comment 记录。
让我们将上面的评论更新得更有意义。
我们将开始查看一些从数据库检索数据的示例。
下面是一些读取查询的示例。
我们将使用 LINQ 方法从数据库中获取数据。
// Code Snippet // ToList() is an LINQ Execution Method // This will return all the Comments in Comment Table var comments = context.Comments.ToList(); // FirstOrDefault() is an LINQ Execution Method // This will return only the First Fetched comment from the Comment Table var comment = context.Comments.FirstOrDefault(); // This will return all the Comments in Comment Table, // that will MATCH the where condition. // This query will fetch all the commenst that have been posted by Member with Id 2. // the '.where' is not an execution method. // The query is fired when it reached the ToList() LINQ method. var comments = context.Comments.where(c => c.MemberId == 2).ToList(); // The Find Method // The Find Method - is used to reterive a record based on the KEY column // Key is nothing but the Primary Key in the database for that table. int primaryKeyValue = 4; // Fetch the record from Comment Table where Id is 4 var comment = context.Comments.Find(primaryKeyValue);
上述查询将查询数据库,获取相关数据,并为所有已获取的相关记录创建 Comment 的领域/实体对象(在此项目中)。
我们将使用这些(实际上,在此项目中已经使用过)LINQ 方法来从数据库获取数据。
现在,回到手头的任务。更新记录 - 在本例中是某些 Comments。
我们将随机选择一个 Comment 并更新它。
注意* 代码是自我解释的,易于理解。
现在的 Main 方法看起来像这样...
Update 方法...
我的测试运行结果
日志文件看起来还可以。如预期的那样,Update 语句已为 Comment 记录触发。
数据库查询
欢迎您自行调试和测试 UpdateRandomComment 方法。
将您遇到的任何问题或麻烦留在评论中。
注意* 您可以通过以下示例在断开连接状态下更新数据。
在代码注释中提供了说明。
UpdateRandomCommentInDisconnectedState() 方法 -
Main 方法
我的测试运行结果
日志文件看起来还可以。如预期的那样,Update 语句已为 Comment 记录触发。
数据库查询
欢迎您自行调试和测试 UpdateRandomCommentInDisconnectedState 方法。
将您遇到的任何问题或麻烦留在评论中。
与数据库交互 - 删除 Discussion 数据库中的记录
DeleteRandomComment() 方法 -
Main 方法
我的测试运行结果
调试 DeleteRandomComment 方法 -
日志文件看起来还可以。如预期的那样,Delete 语句已为 Comment 记录触发。
数据库查询
欢迎您自行调试和测试 DeleteRandomComment 方法。
将您遇到的任何问题或麻烦留在评论中。
与数据库交互 - 检索 Discussion 数据库中的数据记录图
在本节中,我们将研究以下内容:
我们将研究使用 DbSet.Include() 方法实现的急切加载。
我们将研究使用 Load() 方法实现的显式加载。
实现 RetrievingDataGraphs() 方法。
Main 方法...
调试该方法...
急切加载实际运行 - 已为检索到的 Topic 记录加载 Comments...
显式加载实际运行 - 已为检索到的 Topic 记录加载 Comments...
尚未加载 Comments - 为检索到的 Topic 记录...
至此,本文就结束了。:)
您做得很好。去喝杯冰镇啤酒吧,您配得上。:)
最终想法
到此为止。希望您享受这段旅程。
顺便说一下,我写过几篇关于 WCF Web 服务的文章,请随时根据您的方便查阅。
我该告辞了。如果您有任何问题、反馈以及介于两者之间的任何内容,请随时提出,因为我和您在同一条船上。
附注:那条船叫做“燃烧与学习”。
祝您好运。并继续学习:)
历史
版本 1 提交于 2015 年 11 月 15 日。