การควบคุมการทํางานพร้อมกันในแง่ดีป้องกันการสูญหายของข้อมูลเนื่องจากการอัปเดตที่ชนะล่าสุดด้วยการเข้าถึงหลายครั้ง (SQL Server)

ปรับปรุงหน้า :
วันที่สร้างเพจ :

สภาพแวดล้อมในการทํางาน

วิชวลสตูดิโอ
  • วิชวลสตูดิโอ 2022
ตาข่าย
  • .NET 8
แกน Entity Framework
  • Entity Framework Core 8.0
เซิร์ฟเวอร์ SQL
  • SQL Server 2022

* ข้างต้นเป็นสภาพแวดล้อมการตรวจสอบ แต่อาจใช้ได้กับเวอร์ชันอื่น

เกี่ยวกับการชนะการอัปเดตหลังจากไม่มีการควบคุม

ในเว็บแอปพลิเคชันหรือแอปพลิเคชันไคลเอ็นต์เซิร์ฟเวอร์ หลายคนอาจเข้าถึงและอัปเดตข้อมูลจากฐานข้อมูลเดียว หากไม่มีการดําเนินการใดๆ เป็นพิเศษ ข้อมูลของผู้ที่อัปเดตในภายหลังจะปรากฏในฐานข้อมูลเป็นข้อมูลล่าสุด

โดยปกติจะไม่มีปัญหาเฉพาะที่ข้อมูลของบุคคลที่อัปเดตในภายหลังจะสะท้อนเป็นข้อมูลล่าสุด ปัญหาอาจเกิดขึ้นได้เมื่อหลายคนพยายามเข้าถึงและอัปเดตข้อมูลเดียวกันในเวลาเดียวกัน

ตัวอย่างเช่น สมมติว่าคุณมีข้อมูลหนังสือต่อไปนี้

ค่าชื่อพารามิเตอร์
ชื่อหนังสือ หนังสือฐานข้อมูล
ราคา 1000

หากคนสองคนเปิดหน้าจอเพื่อแก้ไขข้อมูลนี้พร้อมกันค่าข้างต้นจะปรากฏขึ้น คุณ/นางสาว A พยายามขึ้นราคาหนังสือเล่มนี้ 500 เยน ต่อมานาย/นางสาว B ได้รับคําสั่งให้ขึ้นราคาหนังสือเล่มนี้อีก 300 เยน

หากทั้งสองคนขึ้นราคาแยกกันแทนที่จะขึ้นพร้อมกันราคาของหนังสือจะอยู่ที่ 1800 เยน หากคุณเข้าถึงพร้อมกัน จะลงทะเบียนเป็น 1500 เยนและ 1300 เยนตามลําดับ ดังนั้นจึงไม่ใช่ 1800 เยนไม่ว่าใครจะลงทะเบียน

ปัญหาที่นี่คือผู้ที่อัปเดตในภายหลังสามารถอัปเดตได้โดยไม่ต้องทราบข้อมูลที่อัปเดตก่อนหน้านี้

การทํางานพร้อมกันในแง่ดี

ปัญหาดังกล่าวสามารถแก้ไขได้โดยการดําเนินการ "การควบคุมการทํางานพร้อมกันในแง่ดี" ในครั้งนี้ เพื่ออธิบายง่ายๆ การควบคุมแบบไหนคือ "ชนะก่อนหากคุณพยายามแก้ไขข้อมูลในเวลาเดียวกัน" ผู้ที่พยายามต่ออายุในภายหลังจะได้รับข้อผิดพลาดในช่วงเวลาของการอัปเดตและจะไม่สามารถลงทะเบียนได้

คุณอาจคิดว่าคุณไม่สามารถลงทะเบียนข้อมูลใหม่กับสิ่งนี้ได้ แต่นี่เป็นเพียง "เมื่อคุณพยายามเปลี่ยนแปลงในเวลาเดียวกัน" เท่านั้น เป็นไปได้ที่คนสองคนจะแก้ไขในเวลาที่ต่างกันโดยสิ้นเชิง แน่นอนว่าในกรณีนั้น ข้อมูลของบุคคลที่อัปเดตล่าสุดจะเป็นข้อมูลล่าสุด

โดยเฉพาะอย่างยิ่งการควบคุมการประมวลผลประเภทใดที่สามารถทําได้โดย "การมีเวอร์ชันของข้อมูล" ตัวอย่างเช่น ในตัวอย่างข้างต้น คุณจะมีข้อมูลต่อไปนี้

ค่าชื่อพารามิเตอร์
ชื่อหนังสือ หนังสือฐานข้อมูล
ราคา 1000
เวอร์ชัน 1

