يمنع التحكم المتزامن المتفائل فقدان البيانات بسبب تحديثات الفوز الأخير مع عمليات الوصول المتعددة (SQL Server)

تحديث الصفحة :
تاريخ إنشاء الصفحة :

بيئة التشغيل

فيجوال ستوديو
  • فيجوال ستوديو 2022
.صافي
  • .NET 8
جوهر إطار عمل الكيان
  • إطار عمل الكيان الأساسي 8.0
خادم SQL
  • خادم SQL 2022

* ما ورد أعلاه هو بيئة تحقق ، ولكنه قد يعمل مع إصدارات أخرى.

حول الفوز بالتحديثات بعد عدم التحكم

في تطبيق ويب أو تطبيق خادم عميل ، يمكن لعدة أشخاص الوصول إلى البيانات وتحديثها من قاعدة بيانات واحدة. إذا لم يتم فعل أي شيء على وجه الخصوص ، فستظهر بيانات الشخص الذي قام بتحديثه لاحقا في قاعدة البيانات على أنها الأحدث.

عادة ، لا توجد مشكلة معينة في أن بيانات الشخص الذي قام بالتحديث لاحقا تنعكس على أنها الأحدث. يمكن أن تنشأ مشاكل عندما يحاول عدة أشخاص الوصول إلى نفس البيانات وتحديثها في نفس الوقت.

على سبيل المثال، افترض أن لديك بيانات الكتاب التالية.

قيمة اسم المعلمة
اسم الكتاب كتب قواعد البيانات
ثمن 1000

إذا فتح شخصان الشاشة لتحرير هذه البيانات في نفس الوقت ، عرض القيمة أعلاه. يحاول السيد / السيدة أ رفع سعر هذا الكتاب بمقدار 500 ين. تم توجيه السيد / السيدة ب لاحقا لرفع سعر هذا الكتاب بمقدار 300 ين أخرى.

إذا رفع الاثنان السعر بشكل منفصل بدلا من نفس الوقت ، فسيكون سعر الكتاب 1800 ين. إذا قمت بالوصول إليه في نفس الوقت ، تسجيله على أنه 1500 ين و 1300 ين ، على التوالي ، لذلك لن يكون 1800 ين بغض النظر عن أي واحد يسجل.

المشكلة هنا أن الشخص الذي قام بالتحديث لاحقا يمكنه تحديثه دون معرفة المعلومات التي تم تحديثها مسبقا.

تزامن متفائل

يمكن حل المشكلة المذكورة أعلاه عن طريق إجراء "التحكم في التزامن المتفائل" هذه المرة. لشرح ببساطة ، ما هو نوع التحكم في "الفوز أولا إذا حاولت تحرير البيانات في نفس الوقت". أولئك الذين يحاولون التجديد لاحقا سيتلقون خطأ في وقت التحديث ولن يتمكنوا من التسجيل.

قد تعتقد أنه لا يمكنك تسجيل بيانات جديدة بهذا ، ولكن هذا فقط "عندما تحاول تغييره في نفس الوقت". من الممكن لشخصين التعديل في أوقات مختلفة تماما. بالطبع ، في هذه الحالة ، ستكون بيانات آخر شخص محدث هي الأحدث.

على وجه التحديد ، ما نوع التحكم في المعالجة الذي يمكن تحقيقه من خلال "الحصول على نسخة من البيانات". على سبيل المثال ، في المثال أعلاه ، سيكون لديك البيانات التالية.

قيمة اسم المعلمة
اسم الكتاب كتب قواعد البيانات
ثمن 1000
الإصدار 1

يتم زيادة الإصدار بمقدار 1 لكل تحديث سجل. على سبيل المثال ، إذا قام السيد / السيدة A بتعيين السعر على 1500 ين ، فسيكون الإصدار 2. في ذلك الوقت، الشرط الذي يمكن إجراء التحديث هو أن الإصدار قبل التحديث هو نفس الإصدار على قاعدة البيانات. عندما يقوم السيد / السيدة بتحديثه ، يكون الإصدار الموجود في قاعدة البيانات هو 1 ، وإصدار البيانات الأصلية التي يتم تحريرها حاليا هو 1 ، لذلك يمكن تحديثها.

بناء على هذه المواصفات ، افترض موقفا يقوم فيه السيد / السيدة أ والسيد / السيدة ب بتحرير نفس البيانات. عندما حدد السيد / السيدة السعر لأول مرة إلى 1500 ين وحاول تحديث قاعدة البيانات ، كان الإصدار هو نفسه ، لذلك يمكن تحديثه. في هذه الحالة ، سيكون الإصدار الموجود على قاعدة البيانات هو 2. يحاول السيد / السيدة ب تحرير بيانات 1000 ين وتحديثها إلى قاعدة البيانات ك 1300 ين. فشل التحديث لأن الإصدار في متناول اليد هو 1 ، ولكن الإصدار الموجود على قاعدة البيانات هو بالفعل 2. هذه هي الطريقة التي يعمل بها التزامن المتفائل.

