Optimistic Concurrency Control novērš datu zudumu, ko izraisa pēdējās uzvaras atjauninājumi ar vairākām piekļuves iespējām (SQL Server)
Darbības vide
- Visual Studio
-
- Visual Studio 2022
- .NETO
-
- .NET 8
- Entītiju struktūras kodols
-
- Entītijas struktūras kodols 8.0
- SQL serveris
-
- SQL Server 2022
* Iepriekš minētais ir verifikācijas vide, taču tas var darboties ar citām versijām.
Par uzvarošajiem atjauninājumiem pēc kontroles
Tīmekļa lietojumprogrammā vai klienta-servera lietojumprogrammā vairāki cilvēki var piekļūt datiem un atjaunināt tos no vienas datu bāzes. Ja nekas netiek darīts, tās personas dati, kura to vēlāk atjaunināja, tiks atspoguļoti datu bāzē kā jaunākie.
Parasti nav īpašas problēmas, ka tās personas dati, kura vēlāk atjaunināja, tiek atspoguļoti kā jaunākie. Problēmas var rasties, ja vairāki cilvēki mēģina vienlaikus piekļūt vieniem un tiem pašiem datiem un tos atjaunināt.
Piemēram, pieņemsim, ka jums ir tālāk norādītie grāmatas dati.
Parametra nosaukuma | vērtība |
---|---|
Grāmatas nosaukums | Datu bāzu grāmatas |
cena | 1000 |
Ja divi cilvēki atver ekrānu, lai vienlaikus rediģētu šos datus, tiks parādīta iepriekš minētā vērtība. Mr./Ms. A mēģina paaugstināt šīs grāmatas cenu par 500 jenām. Mr./Ms. B vēlāk tiek uzdots paaugstināt šīs grāmatas cenu vēl par 300 jenām.
Ja abi no tiem paaugstina cenu atsevišķi, nevis vienlaicīgi, grāmatas cena būs 1800 jenu. Ja jūs tam piekļūstat vienlaicīgi, tas tiks reģistrēts attiecīgi kā 1500 jenas un 1300 jenas, tāpēc tas nebūs 1800 jenu neatkarīgi no tā, kurš no tiem reģistrējas.
Problēma šeit ir tā, ka persona, kas vēlāk atjaunināja, var to atjaunināt, nezinot iepriekš atjaunināto informāciju.
Optimistiska saskaņa
Iepriekš minēto problēmu var atrisināt, šoreiz veicot "optimistisku sakritības kontroli". Lai vienkārši izskaidrotu, kāda veida kontrole ir "vispirms uzvarēt, ja vienlaikus mēģināt rediģēt datus". Tie, kas mēģinās atjaunot vēlāk, atjaunināšanas laikā saņems kļūdu un nevarēs reģistrēties.
Jūs varat domāt, ka ar to nevarat reģistrēt jaunus datus, taču tas ir tikai tad, "kad mēģināt tos mainīt vienlaikus". Diviem cilvēkiem ir iespējams rediģēt pilnīgi atšķirīgos laikos. Protams, tādā gadījumā pēdējās atjauninātās personas dati būs jaunākie.
Konkrēti, kāda veida apstrādes kontroli var panākt, ja "ir datu versija". Piemēram, iepriekš minētajā piemērā jums būs šādi dati.
Parametra nosaukuma | vērtība |
---|---|
Grāmatas nosaukums | Datu bāzu grāmatas |
cena | 1000 |
versija | 1 |
Katram ieraksta atjauninājumam versija tiek palielināta par 1. Piemēram, ja Mr./Ms. A nosaka cenu uz 1500 jenām, versija būs 2. Tajā laikā nosacījums, ka atjauninājumu var veikt, ir tāds, ka versija pirms atjaunināšanas ir tāda pati kā versija datu bāzē. Kad mr./ms. to atjaunina, datu bāzes versija ir 1, un sākotnējo datu versija, kas pašlaik tiek rediģēta, ir 1, tāpēc to var atjaunināt.
Pamatojoties uz šo specifikāciju, pieņemsim, ka mr./ms. A un mr./ms. B rediģē vienus un tos pašus datus. Kad Mr./Ms. pirmo reizi noteica cenu uz 1500 jenām un mēģināja atjaunināt datubāzi, versija bija tāda pati, tāpēc to varēja atjaunināt. Tādā gadījumā datu bāzes versija būs 2. Mr./Ms. B mēģina rediģēt datus par 1000 jenām un atjaunināt tos datu bāzē kā 1300 jenas. Atjaunināšana neizdodas, jo pieejamā versija ir 1, bet datu bāzes versija jau ir 2. Tādā veidā darbojas optimistiska saskaņa.
Entity Framework Core ietver šo "optimistisko sakritību" no kastes, padarot to salīdzinoši viegli īstenojamu.
Starp citu, "optimistiskā sakritība" ir pazīstama arī kā "optimistiska slēdzene" vai "optimistiska slēdzene", un dažreiz to pārbauda un runā par šo nosaukumu. Tā ir bloķēšanas metode, ka datus nevar atjaunināt, bet datus var nolasīt. Ir arī kontrole, ko sauc par "pesimistisku slēdzeni" kā vēl vienu bloķēšanas metodi, kas nav "optimistiska bloķēšana". Šī ir metode, kas bloķē datu ielādi, kad pirmā persona lasa datus, un pat neļauj rediģēt darbības. Tas var atrisināt problēmu, ka datus nevar atjaunināt, pat ja tie ir mainīti, bet, kad kāds rediģē datus, citi cilvēki nevar atvērt datu rediģēšanas ekrānu, un, ja atbloķēšana neizdodas, dati tiks bloķēti uz visiem laikiem. Abiem ir priekšrocības un trūkumi, tāpēc tas ir atkarīgs no operācijas, kuru pieņemt.
Datu bāzes izveide
Šajā rakstā es paskaidrošu, kā vispirms izveidot SQL Server datu bāzi un pēc tam automātiski ģenerēt kodu. Ja vēlaties to ieviest koda pirmajā veidā, lūdzu, šoreiz skatiet automātiski ģenerēto kodu un ieviesiet to apgrieztā procedūrā.
Datu bāzes izveide
Varat arī to izdarīt SQL, taču to ir vieglāk izdarīt, izmantojot GUI, tāpēc es šoreiz to daru ar GUI. Izņemot datu bāzes nosaukumu, tas tiek izveidots pēc noklusējuma.
Tabulas izveide
Izveidojiet to, izmantojot šādu 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 nav jāuztraucas par lielāko daļu parametru, jo tie ir paredzēti tikai datu atjauninājumiem.
Interesējošais parametrs šoreiz ir RowVersion
kolonna, kas apraksta . Tā ir ierakstu versiju izveide.
Norādot kā timestamp
tipu, versija tiek automātiski palielināta katru reizi, kad ieraksts tiek atjaunināts.
Turklāt, tā kā šī versija tiek pārvaldīta, pamatojoties uz tabulu, būtībā nav tās pašas versijas ieraksta, ja vien to neiestatāt manuāli.
Ieraksta pievienošana
To var pievienot ar šādu 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
Kolonnas nav jāiestata, jo tās tiek iestatītas automātiski.
Projekta izveide un automātiska koda ģenerēšana
Šoreiz mēs pārbaudīsim darbību ar konsoles lietojumprogrammu. Darbības, lai izveidotu projektu un automātiski ģenerētu kodu, ir aprakstītas šajos padomos, tāpēc es šeit tajos neiedziļināšos.
Ģenerētais kods ir šāds:
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!;
}
Optimistiskās vienlaicīguma kontroles darbības pārbaude
Tā kā mēs šoreiz to izmantojam vienā lietojumprogrammā, mēs vienlaikus tam stingri nepiekļūstam, bet mēs vēlētos to īstenot tai tuvā formā.
Tāpat kā piemērā sākumā, tiek iegūti divi datu gabali, un, kad katrs tiek atjaunināts, pamatojoties uz pirmajiem datiem, pārbaudiet, vai pēdējais atjauninātājs saņems kļūdu.
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);
}
}
Tas ir garš stāsts, bet lielākā daļa no tā ir rakstīta konsolei.
Izpildes rezultāts ir šāds.
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="}
処理を終了します。
Es to sadalīšu sadaļās.
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("データベースコンテキストを作成しました。");
Es veidoju divus datu bāzes kontekstus,
Ja koplietojat vienu datu bāzes kontekstu, tas tiks saglabāts kešatmiņā, kad dati tiks nolasīti, un tas būs tas pats gadījums.
Pieņemot, ka katram no tiem var piekļūt atsevišķi, tiek izveidoti divi datu bāzes konteksti.
dbContextC
ir paredzēts vērtību pārbaudei datu bāzē. Man tas īsti nav vajadzīgs, jo A vai B var aizstāt.
// それぞれがデータを編集しようと読み込む
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))}");
Pieņemot, ka tiem piekļūst atsevišķi, viņi lasa vienu no dažādiem datu bāzes kontekstiem Book
.
Šajā brīdī mēs neko neesam mainījuši, tāpēc viņi visi ir vienādi.
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="}
Pirmkārt, es mainīju un atjaunināju Mr./Ms. grāmatu.
Atjaunināšanas process tiek UpdateToDatabase
apkopots, izmantojot metodi, un, ja rodas izņēmums, tiek parādīts ziņojums.
// 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="}
Tā rezultātā tas ir veiksmīgi atjaunināts, un Price
RowVersion
A grāmata ir atjaunināta.
Turklāt, ja DB vērtību lasīsit tieši, atjauninātā vērtība būs tāda pati kā atjauninātā vērtība.
Es varēju to atjaunināt, jo gan A, RowVersion
gan RowVersion
DB bija AAAAAAAAH2k=
.
Versija ir mainījusies AAAAAAAAH2o=
atjauninājuma dēļ.
Pēc A atjaunināšanas tādā pašā veidā atjauniniet B.
// そのあと 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="}
Rezultāts ir izņēmumsDbUpdateConcurrencyException
, un atjaunināšana nav izdevusies.
bookB.RowVersion
AAAAAAAAH2k=
ir , bet RowVersion
tā kā jau AAAAAAAAH2o=
ir , tas tiek vērtēts kā neatbilstība un rodas atjaunināšanas kļūda.
Jūs varat redzēt, ka vietējais mainīgais Price
bookB
ir mainījies, bet RowVersion
Jūs varat redzēt, ka vērtības datu bāzes pusē vispār nav mainījušās.
Kopsavilkuma
Tas ir vienkāršākais no vairākiem bloķēšanas veidiem, kas jāievieš, jo, izmantojot automātiski ģenerēto kodu SQL Server un Entity Framework Core, pēc noklusējuma tiek ieviesta optimistiska sakritība. Tomēr, tā kā tas ir tikai tādēļ, lai novērstu "datu sabojāšanu, kas jāatjaunina", ir pienācīgi jārīkojas ar izņēmumiem, ja ir iesaistīti citi dati vai ir iesaistītas lietotāju darbības.
Arī šoreiz man nekas neizdevās, jo es to ieviesu RowVersion
vienkāršā konsoles lietojumprogrammā.
Ja vēlaties ievietot rediģēšanas ekrānu pēc datu ielādes tīmekļa lietojumprogrammā vai klienta lietojumprogrammā,
RowVersion
kaut kādā veidā, lai to varētu pareizi noteikt, kad tas tiek atjaunināts.
Tomēr entity Framework Core ir izmaiņu izsekošanas funkcija, tādēļ, ja vēlaties iestatīt veco RowVersion
vērtību, kas atjaunināšanas laikā tiek nolasīta no DB,
bookB.RowVersion = <古い RowVersion>;
Pat ja tas ir noteikts šādi, tas netiks pareizi novērtēts kā "optimistiska saskaņas kontrole".
RowVersion
Pat ja vērtību iestatāt uz parasti, tā tiks atpazīta tikai kā mainītā vērtība, tāpēc tālāk ir norādīts
dbContextB.Entry(bookB).Property("RowVersion").OriginalValue = <古い RowVersion>;
Pirms izmaiņām ir nepieciešams pārrakstīt vērtību.