Optimistinis sutapimo valdymas apsaugo nuo duomenų praradimo dėl paskutinio laimėjimo naujinimų su keliomis prieigomis (SQL serveris)

Puslapis atnaujintas :
Puslapio sukūrimo data :

Darbo aplinka

Vizualinė studija
  • "Visual Studio 2022"
.GRYNOJI
  • .NET 8
Subjekto pagrindų branduolys
  • Subjektų sistemos branduolys 8.0
SQL serveris
  • "SQL Server 2022"

* Tai, kas išdėstyta pirmiau, yra patvirtinimo aplinka, tačiau ji gali veikti su kitomis versijomis.

Apie sėkmingus atnaujinimus be jokios kontrolės

Žiniatinklio taikomojoje programoje arba kliento-serverio programoje keli žmonės gali pasiekti ir atnaujinti duomenis iš vienos duomenų bazės. Jei konkrečiai nieko nedaroma, vėliau jį atnaujinusio asmens duomenys duomenų bazėje atsispindės kaip naujausi.

Paprastai nėra jokios ypatingos problemos, kad vėliau atnaujinto asmens duomenys atsispindi kaip naujausi. Problemų gali kilti, kai keli žmonės bando pasiekti ir atnaujinti tuos pačius duomenis tuo pačiu metu.

Pavyzdžiui, tarkime, kad turite šiuos knygos duomenis.

Parametro pavadinimo reikšmė
Knygos pavadinimas Duomenų bazių knygos
kaina 1000

Jei du žmonės atidaro ekraną, kad vienu metu redaguotų šiuos duomenis, bus rodoma aukščiau pateikta vertė. Mr./Ponia A bando pakelti šios knygos kainą 500 jenų. Pone/poniai B vėliau nurodoma pakelti šios knygos kainą dar 300 jenų.

Jei du iš jų pakels kainą atskirai, o ne tuo pačiu metu, knygos kaina bus 1800 jenų. Jei jį pasieksite tuo pačiu metu, jis bus užregistruotas atitinkamai kaip 1500 jenų ir 1300 jenų, taigi jis nebus 1800 jenų, nesvarbu, kuris iš jų registruojasi.

Problema ta, kad asmuo, kuris atnaujino vėliau, gali jį atnaujinti nežinodamas anksčiau atnaujintos informacijos.

Optimistinis sutapimas

Minėtą problemą šį kartą galima išspręsti atlikus "optimistinę konkurencinę kontrolę". Paprasčiau paaiškinti, kokia kontrolė yra "pirmiausia laimėti, jei bandysite redaguoti duomenis tuo pačiu metu". Tie, kurie bandys atnaujinti vėliau, atnaujinimo metu gaus klaidą ir negalės užsiregistruoti.

Galbūt manote, kad negalite užregistruoti naujų duomenų, tačiau tai tik "kai bandote juos pakeisti tuo pačiu metu". Du žmonės gali redaguoti visiškai skirtingu laiku. Žinoma, tokiu atveju paskutinio atnaujinto asmens duomenys bus naujausi.

Tiksliau, kokią apdorojimo kontrolę galima pasiekti "turint duomenų versiją". Pavyzdžiui, aukščiau pateiktame pavyzdyje turėsite šiuos duomenis.

Parametro pavadinimo reikšmė
Knygos pavadinimas Duomenų bazių knygos
kaina 1000
versija 1

Versija padidinama 1 kiekvienam įrašo naujinimui. Pavyzdžiui, jei ponas / ponia A nustato kainą iki 1500 jenų, versija bus 2. Tuo metu sąlyga, kad galima atnaujinti, yra ta, kad versija prieš naujinimą yra tokia pati kaip ir duomenų bazės versija. Kai ponas /ponia jį atnaujina, duomenų bazėje esanti versija yra 1, o šiuo metu redaguojamų originalių duomenų versija yra 1, todėl ją galima atnaujinti.

