Il controllo della concorrenza ottimistica impedisce la perdita di dati dovuta agli aggiornamenti dell'ultimo vantaggio con più accessi (SQL Server)

Pagina aggiornata :
Data di creazione della pagina :

Ambiente operativo

Studio visivo
  • Studio visivo 2022
.RETE
  • .NET 8
Nucleo di Entity Framework
  • Entity Framework Core 8.0
SQL Server
  • SQL Server 2022

* Quanto sopra è un ambiente di verifica, ma potrebbe funzionare con altre versioni.

Informazioni sull'acquisizione di aggiornamenti dopo l'assenza di controllo

In un'applicazione Web o in un'applicazione client-server, più persone possono accedere e aggiornare i dati da un singolo database. Se non si interviene in particolare, i dati della persona che li ha aggiornati successivamente si rifletteranno nel database come più recenti.

Normalmente, non c'è alcun problema particolare che i dati della persona che ha aggiornato in seguito si riflettano come gli ultimi. Possono sorgere problemi quando più persone tentano di accedere e aggiornare gli stessi dati contemporaneamente.

Si supponga, ad esempio, di disporre dei seguenti dati del libro.

Valore nome parametro
Nome del libro Libri di database
prezzo 1000

Se due persone aprono lo schermo per modificare questi dati contemporaneamente, verrà visualizzato il valore sopra. Il signor / signora A sta cercando di aumentare il prezzo di questo libro di 500 yen. Il signor / signora B viene successivamente incaricato di aumentare il prezzo di questo libro di altri 300 yen.

Se i due aumentano il prezzo separatamente invece che contemporaneamente, il prezzo del libro sarà di 1800 yen. Se accedi contemporaneamente, verrà registrato rispettivamente come 1500 yen e 1300 yen, quindi non sarà 1800 yen, indipendentemente da quale si registri.

Il problema qui è che la persona che ha aggiornato in seguito può aggiornarlo senza conoscere le informazioni che sono state aggiornate in precedenza.

Concorrenza ottimistica

Il problema di cui sopra può essere risolto eseguendo questa volta il "controllo della concorrenza ottimistica". Per spiegare semplicemente, che tipo di controllo è "vincere per primo se si tenta di modificare i dati allo stesso tempo". Coloro che tentano di rinnovare in un secondo momento riceveranno un errore al momento dell'aggiornamento e non potranno registrarsi.

Potresti pensare di non poter registrare nuovi dati con questo, ma questo è solo "quando provi a cambiarlo allo stesso tempo". È possibile che due persone modifichino in momenti completamente diversi. Naturalmente, in tal caso, i dati dell'ultima persona aggiornata saranno gli ultimi.

In particolare, che tipo di controllo del trattamento può essere ottenuto "avendo una versione dei dati". Ad esempio, nell'esempio precedente, avrai i seguenti dati.

Valore nome parametro
Nome del libro Libri di database
prezzo 1000
Versione 1

La versione viene incrementata di 1 per ogni aggiornamento del record. Ad esempio, se il Sig./Sig.ra A imposta il prezzo a 1500 yen, la versione sarà 2. A quel punto, la condizione per cui è possibile eseguire l'aggiornamento è che la versione precedente all'aggiornamento sia uguale alla versione del database. Quando il Sig./Sig.ra lo aggiorna, la versione sul database è 1 e la versione dei dati originali attualmente in fase di modifica è 1, quindi può essere aggiornato.

In base a questa specifica, si supponga una situazione in cui il Sig./Sig.ra A e il Sig./Sig.ra B modificano gli stessi dati. Quando il signore/la signora ha impostato per la prima volta il prezzo a 1500 yen e ha cercato di aggiornare il database, la versione era la stessa, quindi poteva essere aggiornata. In tal caso, la versione nel database sarà 2. Il signor / signora B cerca di modificare i dati di 1000 yen e aggiornarli nel database come 1300 yen. L'aggiornamento non riesce perché la versione in questione è la 1, ma la versione nel database è già la 2. Ecco come funziona la concorrenza ottimistica.

Entity Framework Core include questa "concorrenza ottimistica" pronta all'uso, che lo rende relativamente facile da implementare.

A proposito, la "concorrenza ottimistica" è nota anche come "blocco ottimistico" o "blocco ottimistico", e a volte viene esaminata e discussa con questo nome. Si tratta di un metodo di blocco in base al quale i dati non possono essere aggiornati, ma i dati possono essere letti. Esiste anche un controllo chiamato "blocco pessimistico" come un altro metodo di blocco diverso dal "blocco ottimistico". Questo è un metodo che blocca il caricamento dei dati quando la prima persona legge i dati e non consente nemmeno le operazioni di modifica. Può risolvere il problema che i dati non possono essere aggiornati anche se sono stati modificati, ma quando qualcuno sta modificando i dati, altre persone non possono aprire la schermata di modifica dei dati e, se lo sblocco non riesce, i dati verranno bloccati per sempre. Entrambi hanno vantaggi e svantaggi, quindi dipende dall'operazione quale adottare.

Creazione di un database

