Optimistlik kokkulangevuse kontroll hoiab ära andmete kadumise viimase võidu mitme juurdepääsuga värskenduste tõttu (SQL Server)

Lehekülg uuendatud :
Lehe loomise kuupäev :

Töökeskkond

Visuaalne stuudio
  • Visual Studio 2022
.NET
  • .NET 8
Olemi raamistiku tuum
  • Olemi raamistiku tuum 8.0
SQL Server
  • SQL Server 2022

* Ülaltoodud on kinnituskeskkond, kuid see võib töötada ka teiste versioonidega.

Teave võitvate värskenduste kohta pärast kontrolli puudumist

Veebirakenduses või klient-server rakenduses võivad mitu inimest pääseda juurde ja värskendada andmeid ühest andmebaasist. Kui midagi konkreetselt ei tehta, kajastuvad hiljem seda uuendanud isiku andmed andmebaasis viimasena.

Tavaliselt ei ole erilist probleemi, et hiljem värskendanud isiku andmed kajastuvad viimastena. Probleemid võivad tekkida siis, kui mitu inimest üritavad samadele andmetele samal ajal juurde pääseda ja neid värskendada.

Oletame näiteks, et teil on järgmised raamatuandmed.

Parameetri nimi Väärtus
Raamatu nimi Andmebaasi raamatud
hind 1000

Kui kaks inimest avavad ekraani, et neid andmeid samal ajal redigeerida, kuvatakse ülaltoodud väärtus. Hr/pr A üritab selle raamatu hinda tõsta 500 jeeni võrra. Hr/pr B-le antakse hiljem korraldus tõsta selle raamatu hinda veel 300 jeeni võrra.

Kui nad kaks tõstavad hinda eraldi, mitte samal ajal, on raamatu hind 1800 jeeni. Kui pääsete sellele juurde samal ajal, registreeritakse see vastavalt 1500 jeenina ja 1300 jeenina, nii et see ei ole 1800 jeeni, olenemata sellest, milline neist registreerib.

Probleem on selles, et hiljem värskendanud isik saab seda värskendada, teadmata eelnevalt värskendatud teavet.

Optimistlik kokkulangevus

Eelmainitud probleemi saab lahendada, tehes seekord "optimistliku kokkulangevuse kontrolli". Lihtsalt selgitades, milline kontroll on "võita kõigepealt, kui proovite andmeid samal ajal redigeerida". Need, kes proovivad hiljem uuendada, saavad värskenduse ajastusel vea ja ei saa registreeruda.

Võite arvata, et te ei saa sellega uusi andmeid registreerida, kuid see on ainult "kui proovite neid samal ajal muuta". Kahel inimesel on võimalik redigeerida täiesti erinevatel aegadel. Loomulikult on sellisel juhul viimati uuendatud isiku andmed uusimad.

Täpsemalt, millist töötlemiskontrolli on võimalik saavutada "andmete versiooni omamisega". Näiteks ülaltoodud näites on teil järgmised andmed.

Parameetri nimi Väärtus
Raamatu nimi Andmebaasi raamatud
hind 1000
versioon 1

Versiooni suurendatakse iga kirje värskenduse puhul 1 võrra. Näiteks kui hr/pr A määrab hinnaks 1500 jeeni, on versioon 2. Sel ajal on tingimus, et värskendust saab teha, et enne värskendust olev versioon on sama, mis andmebaasi versioon. Kui hr/pr. seda värskendab, on andmebaasi versioon 1 ja praegu redigeeritavate algandmete versioon on 1, nii et seda saab uuendada.

Oletame selle spetsifikatsiooni alusel, et olukord, kus hr / pr A ja hr / pr B redigeerivad samu andmeid. Kui hr/pr. määras esmakordselt hinnaks 1500 jeeni ja proovis andmebaasi uuendada, oli versioon sama, nii et seda sai uuendada. Sellisel juhul on andmebaasi versioon 2. Hr/Pr. B üritab redigeerida 1000 jeeni andmeid ja uuendada neid andmebaasis 1300 jeenina. Värskendamine ebaõnnestub, kuna käsilolev versioon on 1, kuid andmebaasi versioon on juba 2. Nii toimib optimistlik kokkulangevus.

Olemi raamistiku tuumik sisaldab seda "optimistlikku kokkulangevust" karbist väljas, muutes selle rakendamise suhteliselt lihtsaks.

Muide, "optimistlik kokkulangevus" on tuntud ka kui "optimistlik lukk" või "optimistlik lukk" ning seda uuritakse ja räägitakse mõnikord selle nimega. See on lukustusmeetod, et andmeid ei saa uuendada, kuid andmeid saab lugeda. Samuti on olemas kontroll, mida nimetatakse "pessimistlikuks lukuks" kui teine lukustusmeetod, välja arvatud "optimistlik lukk". See on meetod, mis lukustab andmete laadimise, kui esimene inimene loeb andmeid ja ei luba isegi redigeerimistoiminguid. See võib lahendada probleemi, et andmeid ei saa värskendada, kuigi neid on muudetud, kuid kui keegi andmeid redigeerib, ei saa teised inimesed andmete redigeerimise ekraani avada ja kui avamine ebaõnnestub, lukustatakse andmed igaveseks. Mõlemal on eeliseid ja puudusi, seega sõltub see operatsioonist, milline neist vastu võtta.

Andmebaasi loomine