يتضمن Entity Framework Core هذا "التزامن المتفائل" خارج الصندوق ، مما يجعله سهل التنفيذ نسبيا.

بالمناسبة ، يعرف "التزامن المتفائل" أيضا باسم "القفل المتفائل" أو "القفل المتفائل" ، ويتم فحصه والتحدث عنه أحيانا بهذا الاسم. إنها طريقة قفل لا يمكن تحديث البيانات ، ولكن يمكن قراءة البيانات. هناك أيضا عنصر تحكم يسمى "القفل المتشائم" كطريقة قفل أخرى غير "القفل المتفائل". هذه طريقة تقفل تحميل البيانات عندما يقرأ الشخص الأول البيانات ولا تسمح حتى بعمليات التحرير. يمكن أن يحل مشكلة عدم إمكانية تحديث البيانات على الرغم من تغييرها ، ولكن عندما يقوم شخص ما بتحرير البيانات ، لا يمكن للأشخاص الآخرين فتح شاشة تحرير البيانات ، وإذا فشل إلغاء القفل ، قفل البيانات إلى الأبد. كلاهما له مزايا وعيوب ، لذلك يعتمد على العملية التي يجب اعتمادها.

إنشاء قاعدة بيانات

في هذه المقالة ، سأشرح كيفية إنشاء قاعدة بيانات ل SQL Server أولا ثم إنشاء التعليمات البرمجية تلقائيا. إذا كنت ترغب في تنفيذه بطريقة التعليمات البرمجية أولا ، فيرجى الرجوع إلى الكود الذي تم إنشاؤه تلقائيا هذه المرة وتنفيذه في الإجراء العكسي.

إنشاء قاعدة بيانات

يمكنك أيضا جعله بتنسيق SQL ، ولكن من الأسهل جعله باستخدام واجهة المستخدم الرسومية ، لذلك أقوم بإنشائه باستخدام واجهة المستخدم الرسومية هذه المرة. باستثناء اسم قاعدة البيانات ، يتم إنشاؤه افتراضيا.

إنشاء جدول

قم بإنشائه باستخدام SQL التالي:

