Az optimista egyidejűség-szabályozás megakadályozza az adatvesztést a több hozzáféréssel (SQL Server) rendelkező utolsó nyereséges frissítések miatt

Oldal frissítve :
Oldal létrehozásának dátuma :

Működési környezet

Visual Studio
  • Visual Studio 2022
.HÁLÓ
  • .NET 8
Entity Framework Core
  • Entity Framework Core 8.0
SQL Server
  • SQL Server 2022

* A fenti egy ellenőrzési környezet, de más verziókkal is működhet.

A frissítések ellenőrzése nélkül történő megnyeréséről

Egy webalkalmazásban vagy ügyfél-kiszolgáló alkalmazásban több személy is hozzáférhet és frissíthet adatokat egyetlen adatbázisból. Ha semmi sem történik különösebben, akkor a később frissített személy adatai a legújabbként jelennek meg az adatbázisban.

Általában nincs különösebb probléma, hogy a később frissített személy adatai a legújabbak. Problémák merülhetnek fel, ha egyszerre többen próbálják elérni és frissíteni ugyanazokat az adatokat.

Tegyük fel például, hogy a következő könyvadatokkal rendelkezik.

Paraméternév értéke
A könyv neve Adatbázis könyvek
ár 1000

Ha két ember egyszerre nyitja meg a képernyőt az adatok szerkesztéséhez, a fenti érték jelenik meg. A úr / asszony megpróbálja 500 jennel emelni ennek a könyvnek az árát. B. urat később arra utasítják, hogy emelje meg a könyv árát további 300 jennel.

Ha ketten külön-külön emelik az árat, nem pedig egyszerre, akkor a könyv ára 1800 jen lesz. Ha egyszerre fér hozzá, akkor 1500 jenként és 1300 jenként lesz regisztrálva, tehát nem lesz 1800 jen, függetlenül attól, hogy melyik regisztrál.

A probléma itt az, hogy a később frissített személy frissítheti azt anélkül, hogy tudná a korábban frissített információkat.

Optimista egyidejűség

A fent említett probléma ezúttal "optimista egyidejűség-szabályozással" oldható meg. Egyszerűen elmagyarázni, hogy milyen vezérlés az, hogy "először nyerjen, ha egyszerre próbálja szerkeszteni az adatokat". Azok, akik később próbálnak megújulni, hibaüzenetet kapnak a frissítés időzítésekor, és nem tudnak regisztrálni.

Azt gondolhatja, hogy ezzel nem regisztrálhat új adatokat, de ez csak akkor van, "ha egyszerre próbálja megváltoztatni". Lehetséges, hogy két ember teljesen különböző időpontokban szerkesszen. Természetesen ebben az esetben az utoljára frissített személy adatai lesznek a legfrissebbek.

Pontosabban, milyen feldolgozási ellenőrzés érhető el az "adatok egy verziójának birtoklásával". A fenti példában például a következő adatok lesznek.

Paraméternév értéke
A könyv neve Adatbázis könyvek
ár 1000
verzió 1

A verzió minden rekordfrissítéskor 1-gyel növekszik. Például, ha Mr./Ms. A 1500 jenre állítja az árat, akkor a verzió 2 lesz. Ekkor a frissítés elvégezhetőségének feltétele, hogy a frissítés előtti verzió megegyezzen az adatbázisban található verzióval. Amikor Mr./Ms. frissíti, az adatbázis verziója 1, az éppen szerkesztés alatt álló eredeti adatok verziója pedig 1, így frissíthető.

E specifikáció alapján tételezzük fel azt a helyzetet, amikor A úr / asszony és B úr / B úr ugyanazokat az adatokat szerkeszti. Amikor Mr./Ms. először állította be az árat 1500 jenre, és megpróbálta frissíteni az adatbázist, a verzió ugyanaz volt, így frissíthető. Ebben az esetben az adatbázis verziója 2 lesz. B úr megpróbálja szerkeszteni 1000 jen adatait, és frissíteni az adatbázisba 1300 jenként. A frissítés sikertelen, mert az aktuális verzió 1, de az adatbázisban lévő verzió már 2. Így működik az optimista egyidejűség.

Az Entity Framework Core tartalmazza ezt az "optimista egyidejűséget" a dobozból, így viszonylag könnyen megvalósítható.

By the way, az "optimista egyidejűség" más néven "optimista zár" vagy "optimista zár", és néha ezt a nevet vizsgálják és beszélnek róla. Ez egy zárolási módszer, hogy az adatok nem frissíthetők, de az adatok olvashatók. Van egy "pesszimista zár" nevű kontroll is, mint egy másik zárolási módszer, amely nem "optimista zár". Ez egy olyan módszer, amely zárolja az adatok betöltését, amikor az első személy elolvassa az adatokat, és nem is engedélyezi a szerkesztési műveleteket. Megoldhatja azt a problémát, hogy az adatokat nem lehet frissíteni, annak ellenére, hogy megváltoztatták, de amikor valaki szerkeszti az adatokat, mások nem tudják megnyitni az adatszerkesztő képernyőt, és ha a feloldás sikertelen, az adatok örökre zárolva lesznek. Mindkettőnek vannak előnyei és hátrányai, ezért attól függ, hogy melyik műveletet kell alkalmazni.

Adatbázis létrehozása

