Optimistisk samtidighetskontroll förhindrar dataförlust på grund av senaste vinstuppdateringar med flera åtkomster (SQL Server)

Sidan uppdaterad :
Datum för skapande av sida :

Omvärld

Visuell studio
  • Visual Studio 2022
.NÄT
  • .NET 8
Kärna för entitetsramverk
  • Entity Framework Core 8.0
SQL Server
  • SQL Server 2022

* Ovanstående är en verifieringsmiljö, men den kan fungera med andra versioner.

Om att vinna uppdateringar efter ingen kontroll

I ett webbprogram eller ett klient-server-program kan flera personer komma åt och uppdatera data från en enda databas. Om inget särskilt görs kommer uppgifterna om den person som uppdaterade det senare att återspeglas i databasen som det senaste.

Normalt är det inget särskilt problem att uppgifterna för den person som uppdaterade senare återspeglas som de senaste. Problem kan uppstå när flera personer försöker komma åt och uppdatera samma data samtidigt.

Anta till exempel att du har följande bokdata.

Parameternamn Värde
Bokens namn Databas Böcker
pris 1000

Om två personer öppnar skärmen för att redigera dessa data samtidigt kommer ovanstående värde att visas. Mr./Ms. A försöker höja priset på den här boken med 500 yen. Mr./Ms. B instrueras senare att höja priset på denna bok med ytterligare 300 yen.

Om de två höjer priset var för sig istället för samtidigt blir priset på boken 1800 yen. Om du går in på den samtidigt kommer den att registreras som 1500 yen respektive 1300 yen, så det kommer inte att vara 1800 yen oavsett vilken som registrerar sig.

Problemet här är att personen som uppdaterade senare kan uppdatera den utan att känna till den information som tidigare uppdaterades.

Optimistisk samtidighet

Det tidigare nämnda problemet kan lösas genom att utföra "optimistisk samtidighetskontroll" den här gången. För att enkelt förklara, vilken typ av kontroll är att "vinna först om du försöker redigera data samtidigt". De som försöker förnya senare kommer att få ett felmeddelande vid tidpunkten för uppdateringen och kommer inte att kunna registrera sig.

Du kanske tror att du inte kan registrera ny data med detta, men det är bara "när du försöker ändra den samtidigt". Det är möjligt för två personer att redigera vid helt olika tidpunkter. Naturligtvis, i så fall, kommer uppgifterna för den senast uppdaterade personen att vara de senaste.

Specifikt, vilken typ av bearbetningskontroll som kan uppnås genom att "ha en version av data". I exemplet ovan har du till exempel följande data.

Parameternamn Värde
Bokens namn Databas Böcker
pris 1000
version 1

Versionen ökas med 1 för varje postuppdatering. Till exempel, om Mr./Ms. A sätter priset till 1500 yen, kommer versionen att vara 2. Förutsättningen för att uppdateringen ska kunna göras är då att versionen före uppdateringen är densamma som versionen i databasen. När Mr./Ms. uppdaterar den är versionen i databasen 1 och den version av originaldata som för närvarande redigeras är 1, så den kan uppdateras.

Baserat på denna specifikation antar du en situation där Herr/Fru A och Herr/Fru B redigerar samma data. När Mr./Ms. först satte priset till 1500 yen och försökte uppdatera databasen var versionen densamma, så den kunde uppdateras. I så fall kommer versionen i databasen att vara 2. Mr./Ms. B försöker redigera data på 1000 yen och uppdatera den till databasen som 1300 yen. Uppdateringen misslyckas eftersom den aktuella versionen är 1, men versionen i databasen är redan 2. Så här fungerar optimistisk samtidighet.

Entity Framework Core innehåller denna "optimistiska samtidighet" direkt, vilket gör det relativt enkelt att implementera.

Förresten, "optimistisk samtidighet" är också känd som "optimistisk lås" eller "optimistisk lås", och det undersöks och pratas ibland om med detta namn. Det är en låsningsmetod att data inte kan uppdateras, men data kan läsas. Det finns också en kontroll som kallas "pessimistiskt lås" som en annan låsmetod än "optimistiskt lås". Detta är en metod som låser dataladdningen när den första personen läser data och inte ens tillåter redigeringsoperationer. Det kan lösa problemet med att data inte kan uppdateras trots att de har ändrats, men när någon redigerar data kan andra personer inte öppna dataredigeringsskärmen, och om upplåsningen misslyckas kommer data att låsas för alltid. Båda har fördelar och nackdelar, så det beror på operationen vilken man ska anta.

Skapa en databas