รุ่นจะเพิ่มขึ้น 1 สําหรับการอัปเดตเรกคอร์ดแต่ละครั้ง ตัวอย่างเช่น หากนาย/นางสาว A ตั้งราคาเป็น 1500 เยน เวอร์ชันจะเป็น 2 ในขณะนั้น เงื่อนไขที่การปรับปรุงสามารถทําได้คือรุ่นก่อนการปรับปรุงจะเหมือนกับรุ่นบนฐานข้อมูล เมื่อ Mr./Ms. อัปเดต เวอร์ชันในฐานข้อมูลคือ 1 และเวอร์ชันของข้อมูลต้นฉบับที่กําลังแก้ไขอยู่ในปัจจุบันคือ 1 ดังนั้นจึงสามารถอัปเดตได้

ตามข้อกําหนดนี้ สมมติว่าสถานการณ์ที่ Mr./Ms. A และ Mr./Ms. B แก้ไขข้อมูลเดียวกัน เมื่อ Mr./Ms. ตั้งราคาเป็น 1500 เยนเป็นครั้งแรกและพยายามอัปเดตฐานข้อมูลเวอร์ชันก็เหมือนกันดังนั้นจึงสามารถอัปเดตได้ ในกรณีนั้น เวอร์ชันในฐานข้อมูลจะเป็น 2 คุณ/นางสาว B พยายามแก้ไขข้อมูล 1,000 เยนและอัปเดตเป็นฐานข้อมูลเป็น 1,300 เยน การปรับปรุงล้มเหลวเนื่องจากรุ่นที่อยู่ในมือคือ 1 แต่รุ่นบนฐานข้อมูลเป็น 2 แล้ว นี่คือวิธีการทํางานของการทํางานพร้อมกันในแง่ดี

Entity Framework Core มี "ความพร้อมกันในแง่ดี" นี้ตั้งแต่แกะกล่อง ทําให้นําไปใช้ค่อนข้างง่าย

อย่างไรก็ตาม "การล็อกในแง่ดี" เรียกอีกอย่างว่า "ล็อคในแง่ดี" หรือ "ล็อคในแง่ดี" และบางครั้งก็มีการตรวจสอบและพูดถึงชื่อนี้ เป็นวิธีการล็อคที่ไม่สามารถอัปเดตข้อมูลได้ แต่สามารถอ่านข้อมูลได้ นอกจากนี้ยังมีการควบคุมที่เรียกว่า "การล็อคในแง่ร้าย" ซึ่งเป็นวิธีการล็อคอีกวิธีหนึ่งนอกเหนือจาก "การล็อคในแง่ดี" นี่เป็นวิธีการที่ล็อคการโหลดข้อมูลเมื่อบุคคลแรกอ่านข้อมูลและไม่อนุญาตให้ดําเนินการแก้ไขด้วยซ้ํา สามารถแก้ปัญหาที่ไม่สามารถอัปเดตข้อมูลได้แม้ว่าจะมีการเปลี่ยนแปลง แต่เมื่อมีคนกําลังแก้ไขข้อมูล บุคคลอื่นจะไม่สามารถเปิดหน้าจอแก้ไขข้อมูลได้ และหากการปลดล็อกล้มเหลว ข้อมูลจะถูกล็อคตลอดไป ทั้งสองมีข้อดีและข้อเสียดังนั้นจึงขึ้นอยู่กับการดําเนินการที่จะนํามาใช้

การสร้างฐานข้อมูล

ในบทความนี้ฉันจะอธิบายวิธีสร้างฐานข้อมูลสําหรับ SQL Server ก่อนแล้วจึงสร้างโค้ดโดยอัตโนมัติ หากคุณต้องการนําไปใช้ในลักษณะที่เน้นรหัสเป็นอันดับแรก โปรดดูโค้ดที่สร้างขึ้นโดยอัตโนมัติในครั้งนี้และนําไปใช้ในขั้นตอนย้อนกลับ

การสร้างฐานข้อมูล

คุณยังสามารถสร้างใน SQL ได้ แต่มันง่ายกว่าที่จะสร้างด้วย GUI ดังนั้นฉันจึงสร้างด้วย GUI ในครั้งนี้ ยกเว้นชื่อฐานข้อมูล จะถูกสร้างขึ้นตามค่าเริ่มต้น

สร้างตาราง

สร้างด้วย 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="}

ส่งผลให้ได้รับการอัปเดตสําเร็จ และPriceRowVersionและของหนังสือ A ได้รับการอัปเดต นอกจากนี้ หากคุณอ่านค่า DB โดยตรง ค่าที่อัปเดตจะเหมือนกับค่าที่อัปเดต ฉันสามารถอัปเดตได้เพราะทั้ง A RowVersion และ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 เป็นค่าที่อ่านจาก DB ในขณะที่อัปเดต

bookB.RowVersion = <古い RowVersion>;

แม้ว่าจะตั้งค่าดังนี้ แต่ก็จะไม่ถูกตัดสินอย่างถูกต้องว่าเป็น "การควบคุมการทํางานพร้อมกันในแง่ดี" RowVersion แม้ว่าคุณจะตั้งค่าเป็น ปกติ แต่จะถูกจดจําว่าเป็นค่าที่เปลี่ยนแปลงเท่านั้น ดังนั้นต่อไปนี้คือ

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

จําเป็นต้องเขียนค่าใหม่ก่อนการเปลี่ยนแปลง