Die Steuerung der optimistischen Parallelität verhindert Datenverluste aufgrund von Last-Win-Updates mit mehreren Zugriffen (SQL Server)
Betriebsumgebung
- Visuelles Studio
-
- Visual Studio 2022
- .NETTO
-
- .NET 8
- Entity Framework-Kern
-
- Entity Framework Core 8.0
- SQL Server
-
- SQL Server 2022
* Bei der obigen Version handelt es sich um eine Verifizierungsumgebung, die jedoch möglicherweise mit anderen Versionen funktioniert.
Über das Gewinnen von Updates ohne Kontrolle
In einer Webanwendung oder einer Client-Server-Anwendung können mehrere Personen auf Daten aus einer einzigen Datenbank zugreifen und diese aktualisieren. Wenn nichts Besonderes unternommen wird, werden die Daten der Person, die sie später aktualisiert hat, in der Datenbank als die neuesten angezeigt.
Normalerweise ist es kein besonderes Problem, dass die Daten der Person, die später aktualisiert hat, als die neuesten angezeigt werden. Probleme können auftreten, wenn mehrere Personen gleichzeitig versuchen, auf dieselben Daten zuzugreifen und diese zu aktualisieren.
Angenommen, Sie verfügen über die folgenden Buchdaten.
Wert des Parameternamens | |
---|---|
Name des Buches | Datenbank-Bücher |
Preis | 1000 |
Wenn zwei Personen gleichzeitig den Bildschirm öffnen, um diese Daten zu bearbeiten, wird der obige Wert angezeigt. Herr/Frau A versucht, den Preis dieses Buches um 500 Yen zu erhöhen. Herr/Frau B wird später angewiesen, den Preis für dieses Buch um weitere 300 Yen zu erhöhen.
Wenn die beiden den Preis getrennt statt gleichzeitig erhöhen, beträgt der Preis des Buches 1800 Yen. Wenn Sie gleichzeitig darauf zugreifen, wird es als 1500 Yen bzw. 1300 Yen registriert, so dass es nicht 1800 Yen sind, egal wer sich registriert.
Das Problem dabei ist, dass die Person, die später aktualisiert hat, es aktualisieren kann, ohne die Informationen zu kennen, die zuvor aktualisiert wurden.
Optimistische Parallelität
Das oben erwähnte Problem kann gelöst werden, indem dieses Mal eine "optimistische Parallelitätskontrolle" durchgeführt wird. Um einfach zu erklären, was für eine Art von Kontrolle es ist, "zuerst zu gewinnen, wenn Sie versuchen, die Daten gleichzeitig zu bearbeiten". Diejenigen, die später versuchen, zu verlängern, erhalten zum Zeitpunkt des Updates eine Fehlermeldung und können sich nicht registrieren.
Sie denken vielleicht, dass Sie damit keine neuen Daten registrieren können, aber das ist nur "wenn Sie versuchen, sie gleichzeitig zu ändern". Es ist möglich, dass zwei Personen zu völlig unterschiedlichen Zeiten bearbeiten. In diesem Fall sind natürlich die Daten der zuletzt aktualisierten Person die aktuellsten.
Konkret, welche Art von Verarbeitungskontrolle erreicht werden kann, indem man "eine Version der Daten hat". Im obigen Beispiel verfügen Sie beispielsweise über die folgenden Daten.
Wert des Parameternamens | |
---|---|
Name des Buches | Datenbank-Bücher |
Preis | 1000 |
Version | 1 |
Die Version wird bei jeder Datensatzaktualisierung um 1 erhöht. Wenn Herr/Frau A den Preis beispielsweise auf 1500 Yen festlegt, ist die Version 2. Zu diesem Zeitpunkt ist die Bedingung, dass die Aktualisierung durchgeführt werden kann, dass die Version vor der Aktualisierung mit der Version in der Datenbank übereinstimmt. Wenn Herr/Frau sie aktualisiert, ist die Version in der Datenbank 1, und die Version der ursprünglichen Daten, die gerade bearbeitet werden, ist 1, sodass sie aktualisiert werden können.
Gehen Sie auf der Grundlage dieser Spezifikation von einer Situation aus, in der Herr/Frau A und Herr/Frau B dieselben Daten bearbeiten. Als Herr/Frau den Preis zum ersten Mal auf 1500 Yen festlegte und versuchte, die Datenbank zu aktualisieren, war die Version dieselbe, sodass sie aktualisiert werden konnte. In diesem Fall ist die Version in der Datenbank 2. Herr/Frau B versucht, die Daten von 1000 Yen zu bearbeiten und sie in der Datenbank als 1300 Yen zu aktualisieren. Das Update schlägt fehl, da die vorliegende Version 1 ist, die Version in der Datenbank jedoch bereits 2 ist. So funktioniert optimistische Parallelität.
Entity Framework Core enthält diese "optimistische Parallelität" von Haus aus, wodurch es relativ einfach zu implementieren ist.
Übrigens ist "optimistische Parallelität" auch als "optimistische Sperre" oder "optimistische Sperre" bekannt und wird manchmal unter diesem Namen untersucht und bezeichnet. Es handelt sich um eine Sperrmethode, bei der die Daten nicht aktualisiert, aber gelesen werden können. Es gibt auch ein Steuerelement namens "pessimistische Sperre" als eine andere Sperrmethode als "optimistische Sperre". Dabei handelt es sich um eine Methode, die das Laden von Daten sperrt, wenn die erste Person die Daten liest, und nicht einmal Bearbeitungsvorgänge zulässt. Es kann das Problem lösen, dass die Daten nicht aktualisiert werden können, obwohl sie geändert wurden, aber wenn jemand die Daten bearbeitet, können andere Personen den Datenbearbeitungsbildschirm nicht öffnen, und wenn das Entsperren fehlschlägt, werden die Daten für immer gesperrt. Beide haben Vor- und Nachteile, so dass es von der Operation abhängt, welche man annimmt.
Erstellen einer Datenbank
In diesem Artikel erkläre ich, wie Sie zuerst eine Datenbank für SQL Server erstellen und dann automatisch Code generieren. Wenn Sie es in einer Code-First-Weise implementieren möchten, beziehen Sie sich bitte diesmal auf den automatisch generierten Code und implementieren Sie ihn im umgekehrten Verfahren.
Erstellen einer Datenbank
Sie können es auch in SQL machen, aber es ist einfacher, es mit einer GUI zu machen, also mache ich es dieses Mal mit einer GUI. Mit Ausnahme des Datenbanknamens wird er standardmäßig erstellt.
Erstellen einer Tabelle
Erstellen Sie es mit der folgenden 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
Sie müssen sich um die meisten Parameter keine Gedanken machen, da sie nur für Datenaktualisierungen gedacht sind.
Der Parameter, der dieses Mal von Interesse ist, ist RowVersion
die Spalte, die beschreibt. Dabei handelt es sich um die Versionierung von Datensätzen.
Wenn Sie als timestamp
Typ angeben, wird die Version bei jeder Aktualisierung des Datensatzes automatisch erhöht.
Da diese Version pro Tabelle verwaltet wird, gibt es im Grunde keine Aufzeichnung derselben Version, es sei denn, Sie legen sie manuell fest.
Hinzufügen eines Datensatzes
Sie können es mit der folgenden SQL hinzufügen:
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
Sie müssen die Spalten nicht festlegen, da sie automatisch festgelegt werden.
Erstellen eines Projekts und automatisches Generieren von Code
Dieses Mal werden wir den Betrieb mit der Konsolenanwendung überprüfen. Die Schritte zum Erstellen eines Projekts und zum automatischen Generieren von Code werden in den folgenden Tipps beschrieben, daher werde ich hier nicht darauf eingehen.
Der generierte Code sieht wie folgt aus:
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!;
}
Überprüfen des Betriebs der Steuerung der optimistischen Parallelität
Da wir es dieses Mal in einer Anwendung ausführen, greifen wir nicht strikt gleichzeitig darauf zu, sondern möchten es in einer Form implementieren, die nahe dran ist.
Wie im Beispiel am Anfang werden zwei Daten abgerufen, und wenn jedes auf der Grundlage der ersten Daten aktualisiert wird, prüfen Sie, ob der zweite Updater einen Fehler erhält.
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);
}
}
Es ist eine lange Geschichte, aber das meiste davon ist auf die Konsole geschrieben.
Das Ergebnis der Ausführung ist wie folgt.
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="}
処理を終了します。
Ich werde es in Abschnitte unterteilen.
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("データベースコンテキストを作成しました。");
Ich erstelle zwei Datenbankkontexte,
Wenn Sie einen Datenbankkontext freigeben, wird dieser beim Lesen der Daten zwischengespeichert, und es handelt sich um dieselbe Instanz.
Unter der Annahme, dass auf jeden Kontext separat zugegriffen wird, werden zwei Datenbankkontexte erstellt.
dbContextC
dient zur Überprüfung der Werte in der Datenbank. Ich brauche es nicht wirklich, weil A oder B substituiert werden können.
// それぞれがデータを編集しようと読み込む
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))}");
Unter der Annahme, dass sie separat aufgerufen werden, lesen sie eine aus verschiedenen Datenbankkontexten Book
.
Zu diesem Zeitpunkt haben wir noch nichts geändert, also sind sie alle gleich.
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="}
Zuerst habe ich das Buch von Herr/Frau geändert und aktualisiert.
Der Aktualisierungsprozess wird UpdateToDatabase
in einer Methode zusammengefasst, und eine Meldung wird angezeigt, wenn eine Ausnahme auftritt.
// 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="}
Als Ergebnis wurde es erfolgreich aktualisiert, und Price
RowVersion
die von Buch A wurden aktualisiert.
Wenn Sie den DB-Wert direkt lesen, ist der aktualisierte Wert mit dem aktualisierten Wert identisch.
Ich konnte es aktualisieren, weil sowohl in RowVersion
A als RowVersion
auch in DB waren AAAAAAAAH2k=
.
Die Version hat sich aufgrund des Updates geändert AAAAAAAAH2o=
.
Aktualisieren Sie B nach dem Update A auf die gleiche Weise.
// そのあと 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="}
Das Ergebnis ist eine Ausnahme, DbUpdateConcurrencyException
und das Update ist fehlgeschlagen.
bookB.RowVersion
AAAAAAAAH2k=
ist , aber RowVersion
da ist bereits AAAAAAAAH2o=
, wird es als Nichtübereinstimmung beurteilt und es tritt ein Aktualisierungsfehler auf.
Sie können sehen, dass sich die Price
lokale Variable bookB
geändert hat, aber RowVersion
die
Sie können sehen, dass sich die Werte auf der Datenbankseite überhaupt nicht geändert haben.
Zusammenfassung
Dies ist der am einfachsten zu implementierende der verschiedenen Sperrtypen, da bei Verwendung des automatisch generierten Codes in SQL Server und Entity Framework Core standardmäßig optimistische Parallelität implementiert wird. Da es jedoch nur darum geht, "zu aktualisierende Datenbeschädigungen" zu verhindern, ist es notwendig, Ausnahmen ordnungsgemäß zu behandeln, wenn andere Daten oder Benutzervorgänge beteiligt sind.
Außerdem habe ich dieses Mal nichts geschafft, weil ich es in einer einfachen Konsolenanwendung implementiert RowVersion
habe.
Wenn Sie den Bearbeitungsbildschirm nach dem Laden der Daten in einer Webanwendung oder Clientanwendung einfügen möchten,
RowVersion
in irgendeiner Weise, damit es bei der Aktualisierung ordnungsgemäß bestimmt werden kann.
Entity Framework Core verfügt jedoch über eine Änderungsverfolgungsfunktion, wenn Sie also den alten RowVersion
Wert auf den Wert setzen möchten, der zum Zeitpunkt des Updates aus der Datenbank gelesen wurde,
bookB.RowVersion = <古い RowVersion>;
Selbst wenn sie wie folgt festgelegt ist, wird sie nicht korrekt als "optimistische Parallelitätssteuerung" beurteilt.
RowVersion
Auch wenn Sie den Wert auf normal setzen, wird er nur als der geänderte Wert erkannt, sodass Folgendes gilt
dbContextB.Entry(bookB).Property("RowVersion").OriginalValue = <古い RowVersion>;
Es ist notwendig, den Wert vor der Änderung neu zu schreiben.