In questo articolo spiegherò come creare prima un database per SQL Server e poi generare automaticamente il codice. Se si desidera implementarlo in modo code-first, questa volta si prega di fare riferimento al codice generato automaticamente e di implementarlo nella procedura inversa.

Creazione di un database

È possibile farlo anche in SQL, ma è più facile farlo con una GUI, quindi questa volta lo farò con una GUI. Fatta eccezione per il nome del database, viene creato per impostazione predefinita.

Creare una tabella

Crearlo con il seguente 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

Non devi preoccuparti della maggior parte dei parametri perché servono solo per l'aggiornamento dei dati. Il parametro di interesse questa volta è RowVersion la colonna che descrive . Questo è il controllo delle versioni dei record. Specificando come tipo timestamp , la versione viene incrementata automaticamente ogni volta che il record viene aggiornato. Inoltre, poiché questa versione viene gestita in base alla tabella, non esiste praticamente alcun record della stessa versione a meno che non venga impostata manualmente.

Aggiungere un record

È possibile aggiungerlo con il seguente 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 Non è necessario impostare le colonne perché vengono impostate automaticamente.

Creare un progetto e generare automaticamente il codice

Questa volta, controlleremo il funzionamento con l'applicazione console. I passaggi per creare un progetto e generare automaticamente il codice sono descritti nei suggerimenti seguenti, quindi non li approfondirò qui.

Il codice generato è il seguente:

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

Verifica del funzionamento del controllo della concorrenza ottimistica

Dal momento che questa volta lo stiamo eseguendo in un'unica applicazione, non vi accediamo strettamente allo stesso tempo, ma vorremmo implementarlo in una forma simile ad esso.

Come nell'esempio all'inizio, vengono acquisiti due dati e, quando ciascuno viene aggiornato in base al primo dato, controlla se il secondo programma di aggiornamento riceverà un errore.

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

È una lunga storia, ma la maggior parte di essa è scritta per la console.

Il risultato dell'esecuzione è il seguente.

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

処理を終了します。

Lo suddividerò in sezioni.

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

Sto creando due contesti di database, Se si condivide un contesto di database, questo verrà memorizzato nella cache quando i dati vengono letti e sarà la stessa istanza. Supponendo che l'accesso a ciascuno di essi sia separato, vengono creati due contesti di database. dbContextC serve per controllare i valori nel database. Non ne ho davvero bisogno perché A o B possono essere sostituiti.

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

Supponendo che siano accessibili separatamente, ne leggono uno da contesti Book di database diversi. A questo punto, non abbiamo cambiato nulla, quindi sono tutti uguali.

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

Per prima cosa, ho cambiato e aggiornato il libro del Sig./Sig.ra. Il processo di aggiornamento viene UpdateToDatabase riepilogato in un metodo e viene visualizzato un messaggio quando si verifica un'eccezione.

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

Di conseguenza, è stato aggiornato con successo e Price RowVersion il libro A sono stati aggiornati. Inoltre, se si legge direttamente il valore DB, il valore aggiornato sarà lo stesso del valore aggiornato. Sono stato in grado di aggiornarlo perché sia in RowVersion A RowVersion che in DB erano AAAAAAAAH2k= . La versione è stata modificata AAAAAAAAH2o= a causa dell'aggiornamento.

Dopo l'aggiornamento A, aggiornare B allo stesso modo.

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

Il risultato è un'eccezione DbUpdateConcurrencyException e l'aggiornamento non è riuscito. bookB.RowVersion AAAAAAAAH2k= è , ma RowVersion poiché è già AAAAAAAAH2o= , viene giudicato una mancata corrispondenza e si verifica un errore di aggiornamento. Si può notare che la Price variabile bookB locale è cambiata, ma RowVersion il parametro Si può notare che i valori sul lato del database non sono cambiati affatto.

Sommario

Questo è il più semplice dei diversi tipi di blocco da implementare, perché quando si utilizza il codice generato automaticamente in SQL Server ed Entity Framework Core, la concorrenza ottimistica viene implementata per impostazione predefinita. Tuttavia, poiché è solo per prevenire la "corruzione dei dati da aggiornare", è necessario gestire correttamente le eccezioni quando sono coinvolti altri dati o operazioni dell'utente.

Inoltre, questa volta non sono riuscito a gestire nulla perché l'ho implementato RowVersion in una semplice applicazione console. Se si desidera inserire la schermata di modifica dopo aver caricato i dati in un'applicazione Web o in un'applicazione client, RowVersion in qualche modo in modo che possa essere determinato correttamente quando viene aggiornato.

Tuttavia, Entity Framework Core dispone di una funzione di rilevamento delle modifiche, quindi se si desidera impostare il vecchio RowVersion sul valore letto dal DB al momento dell'aggiornamento,

bookB.RowVersion = <古い RowVersion>;

Anche se è impostato come segue, non sarà correttamente giudicato come "controllo della concorrenza ottimistica". RowVersion Anche se si imposta il valore su normale, verrà riconosciuto solo come valore modificato, quindi quanto segue è

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

È necessario riscrivere il valore prima della modifica.