I den här artikeln kommer jag att förklara hur man först skapar en databas för SQL Server och sedan automatiskt genererar kod. Om du vill implementera den på ett kod-först-sätt, hänvisa till den automatiskt genererade koden den här gången och implementera den i omvänd procedur.

Skapa en databas

Du kan också göra det i SQL, men det är lättare att göra det med ett GUI, så jag gör det med ett GUI den här gången. Förutom databasnamnet skapas det som standard.

Skapa en tabell

Skapa den med följande 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 inte oroa dig för de flesta parametrarna eftersom de bara är till för datauppdateringar. Parametern av intresse den här gången är RowVersion kolumnen som beskriver . Det här är versionshantering av arkivhandlingar. Genom att ange som timestamp typ ökas versionen automatiskt varje gång posten uppdateras. Dessutom, eftersom den här versionen hanteras per tabell, finns det i princip ingen post för samma version om du inte ställer in den manuellt.

Lägga till en post

Du kan lägga till den med följande 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 inte ange kolumnerna eftersom de ställs in automatiskt.

Skapa ett projekt och generera kod automatiskt

Den här gången kommer vi att kontrollera operationen med konsolapplikationen. Stegen för att skapa ett projekt och automatiskt generera kod beskrivs i följande tips, så jag kommer inte att gå in på dem här.

Den genererade koden är som följer:

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

Kontrollera funktionen för optimistisk samtidighetskontroll

Eftersom vi kör det i en applikation den här gången har vi inte strikt tillgång till det samtidigt, men vi skulle vilja implementera det i en form nära den.

Som i exemplet i början förvärvas två datadelar, och när var och en uppdateras baserat på den första datan, kontrollera om den senare uppdateraren kommer att få ett fel.

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 är en lång historia, men det mesta är skrivet till konsolen.

Resultatet av körningen är följande.

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

処理を終了します。

Jag kommer att dela upp det i sektioner.

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

Jag skapar två databaskontexter, Om du delar en databaskontext cachelagras den när data läses och det kommer att vara samma instans. Förutsatt att var och en används separat skapas två databaskontexter. dbContextC är till för att kontrollera värdena i databasen. Jag behöver det egentligen inte eftersom A eller B kan ersättas.

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

Förutsatt att de används separat läser de en från olika databaskontexter Book . Vid det här laget har vi inte ändrat någonting, så de är alla likadana.

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 ändrade och uppdaterade jag Mr./Ms.'s bok. Uppdateringsprocessen UpdateToDatabase sammanfattas i en metod och ett meddelande visas när ett undantag inträffar.

// 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 ett resultat har den uppdaterats framgångsrikt, och Price RowVersion och av bok A har uppdaterats. Om du läser DB-värdet direkt kommer det uppdaterade värdet att vara detsamma som det uppdaterade värdet. Jag kunde uppdatera den eftersom både kom i RowVersion A och RowVersion i DB var AAAAAAAAH2k= . Versionen har ändrats AAAAAAAAH2o= på grund av uppdateringen.

När du har uppdaterat A uppdaterar du B på samma sätt.

// そのあと 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 är ett undantag DbUpdateConcurrencyException och uppdateringen har misslyckats. bookB.RowVersion AAAAAAAAH2k= är , men RowVersion eftersom det redan AAAAAAAAH2o= är bedöms det vara en felmatchning och ett uppdateringsfel uppstår. Du kan se att den Price lokala variabeln bookB har ändrats, men RowVersion Du kan se att värdena på databassidan inte har ändrats alls.

Sammanfattning

Det här är den enklaste av de flera låstyperna att implementera, eftersom optimistisk samtidighet implementeras som standard när du använder den automatiskt genererade koden i SQL Server och Entity Framework Core. Men eftersom det bara är för att förhindra att "datakorruption uppdateras" är det nödvändigt att hantera undantag på rätt sätt när andra data är inblandade eller användaråtgärder är inblandade.

Den här gången lyckades jag inte heller med något eftersom jag implementerade RowVersion det i en enkel konsolapplikation. Om du vill infoga redigeringsskärmen efter att ha läst in data i ett webbprogram eller klientprogram, RowVersion på något sätt så att den kan bestämmas korrekt när den uppdateras.

Entity Framework Core har dock en funktion för ändringsspårning, så om du vill ange det gamla RowVersion till det värde som läses från databasen vid tidpunkten för uppdateringen,

bookB.RowVersion = <古い RowVersion>;

Även om den är inställd på följande sätt kommer den inte att bedömas korrekt som "optimistisk samtidighetskontroll". RowVersion Även om du ställer in värdet på normalt kommer det bara att kännas igen som det ändrade värdet, så följande är

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

Det är nödvändigt att skriva om värdet före ändringen.