Controlul optimist al concurenței previne pierderea datelor din cauza actualizărilor ultimului câștig cu accesări multiple (SQL Server)

Pagina actualizată :
Data creării paginii :

Mediu de operare

Visual Studio
  • Visual Studio 2022
.NET
  • .NET 8
Nucleul cadrului entității
  • Cadrul de entitate Core 8.0
SQL Server
  • SQL Server 2022

* Cele de mai sus sunt un mediu de verificare, dar pot funcționa cu alte versiuni.

Despre câștigarea actualizărilor după niciun control

Într-o aplicație web sau o aplicație client-server, mai multe persoane pot accesa și actualiza date dintr-o singură bază de date. Dacă nu se face nimic în mod special, datele persoanei care le-a actualizat ulterior vor fi reflectate în baza de date ca fiind cele mai recente.

În mod normal, nu există nicio problemă specială ca datele persoanei care a actualizat ulterior să fie reflectate ca fiind cele mai recente. Problemele pot apărea atunci când mai multe persoane încearcă să acceseze și să actualizeze aceleași date în același timp.

De exemplu, să presupunem că aveți următoarele date din carte.

Valoarea numelui parametrului
Numele cărții Cărți de baze de date
preț 1000

Dacă două persoane deschid ecranul pentru a edita aceste date în același timp, va fi afișată valoarea de mai sus. Domnul / doamna A încearcă să ridice prețul acestei cărți cu 500 de yeni. Domnul B este instruit mai târziu să crească prețul acestei cărți cu încă 300 de yeni.

Dacă cei doi ridică prețul separat în loc de același timp, prețul cărții va fi de 1800 de yeni. Dacă îl accesați în același timp, acesta va fi înregistrat ca 1500 de yeni și, respectiv, 1300 de yeni, deci nu va fi de 1800 de yeni, indiferent care se înregistrează.

Problema aici este că persoana care a actualizat ulterior o poate actualiza fără să cunoască informațiile care au fost actualizate anterior.

Concurență optimistă

Problema menționată mai sus poate fi rezolvată prin efectuarea unui "control optimist al concurenței" de data aceasta. Pentru a explica simplu, ce fel de control este să "câștigi primul dacă încerci să editezi datele în același timp". Cei care încearcă să reînnoiască mai târziu vor primi o eroare la momentul actualizării și nu se vor putea înregistra.

S-ar putea să credeți că nu puteți înregistra date noi cu acest lucru, dar acest lucru este doar "atunci când încercați să le modificați în același timp". Este posibil ca două persoane să editeze în momente complet diferite. Desigur, în acest caz, datele ultimei persoane actualizate vor fi cele mai recente.

Mai exact, ce fel de control al procesării poate fi realizat prin "a avea o versiune a datelor". De exemplu, în exemplul de mai sus, veți avea următoarele date.

Valoarea numelui parametrului
Numele cărții Cărți de baze de date
preț 1000
versiune 1

Versiunea este incrementată cu 1 pentru fiecare actualizare a înregistrării. De exemplu, dacă domnul / doamna A stabilește prețul la 1500 de yeni, versiunea va fi 2. În acel moment, condiția ca actualizarea să poată fi făcută este ca versiunea anterioară actualizării să fie aceeași cu versiunea din baza de date. Când domnul / doamna îl actualizează, versiunea din baza de date este 1, iar versiunea datelor originale în curs de editare este 1, deci poate fi actualizată.

Pe baza acestei specificații, să presupunem o situație în care domnul / doamna A și domnul / doamna B editează aceleași date. Când domnul / doamna a stabilit pentru prima dată prețul la 1500 de yeni și a încercat să actualizeze baza de date, versiunea a fost aceeași, astfel încât să poată fi actualizată. În acest caz, versiunea din baza de date va fi 2. Domnul / doamna B încearcă să editeze datele de 1000 de yeni și să le actualizeze în baza de date ca 1300 de yeni. Actualizarea nu reușește, deoarece versiunea la îndemână este 1, dar versiunea din baza de date este deja 2. Acesta este modul în care funcționează concurența optimistă.

Entity Framework Core include această "concurență optimistă" din cutie, ceea ce o face relativ ușor de implementat.

Apropo, "concurența optimistă" este, de asemenea, cunoscută sub numele de "blocare optimistă" sau "blocare optimistă" și uneori este examinată și discutată cu acest nume. Este o metodă de blocare că datele nu pot fi actualizate, dar datele pot fi citite. Există, de asemenea, un control numit "blocare pesimistă" ca o altă metodă de blocare decât "blocarea optimistă". Aceasta este o metodă care blochează încărcarea datelor atunci când prima persoană citește datele și nici măcar nu permite operații de editare. Poate rezolva problema că datele nu pot fi actualizate chiar dacă au fost modificate, dar atunci când cineva editează datele, alte persoane nu pot deschide ecranul de editare a datelor și, dacă deblocarea eșuează, datele vor fi blocate pentru totdeauna. Ambele au avantaje și dezavantaje, deci depinde de operațiunea pe care să o adoptați.

Crearea unei baze de date

