Entity Framework 6 中的并发处理(数据库优先方法)






3.93/5 (5投票s)
使用数据库优先方法在 Entity Framework 6 中处理数据并发。
引言
在本文中,我们将学习如何使用 Entity Framework 6 的数据库优先方法处理数据并发冲突。我们将创建一个具有 Entity Framework 数据库优先方法的控制台应用程序,并使用乐观方法处理数据并发冲突。
什么是并发冲突?
并发冲突是指两个或多个用户尝试更新同一条记录的情况,最后更新数据库的用户将覆盖其他用户的更改。如果某些更改被覆盖不会造成严重影响的应用程序,则无需处理并发冲突。但是,如果存在这样一种情况:在您即将更新该记录的时间段内,该记录已被修改,那么您的记录不应在未确认的情况下覆盖当前修改的记录。在这种情况下,我们需要处理并发冲突。
我们可以通过两种方式处理并发冲突。
1. 悲观并发
悲观并发处理并发冲突涉及使用数据库锁。如果用户想从数据库读取一行,用户会请求读锁定或写锁定。如果用户锁定一行以进行写访问,则其他用户将无法请求读锁定或写锁定。但是,如果用户请求读锁定,那么其他用户也可以对其进行读锁定,但不能进行写锁定。
但是,随着应用程序用户数量的增加,管理锁可能很复杂,并且可能导致性能问题。
2. 乐观并发
在乐观并发中,用户在读取行时不会请求锁定。当用户尝试更新该行时,系统必须确定自读取该行以来是否已被另一个用户修改。
总的来说,我们可以这样解释:在乐观并发控制中,在表中引入了一个时间戳类型的版本列。假设初始值为零(实际上它以字节形式存储值,而不是以数字形式)。现在,有两个用户 User1 和 User2 准备更新同一条记录。要更新记录,他们需要先获取记录。获取后,他们将获得具有**相同版本值零**的记录。现在 User1 已进行了更改并单击了保存按钮。在将数据存储到表中时,数据库将首先检查版本值是否相同。由于 User1 现在具有版本值零,这与之前获取的值零相同,因此它将允许保存数据。现在版本值将自动递增到 1,因为它属于**时间戳**类型(文章中稍后将解释时间戳是什么)。
现在 User2 也完成了他的更改并单击了保存按钮。数据库将再次检查版本值,但当 User2 获取值进行更新时,当时其版本值为零,但由于 User1 已经更新了记录,因此表中版本值已更改为 1。因此,版本将不匹配,数据库将不允许用户更新记录。我们将在下面的代码中通过示例进行说明。
乐观并发具有更好的性能,因为它不需要锁定,并且记录的锁定需要额外的服务器资源。
使用代码
我们将创建一个控制台应用程序来探索如何在 Entity Framework 数据库优先方法中处理并发冲突。涉及的步骤如下:
步骤
1. 使用 Visual Studio,创建一个控制台应用程序(文件 -> 新建 -> 项目 -> 控制台应用程序(来自 Visual C# 模板)),并将其命名为 ConcurrencyConflicthandelExample。
2. 在此控制台应用程序中安装 **EntityFramework** Nuget 包。为此,请在程序包管理器控制台中运行“Install-Packag EntityFramework”命令。或者,您也可以通过工具 -> Nuget 程序包管理器 -> 管理解决方案的 Nuget 程序包来安装。
安装 Entity Framework 后,您可以展开**引用**并检查其中是否添加了 Entity Framework DLL。
3. 现在右键单击项目名称,然后单击添加 -> 新项。从那里选择数据 -> ADO.NET Entity Data Model。命名它为 EmployeeManagement。单击**添加**。
单击添加按钮后,将打开实体数据模型向导。由于我们在这里使用的是 Entity Framework 数据库优先方法,因此请选择**EF 设计器来自数据库。**然后单击**下一步**。
单击下一步后,将有一个选择数据连接的选项。在此处单击新建连接。将打开连接属性对话框。选择数据源。选择数据库服务器名称。选择登录类型。选择登录类型并提供必要的凭据后,连接到数据库选项将被启用。在选项框中,它将列出该服务器上的所有数据库。选择数据库名称。您可以通过单击测试连接选项来测试连接状态。现在单击**确定**。
单击确定按钮后,向导中的所有字段都将被填充。以黑色标记的字段是我们将在应用程序中使用的数据库。以红色标记的字段是将在配置文件中使用的完整连接字符串。以黄色标记的字段是负责与数据库进行交互的上下文类文件名。默认情况下,Entity Framework 会在选择的数据库名称后附加 Entity,这将是您的上下文文件名。但如果您愿意,也可以更改它。单击**下一步**。
单击下一步后,将出现**选择数据库对象和设置**选项。展开表选项并选择您要处理的所有表。单击**完成**。
单击完成,您的所有设置都已准备就绪。Entity Framework 将生成所有 tt 文件、上下文文件、edmx 文件。单击完成时,您将看到一个警告对话框弹出,提示“运行此文本模板可能会损害您的计算机。如果从不受信任的源获取,请不要运行它。”此警告仅仅是因为您的 tt 文件已在内部更改。只需单击确定即可。您将看到您的 edmx 文件已准备好所有关系。
以上就是所有代码设置。现在我们将执行一些操作,即我们本次讨论的主要议题,即并发冲突处理。
5. 在数据库中打开您要处理并发冲突的表。在我的例子中是 Department 表。
6. 添加一个名为 versionholder 的新列(您可以给出任何合适的名称)。选择其数据类型为 timestamp。**Timestamp** 暴露了一个由数据库自动生成、唯一的二进制数字,存储大小为 8 字节。
7. 现在打开您项目中的 .edmx 文件,在 Visual Studio 中。通过右键单击 .edmx 文件中的任意位置并选择**从数据库更新模型**选项来更新 Department 的表图。
您会注意到在 department 模型中,我们刚刚在数据库中添加的 versionholder 列仍然不存在。
8. 单击更新模型后,将打开一个更新向导对话框。转到刷新选项,选择表,然后选择表名。
现在您将看到,我们已添加到数据库的列现在也已添加到我们的 .edmx 文件中。
注意*
如果在任何情况下您想重命名数据库中添加的 Versionholder 列或现有列,那么在更新 edmx 模型时,您可能会遇到模型中有两个属性的问题。第一个属性是旧的,第二个属性是更新后的名称,.cs 文件也将包含两个属性。在这种情况下,只需右键单击模型头部并选择“从模型中删除”选项来删除 edmx 模型。删除模型后,再次通过右键单击 .edmx 文件中的任意位置并选择“从数据库更新模型”来添加该模型。在此,从更新向导中,选择添加选项,然后从刚才删除的表选项中选择表名。
9. 现在右键单击新添加的属性,然后选择**属性**选项。
10. 在“常规”属性下,将**并发模式**从“无”设置为**固定**。将并发模式设置为固定只会使版本值在每次更新时自动递增。
以上就是演示上面乐观并发定义中解释的示例的全部设置。现在我们将通过代码来了解它是如何工作的。
下面是处理并发冲突的完整代码结构。我将在下面解释代码。
namespace ConcurrencyConflictHandelExample
{
class Program
{
static void Main(string[] args)
{
Department User1 = null;
Department User2 = null;
//First User Brings The Data
using (TestDBEntities context = new TestDBEntities())
{
User1 = context.Departments.Find(2);
}
//Second User Brings the same Data
using (TestDBEntities context = new TestDBEntities())
{
User2 = context.Departments.Find(2);
}
//User1 updating the record
using (TestDBEntities context = new TestDBEntities())
{
context.Departments.Attach(User1);
User1.Credits += 1;
context.SaveChanges();
}
//User1 updating the same record
using (TestDBEntities context = new TestDBEntities())
{
context.Departments.Attach(User2);
User2.Credits += 2;
context.SaveChanges();
}
Console.Read();
}
}
}
首先,我们创建了两个用户来更新部门数据。
Department User1 = null;
Department User2 = null;
现在,使用上下文类,两个用户将获取相同的部门表记录。
//First User Brings The Data
using (TestDBEntities context = new TestDBEntities())
{
User1 = context.Departments.Find(2);
}
//Second User Brings the same Data
using (TestDBEntities context = new TestDBEntities())
{
User2 = context.Departments.Find(2);
}
这里两个用户将获得相同的部门数据,且版本值相同。假设这里两个用户获取的版本值都是零。
现在 User1 将更新部门记录。
//User1 updating the record
using (TestDBEntities context = new TestDBEntities())
{
context.Departments.Attach(User1);
User1.Credits += 1;
context.SaveChanges();
}
这里发生的情况是,在保存时,Entity Framework 将首先将此更新记录的版本值与现有记录进行比较。由于此时尚未进行任何更新,因此版本值不会改变,并且比较结果为真,数据将被保存。您可以在下方查看 Entity Framework 执行此操作时生成的查询。
现在 User2 将更新部门数据。
//User2 updating the same record
using (TestDBEntities context = new TestDBEntities())
{
context.Departments.Attach(User2);
User2.Credits += 2;
context.SaveChanges();
}
由于 User2 仍然拥有版本值零,因此在进行数据操作并尝试保存数据时,版本值将不匹配,因为 User1 更新记录时版本值已被更改。我们将在 SQL Profiler 中查看 Entity Framework 生成的查询中的版本值。
您可以看到 Credits 值不同,即 User2 设置的 43,但版本值与 User1 相同。但是实际的版本值已更改,因此不会匹配,并会抛出“DbUpdateConcurrencyException”。
我们可以使用此异常来警告第二个用户屏幕上的记录已被修改。这就是我们可以处理并发冲突的方式。
但是,如果要求在发生并发冲突时不在屏幕上显示警告,而是直接在代码中处理它呢?是的,我们也可以做到。让我们通过代码来看看。
这里的思路是获取当前数据库值,并将它们设置为实体的原始值。
//User1 updating the same record
using (TestDBEntities context = new TestDBEntities())
{
context.Departments.Attach(User2);
User2.Credits +=2;
try
{
context.SaveChanges();
}
catch(DbUpdateConcurrencyException ex)
{
var value = ex.Entries.Single();
value.OriginalValues.SetValues(value.GetDatabaseValues());
context.SaveChanges();
}
}
现在它将保存值而不会抛出异常。
结论
在本文中,我们学习了如何配置 Entity Framework 数据库优先方法,并通过乐观方法处理并发冲突。