Il controllo della concorrenza ottimistica impedisce la perdita di dati dovuta agli aggiornamenti dell'ultimo vantaggio con più accessi (SQL Server)
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.