USE [TestDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Book](
	[ID] [int] NOT NULL,
	[Name] [nvarchar](100) NOT NULL,
	[Price] [money] NOT NULL,
	[RowVersion] [timestamp] NOT NULL,
 CONSTRAINT [PK_Book] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

لا داعي للقلق بشأن معظم المعلمات لأنها مخصصة فقط لتحديثات البيانات. معلمة الاهتمام هذه المرة هي RowVersion العمود الذي يصف . هذا هو إصدار السجل. من خلال تحديد النوع timestamp ، يتم زيادة الإصدار تلقائيا في كل مرة يتم فيها تحديث السجل. أيضا ، نظرا لأن هذا الإصدار تتم إدارته على أساس كل جدول ، فلا يوجد سجل أساسي لنفس الإصدار ما لم تقم بتعيينه يدويا.

إضافة سجل

يمكنك إضافته باستخدام SQL التالي:

begin transaction;
insert into Book ([ID],[Name],[Price]) values (1,'C#の本','1000.00');
insert into Book ([ID],[Name],[Price]) values (2,'VB.NETの本','1500.00');
insert into Book ([ID],[Name],[Price]) values (3,'SQL Serverの本','2000.00');
commit;

RowVersion لا تحتاج إلى تعيين الأعمدة لأنه يتم تعيينها تلقائيا.

إنشاء مشروع وإنشاء التعليمات البرمجية تلقائيا

هذه المرة ، سوف نتحقق من العملية باستخدام تطبيق وحدة التحكم. يتم وصف خطوات إنشاء مشروع وإنشاء التعليمات البرمجية تلقائيا في النصائح التالية ، لذلك لن أخوض فيها هنا.

الكود الذي تم إنشاؤه هو كما يلي:

TestDatabaseDbContext.cs

using Microsoft.EntityFrameworkCore;

namespace ConcurrencySqlServer.Models.Database;

public partial class TestDatabaseDbContext : DbContext
{
  public TestDatabaseDbContext() { }

  public TestDatabaseDbContext(DbContextOptions<TestDatabaseDbContext> options) : base(options) { }

  public virtual DbSet<Book> Book { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
    => optionsBuilder.UseSqlServer("Data Source=<サーバー名>;Database=<データベース名>;user id=<ユーザー名>;password=<パスワード>;TrustServerCertificate=true");

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder.Entity<Book>(entity =>
    {
      entity.Property(e => e.ID).ValueGeneratedNever();
      entity.Property(e => e.RowVersion)
        .IsRowVersion()
        .IsConcurrencyToken();
    });

    OnModelCreatingPartial(modelBuilder);
  }

  partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

Book.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ConcurrencySqlServer.Models.Database;

public partial class Book
{
  [Key]
  public int ID { get; set; }

  [StringLength(100)]
  public string Name { get; set; } = null!;

  [Column(TypeName = "money")]
  public decimal Price { get; set; }

  public byte[] RowVersion { get; set; } = null!;
}

التحقق من تشغيل التحكم في التزامن المتفائل

نظرا لأننا نقوم بتشغيله في تطبيق واحد هذه المرة ، فإننا لا نصل إليه بشكل صارم في نفس الوقت ، لكننا نرغب في تنفيذه بشكل قريب منه.

كما في المثال في البداية ، يتم الحصول على قطعتين من البيانات ، وعندما يتم تحديث كل منهما بناء على البيانات الأولى ، تحقق مما إذا كان المحدث الأخير سيحصل على خطأ.

Program.cs

using ConcurrencySqlServer.Models.Database;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

Console.WriteLine("Hello, World!");

// 2人が別々にデータベースにアクセスする想定で2つのデータベースコンテキストを作成
Console.WriteLine("データベースコンテキストを作成します。");
using var dbContextA = new TestDatabaseDbContext();
using var dbContextB = new TestDatabaseDbContext();
Console.WriteLine("データベースコンテキストを作成しました。");
Console.WriteLine("");

// それぞれがデータを編集しようと読み込む
Console.WriteLine("ID:1 の Book を読み込みます。");
var bookA = dbContextA.Book.First(x => x.ID == 1);
var bookB = dbContextB.Book.First(x => x.ID == 1);
Console.WriteLine("ID:1 の Book を読み込みました。");
Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
Console.WriteLine("");


// A の人が最初にデータベースに更新する
bookA.Price += 500;
UpdateToDatabase(dbContextA, "A");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
Console.WriteLine("");

// そのあと B の人が最初にデータベースに更新する
bookB.Price += 300;
UpdateToDatabase(dbContextB, "B");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
Console.WriteLine("");

Console.WriteLine("処理を終了します。");


// データベースに更新するメソッド
void UpdateToDatabase(TestDatabaseDbContext dbContext, string updateUser)
{
  try
  {
    Console.WriteLine($"{updateUser} のデータベースコンテキストを変更保存します。");
    dbContext.SaveChanges();
    Console.WriteLine($"{updateUser} のデータベースコンテキストを変更保存しました。");
  }
  catch (DbUpdateConcurrencyException ex)
  {
    Console.WriteLine($"{updateUser} の更新でエラーが発生しました。");
    Console.WriteLine(ex.Message);
  }
}

إنها قصة طويلة ، لكن معظمها مكتوب على وحدة التحكم.

نتيجة التنفيذ هي على النحو التالي.

Hello, World!
データベースコンテキストを作成します。
データベースコンテキストを作成しました。

ID:1 の Book を読み込みます。
ID:1 の Book を読み込みました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}

A のデータベースコンテキストを変更保存します。
A のデータベースコンテキストを変更保存しました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

B のデータベースコンテキストを変更保存します。
B の更新でエラーが発生しました。
The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1300.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

処理を終了します。

سأقسمها إلى أقسام.

Console.WriteLine("Hello, World!");

// 2人が別々にデータベースにアクセスする想定で2つのデータベースコンテキストを作成
Console.WriteLine("データベースコンテキストを作成します。");
using var dbContextA = new TestDatabaseDbContext();
using var dbContextB = new TestDatabaseDbContext();
using var dbContextC = new TestDatabaseDbContext();
Console.WriteLine("データベースコンテキストを作成しました。");

أقوم بإنشاء سياقين لقاعدة البيانات ، إذا قمت بمشاركة سياق قاعدة بيانات واحد ، تخزينه مؤقتا عند قراءة البيانات وسيكون نفس المثيل. بافتراض أنه يتم الوصول إلى كل منها على حدة ، يتم إنشاء سياقي قاعدة بيانات. dbContextC للتحقق من القيم في قاعدة البيانات. لا أحتاجه حقا لأنه يمكن استبدال A أو B.

// それぞれがデータを編集しようと読み込む
Console.WriteLine("ID:1 の Book を読み込みます。");
var bookA = dbContextA.Book.First(x => x.ID == 1);
var bookB = dbContextB.Book.First(x => x.ID == 1);
Console.WriteLine("ID:1 の Book を読み込みました。");
Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");

بافتراض أنه يتم الوصول إليها بشكل منفصل ، فإنهم يقرؤون واحدة من سياقات Book قاعدة بيانات مختلفة . في هذه المرحلة ، لم نغير أي شيء ، لذا فهم جميعا متشابهون.

ID:1 の Book を読み込みます。
ID:1 の Book を読み込みました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}

