65.9K
CodeProject 正在变化。 阅读更多。
Home

Entity Framework Code First 中的乐观并发

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (28投票s)

2014年10月2日

CPOL

5分钟阅读

viewsIcon

73633

downloadIcon

1128

使用 Entity Framework Code First 处理乐观并发。

引言

本文介绍了使用 Entity Framework Code First 处理乐观并发的各种配置。

并发

在计算机科学中,并发是指多个计算同时执行并可能相互交互的系统的属性。

在 Web 应用程序(这是一个多用户环境)中,数据库数据保存过程中存在并发的可能性。并发大致分为两种类型:1)悲观并发 2)乐观并发

1) 悲观并发

数据库中的悲观并发涉及锁定行,以防止其他用户以影响当前用户的方式修改数据。

在这种方法中,用户执行一个会应用锁的操作,其他用户在释放该锁之前不能对该记录执行相同的操作。

2) 乐观并发

相比之下,在乐观并发中,用户读取行时不会对其进行锁定。当用户尝试更新该行时,系统必须确定该记录自读取以来是否已被其他用户修改。

使用代码

让我们创建一个控制台应用程序来探索处理乐观并发的各种选项。

步骤

  1. 使用 Visual Studio,创建一个控制台应用程序(*文件 -> 新建 -> 项目 -> 控制台应用程序(来自 Visual C# 模板)*),并将其命名为**ConcurrencyCheck**。
  2. 向项目中添加一个新的文件夹**Models**。在此文件夹内添加两个类文件:**EducationContext.cs** 和 **Student.cs**。
  3. 在此控制台应用程序中安装 **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 配置来配置一个或多个字段进行并发检查。

非 Timestamp 字段的配置
约定
数据注释 [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(),您将收到如下异常:

DbUpdateConcurrencyException

Store 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 配置。

© . All rights reserved.