Optimistinen samanaikaisuuden hallinta estää tietojen menetyksen, joka johtuu viimeisimmistä päivityksistä, joissa on useita käyttöoikeuksia (SQL Server)
Toimintaympäristö
- Visuaalinen studio
-
- Visuaalinen studio 2022
- .VERKKO
-
- .NET 8
- Entiteettikehyksen ydin
-
- Entity Framework Core 8.0
- SQL Server
-
- SQL Server 2022
* Yllä oleva on vahvistusympäristö, mutta se voi toimia muiden versioiden kanssa.
Tietoja päivitysten voittamisesta ilman hallintaa
Verkkosovelluksessa tai asiakas-palvelin-sovelluksessa useat henkilöt voivat käyttää ja päivittää tietoja yhdestä tietokannasta. Jos mitään ei tehdä erityisesti, sen henkilön tiedot, joka päivitti sen myöhemmin, näkyvät tietokannassa viimeisimpänä.
Normaalisti ei ole erityistä ongelmaa, että myöhemmin päivittäneen henkilön tiedot näkyvät uusimpina. Ongelmia voi ilmetä, kun useat ihmiset yrittävät käyttää ja päivittää samoja tietoja samanaikaisesti.
Oletetaan esimerkiksi, että sinulla on seuraavat kirjan tiedot.
Parametrin nimen | arvo |
---|---|
Kirjan nimi | Tietokannan kirjat |
hinta | 1000 |
Jos kaksi henkilöä avaa näytön muokatakseen näitä tietoja samanaikaisesti, yllä oleva arvo näytetään. Herra / rouva A yrittää nostaa tämän kirjan hintaa 500 jenillä. Herra / rouva B saa myöhemmin ohjeet nostaa tämän kirjan hintaa vielä 300 jenillä.
Jos he kaksi nostavat hintaa erikseen eikä samanaikaisesti, kirjan hinta on 1800 jeniä. Jos käytät sitä samanaikaisesti, se rekisteröidään vastaavasti 1500 jeniksi ja 1300 jeniksi, joten se ei ole 1800 jeniä riippumatta siitä, kumpi rekisteröityy.
Ongelmana on, että myöhemmin päivittänyt henkilö voi päivittää sen tietämättä aiemmin päivitettyjä tietoja.
Optimistinen samanaikaisuus
Edellä mainittu ongelma voidaan ratkaista suorittamalla tällä kertaa "optimistinen samanaikaisuuden hallinta". Selittää yksinkertaisesti, millainen hallinta on "voittaa ensin, jos yrität muokata tietoja samanaikaisesti". Ne, jotka yrittävät uusia myöhemmin, saavat virheen päivityksen ajankohtana eivätkä voi rekisteröityä.
Saatat ajatella, että et voi rekisteröidä uusia tietoja tähän, mutta tämä on vain "kun yrität muuttaa sitä samanaikaisesti". Kaksi ihmistä voi muokata täysin eri aikoina. Tietenkin siinä tapauksessa viimeksi päivitetyn henkilön tiedot ovat viimeisimmät.
Erityisesti, millainen käsittelyn ohjaus voidaan saavuttaa "tietojen versiolla". Esimerkiksi yllä olevassa esimerkissä sinulla on seuraavat tiedot.
Parametrin nimen | arvo |
---|---|
Kirjan nimi | Tietokannan kirjat |
hinta | 1000 |
versio | 1 |
Versiota kasvatetaan 1:llä jokaista tietuepäivitystä kohden. Esimerkiksi, jos herra / rouva A asettaa hinnaksi 1500 jeniä, versio on 2. Tuolloin päivityksen ehtona on, että päivitystä edeltävä versio on sama kuin tietokannassa oleva versio. Kun Mr./Ms. päivittää sen, tietokannan versio on 1 ja parhaillaan muokattavien alkuperäisten tietojen versio on 1, joten se voidaan päivittää.
Oletetaan tämän määrityksen perusteella tilanne, jossa herra / rouva A ja herra / rouva B muokkaavat samoja tietoja. Kun Mr./Ms. asetti hinnan ensimmäisen kerran 1500 jeniin ja yritti päivittää tietokantaa, versio oli sama, joten se voitiin päivittää. Tällöin tietokannan versio on 2. Herra / rouva B yrittää muokata 1000 jenin tietoja ja päivittää ne tietokantaan 1300 jeniksi. Päivitys epäonnistuu, koska käsillä oleva versio on 1, mutta tietokannan versio on jo 2. Näin optimistinen samanaikaisuus toimii.
Entity Framework Core sisältää tämän "optimistisen samanaikaisuuden" heti laatikosta, mikä tekee siitä suhteellisen helpon toteuttaa.
Muuten, "optimistinen samanaikaisuus" tunnetaan myös nimellä "optimistinen lukko" tai "optimistinen lukko", ja sitä tutkitaan ja siitä puhutaan joskus tällä nimellä. Se on lukitusmenetelmä, että tietoja ei voi päivittää, mutta tiedot voidaan lukea. On myös ohjaus, jota kutsutaan "pessimistiseksi lukoksi" toisena lukitusmenetelmänä kuin "optimistinen lukko". Tämä on menetelmä, joka lukitsee tietojen lataamisen, kun ensimmäinen henkilö lukee tiedot, eikä edes salli muokkaustoimintoja. Se voi ratkaista ongelman, että tietoja ei voida päivittää, vaikka niitä on muutettu, mutta kun joku muokkaa tietoja, muut ihmiset eivät voi avata tietojen muokkausnäyttöä, ja jos lukituksen avaaminen epäonnistuu, tiedot lukitaan ikuisesti. Molemmilla on etuja ja haittoja, joten riippuu toiminnasta, kumpi niistä hyväksytään.
Tietokannan luominen
Tässä artikkelissa selitän kuinka luoda tietokanta SQL Server ensin ja luo sitten koodi automaattisesti. Jos haluat toteuttaa sen koodi-ensin-tavalla, katso tällä kertaa automaattisesti luotu koodi ja toteuta se käänteisessä menettelyssä.
Tietokannan luominen
Voit tehdä sen myös SQL: ssä, mutta se on helpompi tehdä graafisella käyttöliittymällä, joten teen sen tällä kertaa graafisella käyttöliittymällä. Tietokannan nimeä lukuun ottamatta se luodaan oletusarvoisesti.
Taulukon luominen
Luo se seuraavalla SQL: llä:
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
Sinun ei tarvitse huolehtia useimmista parametreista, koska ne on tarkoitettu vain tietojen päivittämiseen.
Tällä kertaa kiinnostava parametri on RowVersion
sarake, joka kuvaa . Tämä on tietueiden versiointi.
Kun tyypiksi timestamp
määritetään, versio kasvaa automaattisesti aina, kun tietue päivitetään.
Koska tätä versiota hallitaan taulukkokohtaisesti, samasta versiosta ei periaatteessa ole tietuetta, ellet aseta sitä manuaalisesti.
Tietueen lisääminen
Voit lisätä sen seuraavalla SQL: llä:
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
Sarakkeita ei tarvitse määrittää, koska ne määritetään automaattisesti.
Luo projekti ja luo koodi automaattisesti
Tällä kertaa tarkistamme toiminnan konsolisovelluksella. Vaiheet projektin luomiseksi ja koodin automaattiseksi luomiseksi on kuvattu seuraavissa vinkeissä, joten en mene niihin tässä.
Luotu koodi on seuraava:
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!;
}
Optimistisen samanaikaisuuden hallinnan toiminnan tarkistaminen
Koska käytämme sitä tällä kertaa yhdessä sovelluksessa, emme käytä sitä tiukasti samanaikaisesti, mutta haluaisimme toteuttaa sen lähellä olevaa muotoa.
Kuten alussa olevassa esimerkissä, hankitaan kaksi tietoa, ja kun kukin päivitetään ensimmäisten tietojen perusteella, tarkista, saako jälkimmäinen päivitysohjelma virheen.
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);
}
}
Se on pitkä tarina, mutta suurin osa siitä on kirjoitettu konsolille.
Suorituksen tulos on seuraava.
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="}
処理を終了します。
Jaan sen osiin.
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("データベースコンテキストを作成しました。");
Luon kaksi tietokantakontekstia,
Jos jaat yhden tietokantakontekstin, se tallennetaan välimuistiin, kun tiedot luetaan, ja se on sama esiintymä.
Olettaen, että kutakin käytetään erikseen, luodaan kaksi tietokantakontekstia.
dbContextC
on tarkoitettu tietokannan arvojen tarkistamiseen. En oikeastaan tarvitse sitä, koska A tai B voidaan korvata.
// それぞれがデータを編集しようと読み込む
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))}");
Olettaen, että niitä käytetään erikseen, he lukevat yhden eri tietokantakonteksteista Book
.
Tässä vaiheessa emme ole muuttaneet mitään, joten ne ovat kaikki samanlaisia.
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="}
Ensin muutin ja päivitin Mr./Ms.:n kirjaa.
Päivitysprosessista tehdään UpdateToDatabase
yhteenveto menetelmässä, ja sanoma tulee näkyviin, kun poikkeus ilmenee.
// 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ämän seurauksena se on päivitetty onnistuneesti, ja Price
RowVersion
kirjaa A on päivitetty.
Jos luet DB-arvon suoraan, päivitetty arvo on sama kuin päivitetty arvo.
Pystyin päivittämään sen, koska sekä ARowVersion
: ssa että DB: ssä RowVersion
olivat AAAAAAAAH2k=
.
Versio on muuttunut AAAAAAAAH2o=
päivityksen vuoksi.
Kun olet päivittänyt A:n, päivitä B samalla tavalla.
// そのあと 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="}
Tulos on poikkeus DbUpdateConcurrencyException
ja päivitys epäonnistui.
bookB.RowVersion
AAAAAAAAH2k=
on , mutta RowVersion
koska se on jo AAAAAAAAH2o=
, se katsotaan yhteensopimattomaksi ja tapahtuu päivitysvirhe.
Näet, että Price
paikallinen muuttuja bookB
on muuttunut, mutta RowVersion
Voit nähdä, että tietokannan puolen arvot eivät ole muuttuneet lainkaan.
Yhteenveto
Tämä on useista lukitustyypeistä helpoin toteuttaa, koska kun käytät automaattisesti luotua koodia SQL Serverissä ja Entity Framework Coressa, optimistinen samanaikaisuus toteutetaan oletusarvoisesti. Koska tarkoituksena on kuitenkin vain estää "tietojen vioittuminen, jota päivitetään", on tarpeen käsitellä asianmukaisesti poikkeuksia, kun kyse on muista tiedoista tai käyttäjän toiminnoista.
Tällä kertaa en myöskään onnistunut mitään, koska toteutin RowVersion
sen yksinkertaisessa konsolisovelluksessa.
Jos haluat lisätä muokkausnäytön sen jälkeen, kun olet ladannut tiedot verkkosovellukseen tai asiakassovellukseen,
RowVersion
jollain tavalla, jotta se voidaan määrittää oikein, kun se päivitetään.
Entity Framework Coressa on kuitenkin muutosten seurantatoiminto, joten jos haluat asettaa vanhaksi RowVersion
arvoksi, joka luetaan DB: stä päivityksen yhteydessä,
bookB.RowVersion = <古い RowVersion>;
Vaikka se asetettaisiin seuraavasti, sitä ei arvioida oikein "optimistiseksi samanaikaisuuden hallinnaksi".
RowVersion
Vaikka asetat arvon normaaliksi, se tunnistetaan vain muuttuneeksi arvoksi, joten seuraava on
dbContextB.Entry(bookB).Property("RowVersion").OriginalValue = <古い RowVersion>;
Arvo on kirjoitettava uudelleen ennen muutosta.