Remiantis šia specifikacija, daroma prielaida, kad p. / ponia A ir ponas / ponia B redaguoja tuos pačius duomenis. Kai ponas /ponia pirmą kartą nustatė 1500 jenų kainą ir bandė atnaujinti duomenų bazę, versija buvo ta pati, todėl ją buvo galima atnaujinti. Tokiu atveju duomenų bazės versija bus 2. Mr./Ponia B bando redaguoti 1000 jenų duomenis ir atnaujinti juos į duomenų bazę kaip 1300 jenų. Nepavyksta atnaujinti, nes turima versija yra 1, bet versija duomenų bazėje jau yra 2. Taip veikia optimistinis sutapimas.

"Entity Framework Core" apima šį "optimistinį sutapimą", todėl jį gana lengva įgyvendinti.

Beje, "optimistinis sutapimas" taip pat žinomas kaip "optimistinis užraktas" arba "optimistinis užraktas", ir kartais jis nagrinėjamas ir kalbama apie šį pavadinimą. Tai užrakinimo būdas, kai duomenų negalima atnaujinti, tačiau duomenis galima perskaityti. Taip pat yra kontrolė, vadinama "pesimistiniu užraktu", kaip dar vienas fiksavimo būdas, išskyrus "optimistinį užraktą". Tai metodas, kuris užrakina duomenų įkėlimą, kai pirmasis asmuo perskaito duomenis ir net neleidžia redaguoti operacijų. Tai gali išspręsti problemą, kad duomenų negalima atnaujinti, net jei jie buvo pakeisti, tačiau kai kas nors redaguoja duomenis, kiti žmonės negali atidaryti duomenų redagavimo ekrano, o jei atrakinti nepavyks, duomenys bus užrakinti visam laikui. Abu turi privalumų ir trūkumų, todėl nuo operacijos priklauso, kurį iš jų priimti.

Duomenų bazės kūrimas

Šiame straipsnyje paaiškinsiu, kaip pirmiausia sukurti SQL serverio duomenų bazę ir tada automatiškai generuoti kodą. Jei norite jį įdiegti pirmiausia kodu, šį kartą peržiūrėkite automatiškai sugeneruotą kodą ir įdiekite jį atvirkštine tvarka.

Duomenų bazės kūrimas

Taip pat galite tai padaryti naudodami SQL, bet lengviau tai padaryti naudojant GUI, todėl šį kartą kuriu su GUI. Išskyrus duomenų bazės pavadinimą, jis sukuriamas pagal numatytuosius nustatymus.

Lentelės kūrimas

Sukurkite jį naudodami šį 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

Jums nereikia jaudintis dėl daugumos parametrų, nes jie skirti tik duomenų naujinimams. Šį kartą dominantis parametras yra RowVersion stulpelis, kuriame aprašoma . Tai yra įrašų versijos. Nurodant kaip timestamp tipą, versija automatiškai padidinama kiekvieną kartą, kai atnaujinamas įrašas. Be to, kadangi ši versija valdoma pagal lentelę, iš esmės nėra tos pačios versijos įrašo, nebent ją nustatysite rankiniu būdu.

Įrašo įtraukimas

Jį galite pridėti naudodami šį 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 Stulpelių nustatyti nereikia, nes jie nustatomi automatiškai.

Sukurkite projektą ir automatiškai sugeneruokite kodą

Šį kartą mes patikrinsime operaciją su konsolės programa. Projekto kūrimo ir automatinio kodo generavimo veiksmai aprašyti šiuose patarimuose, todėl čia į juos nesigilinsiu.

Sugeneruotas kodas yra toks:

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

Optimistinės konkurencinės kontrolės veikimo tikrinimas

Kadangi šį kartą ją paleidžiame vienoje programoje, tuo pačiu metu jos griežtai nepasiekiame, tačiau norėtume ją įgyvendinti artima forma.

