Entity Framework Code First 中的乐观并发






4.75/5 (28投票s)
使用 Entity Framework Code First 处理乐观并发。
引言
本文介绍了使用 Entity Framework Code First 处理乐观并发的各种配置。
并发
在计算机科学中,并发是指多个计算同时执行并可能相互交互的系统的属性。
在 Web 应用程序(这是一个多用户环境)中,数据库数据保存过程中存在并发的可能性。并发大致分为两种类型:1)悲观并发 2)乐观并发
1) 悲观并发
数据库中的悲观并发涉及锁定行,以防止其他用户以影响当前用户的方式修改数据。
在这种方法中,用户执行一个会应用锁的操作,其他用户在释放该锁之前不能对该记录执行相同的操作。
2) 乐观并发
相比之下,在乐观并发中,用户读取行时不会对其进行锁定。当用户尝试更新该行时,系统必须确定该记录自读取以来是否已被其他用户修改。
使用代码
让我们创建一个控制台应用程序来探索处理乐观并发的各种选项。
步骤
- 使用 Visual Studio,创建一个控制台应用程序(*文件 -> 新建 -> 项目 -> 控制台应用程序(来自 Visual C# 模板)*),并将其命名为**ConcurrencyCheck**。
- 向项目中添加一个新的文件夹**Models**。在此文件夹内添加两个类文件:**EducationContext.cs** 和 **Student.cs**。
- 在此控制台应用程序中安装 **EntityFramework** Nuget 包。请在程序包管理器控制台中运行“*Install-Package EntityFramework*”命令以完成此操作。或者,您也可以从“NuGet 程序包管理器”窗口安装。
下表显示了乐观并发的各种配置。
约定 | 无 |
数据注释 | [Timestamp] |
Fluent API | .IsRowVersion() |
1) 约定
Entity Framework Code First 没有处理乐观并发的约定。您可以使用数据注释或 Fluent API 来处理乐观并发。
2) 数据注释
Code First 使用 **[Timestamp]** 属性来处理乐观并发。
a) 如下修改 *EducationContext.cs* 文件
using System.Data.Entity;
namespace ConcurrencyCheck.Models
{
class EducationContext : DbContext
{
public EducationContext()
: base("EducationContext")
{
}
public DbSet<Student> Students { get; set; }
}
}
此处 **base("EducationContext")** 指示 Code First 使用 *App.config* 文件中名为 "EducationContext" 的连接字符串。
b) 如下修改 *Student.cs* 文件
using System.ComponentModel.DataAnnotations;
namespace ConcurrencyCheck.Models
{
public class Student
{
public int StudentId { get; set; }
public string RollNumber { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
}
请注意,*Student* 类中有一个名为 **RowVersion** 的属性,其类型为 *byte[]*,并已分配了 *[Timestamp]* 属性以处理乐观并发。
c) 更改 **App.config** 文件中的连接字符串以指向有效数据库
<connectionStrings>
<add name="EducationContext" providerName="System.Data.SqlClient" connectionString="Server=DUKHABANDHU-PC; Database=ConcurrencyCheck;Integrated Security=SSPI" />
</connectionStrings>
此处我们为数据库名称指定了 **ConcurrencyCheck**,它将在应用程序运行时由 Code First 创建。
d) 修改 *Program.cs* 文件,以便每次应用程序运行时都删除并重新创建数据库
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<EducationContext>());
using (var context = new EducationContext())
{
context.Students.Add(new Student
{
FirstName = "Dukhabandhu",
LastName = "Sahoo",
RollNumber = "1"
});
context.SaveChanges();
}
Console.WriteLine("Database Created!!!");
Console.ReadKey();
}
如果运行应用程序,Code First 将创建名为 **ConcurrenCheck** 的数据库,其中包含两个表:*_MigrationHistory* 和 *Students*。
如果在 SQL Server 中查看 *Students* 表的 *RowVersion* 列,其数据类型为 *timestamp*。
**RowVersion** 和 **TimeStamp** 是不同数据库提供商用于相同目的的两个术语。当 *Students* 表中的记录被创建或更新时,数据库会自动将 RowVersion 值更新为新值。即使您发送了 RowVersion 列的值,数据库(SQL Server)也不会使用该值来插入或更新行版本。
添加新记录到 *Students* 表时生成的 SQL
exec sp_executesql N'INSERT [dbo].[Students]([RollNumber], [FirstName], [LastName])
VALUES (@0, @1, @2)
SELECT [StudentId], [RowVersion]
FROM [dbo].[Students]
WHERE @@ROWCOUNT > 0 AND [StudentId] = scope_identity()',N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 nvarchar(max) ',@0=N'1',@1=N'Dukhabandhu',@2=N'Sahoo'
您可以看到查询不仅插入了一条新记录,还返回了 RowVersion 的值。
实际的并发检查发生在 UPDATE 和 DELETE 操作进行时。请看下面当从 *Students* 表更新和删除记录时并发检查是如何发生的。
UPDATE SQL
exec sp_executesql N'UPDATE [dbo].[Students]
SET [RollNumber] = @0 WHERE (([StudentId] = @1) AND ([RowVersion] = @2))
SELECT [RowVersion] FROM [dbo].[Students]
WHERE @@ROWCOUNT > 0 AND [StudentId] = @1',N'@0 nvarchar(max) ,@1 int,@2 binary(8)',@0=N'2',@1=1,@2=0x00000000000007D1
查看 *WHERE* 条件,它在更新记录时同时比较了 StudentId(主键)和 RowVersion。
DELETE SQL
exec sp_executesql N'DELETE [dbo].[Students]
WHERE (([StudentId] = @0) AND ([RowVersion] = @1))',N'@0 int,@1 binary(8)',@0=1,@1=0x00000000000007D1
此处,在删除记录之前,Code First 也会创建查询来比较标识符(即主键 *StudentId*)和行版本(*RowVersion* 字段),以实现乐观并发。
3) Fluent API
Fluent API 使用 **IsRowVersion()** 方法配置乐观并发。
要测试 Fluent API 配置,请从 *Students* 类的 *RowVersion* 属性中删除 *[Timestamp]* 属性,并如下重写 *EducationContext* 类中的 **OnModelCreating()** 方法
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().Property(s => s.RowVersion).IsRowVersion();
base.OnModelCreating(modelBuilder);
}
非 Timestamp 字段的配置
在不保留专用列进行并发检查的情况下,您仍然可以处理并发。有些数据库不支持 *RowVersion/Timestamp* 类型的列。在这些情况下,您可以使用数据注释或 Fluent API 配置来配置一个或多个字段进行并发检查。
约定 | 无 |
数据注释 | [ConcurrencyCheck] |
Fluent API | .IsConcurrencyToken() |
1) 数据注释
如下修改 *Student* 类以使用 *[ConcurrencyCheck]* 数据注释属性
public class Student
{
public int StudentId { get; set; }
[ConcurrencyCheck]
public string RollNumber { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
当应用程序运行时,Code First 会创建 **Students** 表(请参见下图)。数据库不会为 *RollNumber* 列中的 *[ConcurrencyCheck]* 属性执行任何特殊操作。
但是,当对 Students 表进行任何修改/更改时,Code First 会负责并发检查。请继续阅读 Code First 如何考虑并发检查来创建 UPDATE 和 DELETE 查询。
UPDATE SQL
exec sp_executesql N'UPDATE [dbo].[Students]
SET [RollNumber] = @0
WHERE (([StudentId] = @1) AND ([RollNumber] = @2))
',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'2',@1=1,@2=N'1'
注意 *WHERE* 条件。它在更新记录时同时比较了 *StudentId*(主键)和 *RollNumber*。
DELETE SQL
exec sp_executesql N'DELETE [dbo].[Students]
WHERE (([StudentId] = @0) AND ([RollNumber] = @1))',N'@0 int,@1 nvarchar(max) ',@0=1,@1=N'2'
当从 *Students* 表删除记录时,它也会检查 *StudentId* 和 *RollNumber* 列的值。如果自读取以来 *RollNumber* 列的值已更改,而您现在正在更新该记录,则会收到一个**OptimisticConcurrencyException**。
2) Fluent API
使用 Code First 的 **IsConcurrencyToken()** 方法来处理非 Timestamp 字段的并发。
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().Property(s => s.RollNumber).IsConcurrencyToken();
base.OnModelCreating(modelBuilder);
}
关注点
要测试并发效果,请添加代码以更新 Students 表中的现有记录,如下所示
var student = context.Students.FirstOrDefault(u => u.StudentId == 1);
if (student != null)
{
student.RollNumber = "2";
context.SaveChanges();
}
在 Visual Studio 中,在 *context.SaveChanges();* 行上添加一个**断点**。在执行 **SaveChanges()** 方法之前,修改数据库中 StudentId = 1 的 Students 表记录。
UPDATE Students SET RollNumber = '123' WHERE StudentId = 1;
现在,如果您执行下一行,即 context.SaveChanges(),您将收到如下异常:
DbUpdateConcurrencyExceptionStore update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.
*DbUpdateConcurrencyException* 被抛出是因为自读取以来记录已被修改。
结论
在本文中,我们学习了如何配置 Entity Framework Code First 来处理乐观并发,无论是通过在表中保留专用字段,还是通过添加特殊的数据注释属性或 Fluent API 配置。