أولا ، قمت بتغيير وتحديث كتاب السيد / السيدة. يتم UpdateToDatabase تلخيص عملية التحديث في أسلوب، ويتم عرض رسالة عند حدوث استثناء.

// A の人が最初にデータベースに更新する
bookA.Price += 500;
UpdateToDatabase(dbContextA, "A");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
A のデータベースコンテキストを変更保存します。
A のデータベースコンテキストを変更保存しました。
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1000.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

نتيجة لذلك ، تم تحديثه بنجاح ، وتم Price RowVersion تحديث الكتاب أ. أيضا ، إذا قرأت قيمة قاعدة البيانات مباشرة ، فستكون القيمة المحدثة هي نفسها القيمة المحدثة. تمكنت من تحديثه لأن كلاهما حصل في RowVersion A وفي RowVersion DB كانا AAAAAAAAH2k= . تم تغيير AAAAAAAAH2o= الإصدار بسبب التحديث.

بعد تحديث A ، قم بتحديث B بنفس الطريقة.

// そのあと B の人が最初にデータベースに更新する
bookB.Price += 300;
UpdateToDatabase(dbContextB, "B");

Console.WriteLine($"A  Book : {JsonSerializer.Serialize(bookA)}");
Console.WriteLine($"B  Book : {JsonSerializer.Serialize(bookB)}");
Console.WriteLine($"DB Book : {JsonSerializer.Serialize(dbContextC.Book.AsNoTracking().First(x => x.ID == 1))}");
B のデータベースコンテキストを変更保存します。
B の更新でエラーが発生しました。
The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
A  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}
B  Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1300.0000,"RowVersion":"AAAAAAAAH2k="}
DB Book : {"ID":1,"Name":"C#\u306E\u672C","Price":1500.0000,"RowVersion":"AAAAAAAAH2o="}

والنتيجة هي استثناء DbUpdateConcurrencyException وفشل التحديث. bookB.RowVersion AAAAAAAAH2k= هو ، ولكن RowVersion منذ ذلك الحين AAAAAAAAH2o= بالفعل ، يتم الحكم عليه على أنه عدم تطابق ويحدث خطأ في التحديث. يمكنك أن ترى أن Price المتغير bookB المحلي قد تغير ، لكن RowVersion يمكنك أن ترى أن القيم الموجودة على جانب قاعدة البيانات لم تتغير على الإطلاق.

ملخص

هذا هو الأسهل من بين أنواع الأقفال العديدة التي يجب تنفيذها ، لأنه عند استخدام التعليمات البرمجية التي تم إنشاؤها تلقائيا في SQL Server و Entity Framework Core ، يتم تنفيذ التزامن المتفائل افتراضيا. ومع ذلك ، نظرا لأنه فقط لمنع "تحديث تلف البيانات" ، فمن الضروري التعامل بشكل صحيح مع الاستثناءات عند تضمين بيانات أخرى أو مشاركة عمليات المستخدم.

أيضا ، هذه المرة لم أتمكن من إدارة أي شيء لأنني قمت بتطبيقه RowVersion في تطبيق وحدة تحكم بسيط. إذا كنت تريد إدراج شاشة التحرير بعد تحميل البيانات في تطبيق ويب أو تطبيق عميل ، RowVersion بطريقة ما بحيث يمكن تحديدها بشكل صحيح عند تحديثها.

ومع ذلك ، يحتوي Entity Framework Core على وظيفة تتبع التغيير ، لذلك إذا كنت ترغب في تعيين القديم RowVersion على القيمة المقروءة من قاعدة البيانات في وقت التحديث ،

bookB.RowVersion = <古い RowVersion>;

حتى إذا تم تعيينه على النحو التالي ، فلن يتم الحكم عليه بشكل صحيح على أنه "تحكم متزامن متفائل". RowVersion حتى إذا قمت بتعيين القيمة إلى بشكل طبيعي ، التعرف عليها فقط على أنها القيمة التي تم تغييرها ، وبالتالي فإن ما يلي هو

dbContextB.Entry(bookB).Property("RowVersion").OriginalValue = <古い RowVersion>;

من الضروري إعادة كتابة القيمة قبل التغيير.