Kaip ir pradžioje pateiktame pavyzdyje, gaunami du duomenų elementai, o kai kiekvienas atnaujinamas pagal pirmuosius duomenis, patikrinkite, ar pastarasis atnaujinimas negaus klaidos.

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

Tai ilga istorija, tačiau didžioji jos dalis parašyta į konsolę.

Vykdymo rezultatas yra toks.

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

処理を終了します。

Aš jį suskirstysiu į skyrius.

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

Kuriu du duomenų bazės kontekstus, Jei bendrinate vieną duomenų bazės kontekstą, jis bus saugomas talpykloje, kai bus skaitomi duomenys, ir tai bus tas pats egzempliorius. Darant prielaidą, kad kiekvienas iš jų pasiekiamas atskirai, sukuriami du duomenų bazės kontekstai. dbContextC skirtas patikrinti duomenų bazės reikšmes. Man to tikrai nereikia, nes A ar B galima pakeisti.

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

Darant prielaidą, kad jie pasiekiami atskirai, jie skaito vieną iš skirtingų duomenų bazių kontekstų Book . Šiuo metu mes nieko nekeitėme, todėl jie visi vienodi.

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

Pirmiausia pakeičiau ir atnaujinau pono/ponios knygą. Atnaujinimo procesas UpdateToDatabase apibendrinamas metodu, o atsiradus išimtims rodomas pranešimas.

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

Dėl to jis buvo sėkmingai atnaujintas, o Price RowVersion A knyga buvo atnaujinta. Be to, jei DB reikšmę perskaitysite tiesiogiai, atnaujinta reikšmė bus tokia pati kaip atnaujinta reikšmė. Galėjau jį atnaujinti, nes abu pateko į RowVersion A ir RowVersion DB buvo AAAAAAAAH2k= . Versija pasikeitė AAAAAAAAH2o= dėl atnaujinimo.

Atnaujinę A, atnaujinkite B tokiu pačiu būdu.

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

Rezultatas yra išimtis DbUpdateConcurrencyException ir atnaujinti nepavyko. bookB.RowVersion AAAAAAAAH2k= yra , bet RowVersion kadangi jau AAAAAAAAH2o= yra , tai laikoma neatitikimu ir įvyksta atnaujinimo klaida. Matote, kad Price vietinis kintamasis bookB pasikeitė, bet RowVersion Matote, kad duomenų bazės pusės reikšmės visiškai nepasikeitė.

Suvestinė

Tai lengviausia įdiegti iš kelių užrakto tipų, nes kai naudojate automatiškai sugeneruotą kodą "SQL Server" ir "Entity Framework Core", optimistinis sutapimas įdiegiamas pagal numatytuosius nustatymus. Tačiau, kadangi tai yra tik siekiant užkirsti kelią "duomenų korupcijai, kurią reikia atnaujinti", būtina tinkamai tvarkyti išimtis, kai tai susiję su kitais duomenimis arba vartotojo operacijomis.

Be to, šį kartą man nieko nepavyko, nes įdiegiau RowVersion tai paprastoje konsolės programoje. Jei norite įterpti redagavimo ekraną įkėlę duomenis į žiniatinklio programą arba kliento programą, RowVersion tam tikru būdu, kad jį būtų galima tinkamai nustatyti, kai jis atnaujinamas.

Tačiau "Entity Framework Core" turi pakeitimų sekimo funkciją, taigi, jei norite nustatyti seną RowVersion reikšmę, nuskaitytą iš DB atnaujinimo metu,

bookB.RowVersion = <古い RowVersion>;

Net jei jis bus nustatytas taip, jis nebus teisingai vertinamas kaip "optimistinė konkurencinė kontrolė". RowVersion Net jei reikšmę nustatysite įprastai, ji bus atpažįstama tik kaip pakeista reikšmė, todėl toliau nurodyta

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

Prieš pakeitimą būtina perrašyti vertę.