Ebben a cikkben elmagyarázom, hogyan lehet először adatbázist létrehozni az SQL Server számára, majd automatikusan kódot generálni. Ha először kódszerűen szeretné megvalósítani, kérjük, olvassa el ezúttal az automatikusan generált kódot, és hajtsa végre fordított eljárással.

Adatbázis létrehozása

SQL-ben is elkészítheti, de könnyebb grafikus felhasználói felülettel elkészíteni, ezért ezúttal grafikus felhasználói felülettel készítem. Az adatbázis nevét kivéve alapértelmezés szerint létrejön.

Tábla létrehozása

Hozza létre a következő 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

Nem kell aggódnia a legtöbb paraméter miatt, mert csak az adatfrissítésre szolgálnak. Az érdekes paraméter ezúttal az oszlop, RowVersion amely leírja . Ez a rekordverzió-kezelés. Ha típusként timestamp adja meg, a verzió automatikusan növekszik a rekord minden frissítésekor. Továbbá, mivel ezt a verziót táblánként kezelik, alapvetően nincs rekord ugyanarról a verzióról, hacsak nem állítja be manuálisan.

Rekord hozzáadása

A következő SQL-lel adhatja hozzá:

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 Az oszlopokat nem kell beállítania, mert azok beállítása automatikus.

Projekt létrehozása és kód automatikus létrehozása

Ezúttal a konzolalkalmazással ellenőrizzük a működést. A projekt létrehozásának és a kód automatikus létrehozásának lépéseit a következő tippek ismertetik, ezért itt nem megyek bele ezekbe.

A generált kód a következő:

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!;
}

Az optimista egyidejűség kontroll működésének ellenőrzése

Mivel ezúttal egy alkalmazásban futtatjuk, nem szigorúan érjük el egyszerre, hanem szeretnénk egy hozzá közeli formában megvalósítani.

Az elején látható példához hasonlóan a rendszer két adatot szerez be, és amikor mindkettőt az első adatok alapján frissíti, ellenőrizze, hogy az utóbbi frissítő kap-e hibát.

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);
  }
}

Ez egy hosszú történet, de a legtöbbet a konzolra írták.

A végrehajtás eredménye a következő.

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="}

処理を終了します。

Szakaszokra bontom.

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("データベースコンテキストを作成しました。");

Két adatbázis-környezetet hozok létre, Ha megoszt egy adatbázis-környezetet, az gyorsítótárazza az adatok olvasásakor, és ugyanaz a példány lesz. Feltételezve, hogy mindegyik külön érhető el, két adatbázis-környezet jön létre. dbContextC az adatbázis értékeinek ellenőrzésére szolgál. Nem igazán van rá szükségem, mert A vagy B helyettesíthető.

// それぞれがデータを編集しようと読み込む
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))}");

Feltételezve, hogy külön érhetők el, különböző adatbázis-környezetekből Book olvasnak egyet. Ezen a ponton nem változtattunk semmit, tehát mind ugyanazok.

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="}

Először megváltoztattam és frissítettem Mr./Ms. könyvét. A frissítési folyamat UpdateToDatabase egy metódusban van összefoglalva, és kivétel esetén üzenet jelenik meg.

// 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="}

Ennek eredményeként sikeresen frissítették, és Price RowVersion az A könyvet frissítették. Továbbá, ha közvetlenül olvassa be az adatbázis értékét, a frissített érték megegyezik a frissített értékkel. Tudtam frissíteni, mert mindkettő A-ban RowVersion és RowVersion DB-ben is volt AAAAAAAAH2k= . A verzió a frissítés miatt megváltozott AAAAAAAAH2o= .

Az A frissítése után ugyanúgy frissítse a B-t.

// そのあと 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="}

Az eredmény kivételDbUpdateConcurrencyException, és a frissítés sikertelen volt. bookB.RowVersion AAAAAAAAH2k=, de RowVersion mivel már AAAAAAAAH2o= , eltérésnek ítélik, és frissítési hiba lép fel. Láthatja, hogy a Price helyi változó bookB megváltozott, de RowVersion a Láthatja, hogy az adatbázis oldalán lévő értékek egyáltalán nem változtak.

Összefoglalás

Ez a legkönnyebben implementálható zárolási típus a számos zárolási típus közül, mert ha az automatikusan létrehozott kódot használja a SQL Server és az Entity Framework Core-ban, az optimista egyidejűség alapértelmezés szerint implementálva van. Mivel azonban csak az "adatsérülés frissítésének" megakadályozására szolgál, megfelelően kell kezelni a kivételeket, amikor más adatok vagy felhasználói műveletek érintettek.

Ezenkívül ezúttal nem sikerült semmit, mert egy egyszerű konzolalkalmazásban valósítottam RowVersion meg. Ha be szeretné szúrni a szerkesztési képernyőt, miután betöltötte az adatokat egy webalkalmazásba vagy ügyfélalkalmazásba, RowVersion valamilyen módon, hogy a frissítéskor megfelelően meghatározható legyen.

Az Entity Framework Core azonban rendelkezik változáskövetési funkcióval, így ha a régit RowVersion a frissítéskor az adatbázisból beolvasott értékre szeretné állítani,

bookB.RowVersion = <古い RowVersion>;

Még akkor is, ha az alábbiak szerint van beállítva, nem fogják helyesen megítélni "optimista egyidejűség-ellenőrzésként". RowVersion Még akkor is, ha az értéket normálra állítja, a rendszer csak megváltozott értékként ismeri fel, így a következő

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

A módosítás előtt át kell írni az értéket.