Optimistisk samtidighedskontrol forhindrer tab af data på grund af sidste gevinst-opdateringer med flere adgange (SQL Server)

Side opdateret :
Dato for oprettelse af side :

Driftsmiljø

Visual Studio
  • Visual Studio 2022
.NET
  • .NET 8
Enhed Framework Core
  • Enhedsramme Core 8.0
SQL Server
  • SQL Server 2022

* Ovenstående er et verifikationsmiljø, men det kan fungere med andre versioner.

Om at vinde opdateringer uden kontrol

I en webapplikation eller en klient-server-applikation kan flere personer få adgang til og opdatere data fra en enkelt database. Hvis der ikke gøres noget særligt, vil oplysningerne om den person, der senere har opdateret dem, blive afspejlet i databasen som det seneste.

Normalt er der ikke noget særligt problem med, at dataene fra den person, der opdaterede senere, afspejles som de seneste. Der kan opstå problemer, når flere personer forsøger at få adgang til og opdatere de samme data på samme tid.

Antag f.eks., at du har følgende bogdata.

Parameter Navn Værdi
Bogens navn Databasebøger
pris 1000

Hvis to personer åbner skærmen for at redigere disse data på samme tid, vises ovenstående værdi. Mr./Ms. A forsøger at hæve prisen på denne bog med 500 yen. Hr./Ms. B bliver senere instrueret i at hæve prisen på denne bog med yderligere 300 yen.

Hvis de to hæver prisen hver for sig i stedet for på samme tid, vil bogens pris være 1800 yen. Hvis du tilgår den på samme tid, vil den blive registreret som henholdsvis 1500 yen og 1300 yen, så den bliver ikke 1800 yen, uanset hvilken der registreres.

Problemet her er, at den person, der opdaterede senere, kan opdatere den uden at kende de oplysninger, der tidligere blev opdateret.

Optimistisk samtidighed

Det førnævnte problem kan løses ved at udføre "optimistisk samtidighedskontrol" denne gang. For at forklare simpelt, hvilken slags kontrol der er at "vinde først, hvis du forsøger at redigere dataene på samme tid". De, der forsøger at forny senere, vil modtage en fejl på tidspunktet for opdateringen og vil ikke være i stand til at registrere sig.

Du tror måske, at du ikke kan registrere nye data med dette, men det er kun "når du forsøger at ændre det på samme tid". Det er muligt for to personer at redigere på helt forskellige tidspunkter. I så fald vil dataene fra den sidst opdaterede person naturligvis være de seneste.

Helt konkret, hvilken form for behandlingskontrol der kan opnås ved at "have en version af dataene". I ovenstående eksempel vil du f.eks. have følgende data.

Parameter Navn Værdi
Bogens navn Databasebøger
pris 1000
version 1

Versionen øges med 1 for hver postopdatering. For eksempel, hvis Mr./Ms. A sætter prisen til 1500 yen, vil versionen være 2. På det tidspunkt er betingelsen for, at opdateringen kan foretages, at versionen før opdateringen er den samme som versionen i databasen. Når Mr./Ms. opdaterer den, er versionen på databasen 1, og versionen af de originale data, der aktuelt redigeres, er 1, så den kan opdateres.

Baseret på denne specifikation skal du antage en situation, hvor hr./frk. A og hr./fr. B redigerer de samme data. Da Mr./Ms. først satte prisen til 1500 yen og forsøgte at opdatere databasen, var versionen den samme, så den kunne opdateres. I så fald vil versionen på databasen være 2. Mr./Ms. B forsøger at redigere dataene på 1000 yen og opdatere dem til databasen som 1300 yen. Opdateringen mislykkes, fordi den aktuelle version er 1, men versionen i databasen er allerede 2. Sådan fungerer optimistisk samtidighed.

Entity Framework Core inkluderer denne "optimistiske samtidighed" ud af boksen, hvilket gør den relativt nem at implementere.

Forresten er "optimistisk samtidighed" også kendt som "optimistisk lås" eller "optimistisk lås", og det undersøges og omtales nogle gange med dette navn. Det er en låsemetode, at dataene ikke kan opdateres, men dataene kan læses. Der er også en kontrol kaldet "pessimistisk lås" som en anden låsemetode end "optimistisk lås". Dette er en metode, der låser dataindlæsningen, når den første person læser dataene og ikke engang tillader redigeringshandlinger. Det kan løse problemet med, at dataene ikke kan opdateres, selvom de er blevet ændret, men når nogen redigerer dataene, kan andre ikke åbne dataredigeringsskærmen, og hvis oplåsningen mislykkes, låses dataene for evigt. Begge har fordele og ulemper, så det afhænger af operationen, hvilken man skal vedtage.

Oprettelse af en database