Selles artiklis selgitan, kuidas kõigepealt luua SQL Serveri andmebaas ja seejärel genereerida kood automaatselt. Kui soovite seda rakendada koodipõhiselt, vaadake seekord automaatselt genereeritud koodi ja rakendage see vastupidises protseduuris.

Andmebaasi loomine

Saate seda teha ka SQL-is, kuid seda on lihtsam teha GUI-ga, nii et ma teen seda seekord GUI-ga. Välja arvatud andmebaasi nimi, luuakse see vaikimisi.

Tabeli loomine

Looge see järgmise SQL-iga:

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

Te ei pea enamiku parameetrite pärast muretsema, kuna need on mõeldud ainult andmete värskendamiseks. Seekord huvipakkuv parameeter on RowVersion veerg, mis kirjeldab . See on rekordiline versioonimine. Määrates tüübiks timestamp , suurendatakse versiooni automaatselt iga kord, kui kirjet värskendatakse. Samuti, kuna seda versiooni hallatakse tabelipõhiselt, pole põhimõtteliselt sama versiooni kirjet, kui te seda käsitsi ei seadista.

Kirje lisamine

Saate selle lisada järgmise SQL-iga:

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 Te ei pea veerge määrama, kuna need määratakse automaatselt.

Looge projekt ja genereerige kood automaatselt

Seekord kontrollime toimingut konsoolirakendusega. Projekti loomise ja koodi automaatse genereerimise samme on kirjeldatud järgmistes näpunäidetes, nii et ma ei hakka neid siin käsitlema.

Genereeritud kood on järgmine:

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

Optimistliku kokkulangevuse kontrolli toimimise kontrollimine

Kuna me käitame seda seekord ühes rakenduses, ei pääse me sellele samal ajal rangelt juurde, kuid tahaksime seda rakendada sellele lähedasel kujul.

Nagu alguses olevas näites, hangitakse kaks andmeühikut ja kui iga värskendatakse esimeste andmete põhjal, kontrollige, kas viimane värskendaja saab vea.

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

See on pikk lugu, kuid enamik sellest on kirjutatud konsoolile.

Täitmise tulemus on järgmine.

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

処理を終了します。

Jagan selle osadeks.

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

Loon kaks andmebaasikonteksti, Kui jagate ühte andmebaasikonteksti, salvestatakse see andmete lugemisel vahemällu ja see on sama eksemplar. Eeldades, et mõlemale pääseb juurde eraldi, luuakse kaks andmebaasikonteksti. dbContextC on andmebaasi väärtuste kontrollimiseks. Mul pole seda tegelikult vaja, sest A või B saab asendada.

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

Eeldades, et neile pääseb juurde eraldi, loevad nad seda erinevatest andmebaasi kontekstidest Book . Praegu pole me midagi muutnud, nii et nad on kõik ühesugused.

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

Esiteks muutsin ja uuendasin hr/pr. raamatut. Värskendusprotsess on UpdateToDatabase kokku võetud meetodis ja erandi ilmnemisel kuvatakse teade.

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

Selle tulemusena on seda edukalt ajakohastatud ja Price RowVersion raamatut A on uuendatud. Samuti, kui loete DB väärtust otse, on värskendatud väärtus sama, mis värskendatud väärtus. Sain seda värskendada, sest mõlemad said A-s RowVersion ja RowVersion DB-s olid AAAAAAAAH2k= . Versioon on värskenduse tõttu muutunud AAAAAAAAH2o= .

Pärast A värskendamist värskendage B-d samal viisil.

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

Tulemus on erand DbUpdateConcurrencyException ja värskendamine ebaõnnestus. bookB.RowVersion AAAAAAAAH2k= on , kuid RowVersion kuna on juba AAAAAAAAH2o= , hinnatakse seda ebakõlaks ja ilmneb värskendusviga. Näete, et Price kohalik muutuja bookB on muutunud, kuid RowVersion Näete, et andmebaasi poolel olevad väärtused ei ole üldse muutunud.

Kokkuvõte

Seda on mitmest lukutüübist kõige lihtsam rakendada, sest kui kasutate SQL Serveris ja Entity Framework Core'is automaatselt genereeritud koodi, rakendatakse vaikimisi optimistlikku kokkulangevust. Kuna see on aga ainult selleks, et vältida "ajakohastatavate andmete rikkumist", on vaja nõuetekohaselt käsitleda erandeid, kui tegemist on muude andmetega või kui tegemist on kasutaja toimingutega.

Samuti ei saanud ma seekord midagi hallata, sest rakendasin RowVersion selle lihtsas konsoolirakenduses. Kui soovite lisada redigeerimiskuva pärast andmete laadimist veebirakendusse või klientrakendusse, RowVersion mingil viisil, et seda saaks ajakohastamisel korralikult kindlaks määrata.

Kuid olemi raamistiku tuumal on muudatuste jälgimise funktsioon, nii et kui soovite seada vanale RowVersion väärtusele värskenduse ajal DB-st loetud väärtuse,

bookB.RowVersion = <古い RowVersion>;

Isegi kui see on seatud järgmiselt, ei hinnata seda õigesti kui "optimistlikku kokkulangevuse kontrolli". RowVersion Isegi kui määrate väärtuse normaalseks, tuvastatakse see ainult muudetud väärtusena, nii et järgmine on

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

Enne muudatust on vaja väärtus ümber kirjutada.