În acest articol, voi explica cum să creați mai întâi o bază de date pentru SQL Server și apoi să generați automat cod. Dacă doriți să îl implementați într-o manieră care pune codul în primul rând, vă rugăm să consultați codul generat automat de data aceasta și să îl implementați în procedura inversă.

Crearea unei baze de date

Puteți face și în SQL, dar este mai ușor să o faceți cu o interfață grafică, așa că o fac cu o interfață grafică de data aceasta. Cu excepția numelui bazei de date, aceasta este creată în mod implicit.

Crearea unui tabel

Creați-l cu următorul 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

Nu trebuie să vă faceți griji cu privire la majoritatea parametrilor, deoarece aceștia sunt doar pentru actualizări de date. Parametrul de interes de data aceasta este RowVersion coloana care descrie . Aceasta este versiunea de înregistrare. Prin specificarea timestamp ca tip, versiunea este incrementată automat de fiecare dată când înregistrarea este actualizată. De asemenea, deoarece această versiune este gestionată pe bază de tabel, practic nu există nicio înregistrare a aceleiași versiuni decât dacă o setați manual.

Adăugarea unei înregistrări

Îl puteți adăuga cu următorul 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 Nu este necesar să setați coloanele, deoarece acestea sunt setate automat.

Creați un proiect și generați automat cod

De data aceasta, vom verifica operațiunea cu aplicația consolei. Pașii pentru crearea unui proiect și generarea automată a codului sunt descriși în următoarele sfaturi, așa că nu voi intra în ele aici.

Codul generat este următorul:

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

Verificarea funcționării controlului concurenței optimiste

Deoarece îl rulăm într-o singură aplicație de data aceasta, nu îl accesăm strict în același timp, dar am dori să îl implementăm într-o formă apropiată de acesta.

Ca și în exemplul de la început, sunt achiziționate două date, iar când fiecare este actualizată pe baza primelor date, verificați dacă acesta din urmă va primi o eroare.

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

Este o poveste lungă, dar cea mai mare parte este scrisă pentru consolă.

Rezultatul execuției este următorul.

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

処理を終了します。

O voi împărți în secțiuni.

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

Creez două contexte de baze de date, Dacă partajați un context al bazei de date, acesta va fi memorat în cache atunci când datele sunt citite și va fi aceeași instanță. Presupunând că fiecare este accesat separat, sunt create două contexte de bază de date. dbContextC este pentru verificarea valorilor din baza de date. Nu prea am nevoie de ea pentru că A sau B pot fi înlocuite.

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

Presupunând că sunt accesate separat, ele citesc unul din diferite contexte de baze de Book date . În acest moment, nu am schimbat nimic, așa că toate sunt la fel.

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

În primul rând, am schimbat și actualizat cartea domnului / doamnei. Procesul de actualizare este UpdateToDatabase rezumat într-o metodă și se afișează un mesaj atunci când apare o excepție.

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

Ca urmare, a fost actualizat cu succes, iar Price RowVersion Cartea A a fost actualizată. De asemenea, dacă citiți direct valoarea DB, valoarea actualizată va fi aceeași cu valoarea actualizată. Am reușit să-l actualizez pentru că ambele au intrat în RowVersion A și RowVersion în DB au fost AAAAAAAAH2k= . Versiunea s-a schimbat AAAAAAAAH2o= din cauza actualizării.

După actualizarea A, actualizați B în același mod.

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

Rezultatul este o excepție DbUpdateConcurrencyException și actualizarea nu a reușit. bookB.RowVersion AAAAAAAAH2k= este , dar RowVersion din moment ce este deja AAAAAAAAH2o= , se consideră a fi o nepotrivire și apare o eroare de actualizare. Puteți vedea că variabila Price locală s-a schimbat, dar RowVersion pictograma bookB Puteți vedea că valorile din partea bazei de date nu s-au modificat deloc.

Rezumat

Acesta este cel mai ușor dintre mai multe tipuri de blocare de implementat, deoarece atunci când utilizați codul generat automat în SQL Server și Entity Framework Core, concurența optimistă este implementată în mod implicit. Cu toate acestea, deoarece este doar pentru a preveni "coruperea datelor care trebuie actualizate", este necesar să se gestioneze în mod corespunzător excepțiile atunci când sunt implicate alte date sau sunt implicate operațiuni ale utilizatorilor.

De asemenea, de data aceasta nu am reușit nimic pentru că l-am implementat RowVersion într-o simplă aplicație de consolă. Dacă doriți să inserați ecranul de editare după încărcarea datelor într-o aplicație web sau într-o aplicație client, RowVersion într-un fel, astfel încât să poată fi determinat în mod corespunzător atunci când este actualizat.

Cu toate acestea, Entity Framework Core are o funcție de urmărire a modificărilor, deci dacă doriți să setați vechiul RowVersion la valoarea citită din baza de date în momentul actualizării,

bookB.RowVersion = <古い RowVersion>;

Chiar dacă este stabilit după cum urmează, nu va fi judecat corect ca "control optimist al concurenței". RowVersion Chiar dacă setați valoarea la normal, aceasta va fi recunoscută doar ca valoare modificată, deci următoarele sunt

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

Este necesar să rescrieți valoarea înainte de modificare.