I denne artikel vil jeg forklare, hvordan man først opretter en database til SQL Server og derefter automatisk genererer kode. Hvis du vil implementere det på en kode-først-måde, skal du henvise til den automatisk genererede kode denne gang og implementere den i den omvendte procedure.

Oprettelse af en database

Du kan også lave det i SQL, men det er nemmere at lave det med en GUI, så jeg laver det med en GUI denne gang. Bortset fra databasenavnet oprettes det som standard.

Opret en tabel

Opret det med følgende 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

Du behøver ikke bekymre dig om de fleste parametre, fordi de kun er til dataopdateringer. Parameteren af interesse denne gang er RowVersion den kolonne, der beskriver . Dette er versionsstyring af poster. Ved at angive som timestamp type øges versionen automatisk, hver gang posten opdateres. Da denne version administreres pr. tabel, er der dybest set ingen registrering af den samme version, medmindre du indstiller den manuelt.

Tilføj en post

Du kan tilføje det med følgende 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 Du behøver ikke at angive kolonnerne, fordi de angives automatisk.

Opret et projekt og generer automatisk kode

Denne gang kontrollerer vi betjeningen med konsolapplikationen. Trinene til at oprette et projekt og automatisk generere kode er beskrevet i følgende tips, så jeg vil ikke gå ind på dem her.

Den genererede kode er som følger:

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

Kontrol af driften af optimistisk samtidighedskontrol

Da vi kører det i én applikation denne gang, har vi ikke strengt adgang til det på samme tid, men vi vil gerne implementere det i en form tæt på det.

Som i eksemplet i begyndelsen indsamles to stykker data, og når hver opdateres baseret på de første data, skal du kontrollere, om sidstnævnte opdaterer får en fejl.

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

Det er en lang historie, men det meste af den er skrevet til konsollen.

Resultatet af udførelsen er som følger.

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

処理を終了します。

Jeg vil dele det op i afsnit.

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

Jeg opretter to databasekontekster, Hvis du deler én databasekontekst, cachelagres den, når dataene læses, og det er den samme forekomst. Hvis vi antager, at hver enkelt åbnes separat, oprettes der to databasekontekster. dbContextC er til kontrol af værdierne i databasen. Jeg har ikke rigtig brug for det, fordi A eller B kan erstattes.

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

Hvis vi antager, at de tilgås separat, læser de en fra forskellige databasesammenhænge Book . På dette tidspunkt har vi ikke ændret noget, så de er alle ens.

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

Først ændrede og opdaterede jeg hr./Ms.'s bog. Opdateringsprocessen UpdateToDatabase opsummeres i en metode, og der vises en meddelelse, når der opstår en undtagelse.

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

Som følge heraf er den blevet opdateret, og Price RowVersion af bog A er blevet opdateret. Hvis du læser DB-værdien direkte, vil den opdaterede værdi også være den samme som den opdaterede værdi. Jeg var i stand til at opdatere det, fordi begge kom i RowVersion A og RowVersion i DB var AAAAAAAAH2k= . Versionen er ændret AAAAAAAAH2o= på grund af opdateringen.

Efter opdatering af A skal du opdatere B på samme måde.

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

Resultatet er en undtagelse, DbUpdateConcurrencyException og opdateringen mislykkedes. bookB.RowVersion AAAAAAAAH2k= er , men RowVersion da det allerede AAAAAAAAH2o= er , vurderes det til at være et mismatch, og der opstår en opdateringsfejl. Du kan se, at den Price lokale variabel bookB er ændret, men RowVersion Du kan se, at værdierne på databasesiden slet ikke er ændret.

Resumé

Dette er den nemmeste af de mange låsetyper at implementere, for når du bruger den automatisk genererede kode i SQL Server og Entity Framework Core, implementeres optimistisk samtidighed som standard. Men da det kun er for at forhindre "datakorruption, der skal opdateres", er det nødvendigt at håndtere undtagelser korrekt, når andre data er involveret, eller brugeroperationer er involveret.

Denne gang lykkedes det mig heller ikke at gøre noget, fordi jeg implementerede RowVersion det i en simpel konsolapplikation. Hvis du vil indsætte redigeringsskærmbilledet efter indlæsning af data i et webprogram eller klientprogram, RowVersion på en eller anden måde, så det kan bestemmes korrekt, når det opdateres.

Entity Framework Core har dog en funktion til sporing af ændringer, så hvis du vil angive den gamle RowVersion til den værdi, der læses fra databasen på opdateringstidspunktet, skal du

bookB.RowVersion = <古い RowVersion>;

Selv hvis det er indstillet som følger, vil det ikke blive korrekt bedømt som "optimistisk samtidighedskontrol". RowVersion Selvom du indstiller værdien til normalt, vil den kun blive genkendt som den ændrede værdi, så følgende er

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

Det er nødvendigt at omskrive værdien før ændringen.