El control de simultaneïtat optimista evita la pèrdua de dades a causa de les últimes actualitzacions guanyadores amb accessos múltiples (SQL Server)

Pàgina actualitzada :
Data de creació de la pàgina :

Entorn operatiu

Visual Studio
  • Visual Studio 2022
.XARXA
  • .NET 8
Nucli de l'Entity Framework
  • Entity Framework Core 8.0
Servidor SQL
  • SQL Server 2022

* L'anterior és un entorn de verificació, però pot funcionar amb altres versions.

Sobre les actualitzacions guanyadores després de no tenir control

En una aplicació web o una aplicació client-servidor, diverses persones poden accedir i actualitzar dades d'una sola base de dades. Si no es fa res en particular, les dades de la persona que l'ha actualitzat posteriorment es reflectiran a la base de dades com a última.

Normalment, no hi ha cap problema particular que les dades de la persona que va actualitzar més tard es reflecteixin com les últimes. Els problemes poden sorgir quan diverses persones intenten accedir i actualitzar les mateixes dades alhora.

Per exemple, suposem que teniu les dades de llibre següents.

Valor del nom del paràmetre
Nom del llibre Llibres de bases de dades
preu 1000

Si dues persones obren la pantalla per editar aquestes dades alhora, es mostrarà el valor anterior. El Sr. / Sra. A està tractant d'augmentar el preu d'aquest llibre en 500 iens. Més tard, el Sr. / Sra. B rep instruccions d'augmentar el preu d'aquest llibre en altres 300 iens.

Si els dos apugen el preu per separat en lloc de al mateix temps, el preu del llibre serà de 1800 iens. Si hi accedeixes al mateix temps, es registrarà com a 1500 iens i 1300 iens, respectivament, per la qual cosa no seran 1800 iens independentment de quin es registri.

El problema aquí és que la persona que ha actualitzat més tard pot actualitzar-lo sense conèixer la informació que s'ha actualitzat anteriorment.

Concurrència optimista

El problema esmentat es pot resoldre realitzant un "control de concurrència optimista" aquesta vegada. Per explicar simplement, quin tipus de control és "guanyar primer si intentes editar les dades al mateix temps". Els que intentin renovar més tard rebran un error en el moment de l'actualització i no podran registrar-se.

Potser penseu que no podeu registrar noves dades amb això, però això només és "quan intenteu canviar-les al mateix temps". És possible que dues persones editin en moments completament diferents. Per descomptat, en aquest cas, les dades de l'última persona actualitzada seran les últimes.

Concretament, quin tipus de control de processament es pot aconseguir "tenint una versió de les dades". Per exemple, a l'exemple anterior, tindreu les dades següents.

Valor del nom del paràmetre
Nom del llibre Llibres de bases de dades
preu 1000
versió 1

La versió s'incrementa en 1 per a cada actualització de registre. Per exemple, si el Sr./Sra. A estableix el preu a 1500 iens, la versió serà 2. En aquest moment, la condició que es pot fer l'actualització és que la versió anterior a l'actualització sigui la mateixa que la versió de la base de dades. Quan el Sr./Sra. l'actualitza, la versió de la base de dades és 1, i la versió de les dades originals que s'estan editant és 1, de manera que es pot actualitzar.

Basant-se en aquesta especificació, suposem una situació en què el Sr./Sra. A i el Sr./Sra. B editen les mateixes dades. Quan el Sr. / Sra. va establir el preu per primera vegada a 1500 iens i va intentar actualitzar la base de dades, la versió era la mateixa, de manera que es podia actualitzar. En aquest cas, la versió de la base de dades serà 2. El Sr./Sra. B intenta editar les dades de 1000 iens i actualitzar-les a la base de dades com a 1300 iens. L'actualització falla perquè la versió en qüestió és 1, però la versió de la base de dades ja és 2. Així és com funciona la concurrència optimista.

Entity Framework Core inclou aquesta "concurrència optimista" fora de la caixa, cosa que fa que sigui relativament fàcil d'implementar.

Per cert, la "concurrència optimista" també es coneix com a "bloqueig optimista" o "bloqueig optimista", i de vegades s'examina i es parla amb aquest nom. És un mètode de bloqueig que les dades no es poden actualitzar, però les dades es poden llegir. També hi ha un control anomenat "bloqueig pessimista" com un altre mètode de bloqueig diferent del "bloqueig optimista". Aquest és un mètode que bloqueja la càrrega de dades quan la primera persona llegeix les dades i ni tan sols permet operacions d'edició. Pot resoldre el problema que les dades no es poden actualitzar encara que s'hagin canviat, però quan algú està editant les dades, altres persones no poden obrir la pantalla d'edició de dades i, si el desbloqueig falla, les dades es bloquejaran per sempre. Tots dos tenen avantatges i desavantatges, de manera que depèn de l'operació quin adoptar.

Creació d'una base de dades

En aquest article, explicaré com crear una base de dades per a SQL Server primer i després generar codi automàticament. Si voleu implementar-lo d'una manera de codi primer, consulteu el codi generat automàticament aquesta vegada i implementeu-lo en el procediment invers.

Creació d'una base de dades

També podeu fer-ho en SQL, però és més fàcil fer-ho amb una interfície gràfica, així que aquesta vegada ho faré amb una interfície gràfica. Excepte el nom de la base de dades, es crea per defecte.

Crear una taula

Creeu-lo amb el següent 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

No us haureu de preocupar per la majoria dels paràmetres perquè només són per a actualitzacions de dades. El paràmetre d'interès aquesta vegada és RowVersion la columna que descriu . Això és el control de versions de registres. En especificar com a tipus, la versió s'incrementa timestamp automàticament cada vegada que s'actualitza el registre. A més, com que aquesta versió es gestiona per taula, bàsicament no hi ha cap registre de la mateixa versió tret que la configureu manualment.

Afegir un registre

Podeu afegir-lo amb el següent 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 No cal que definiu les columnes perquè es defineixen automàticament.

Crea un projecte i genera codi automàticament

Aquesta vegada, comprovarem el funcionament amb l'aplicació de consola. Els passos per crear un projecte i generar codi automàticament es descriuen en els següents consells, així que no els entraré aquí.

El codi generat és el següent:

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

Comprovació del funcionament del control de concurrència optimista

Com que aquesta vegada l'estem executant en una aplicació, no hi accedim estrictament al mateix temps, però ens agradaria implementar-la d'una forma propera.

Com en l'exemple del principi, s'adquireixen dues dades i, quan cadascuna s'actualitza en funció de les primeres dades, comproveu si aquest últim actualitzador rebrà un error.

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

És una llarga història, però la major part està escrita a la consola.

El resultat de l'execució és el següent.

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

処理を終了します。

Ho dividiré en seccions.

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

Estic creant dos contextos de bases de dades, Si compartiu un context de base de dades, es posarà a la memòria cau quan es llegeixin les dades i serà la mateixa instància. Suposant que s'accedeix a cadascun per separat, es creen dos contextos de base de dades. dbContextC és per comprovar els valors de la base de dades. Realment no ho necessito perquè A o B es poden substituir.

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

Suposant que s'accedeix per separat, en llegeixen un de diferents contextos Book de bases de dades. En aquest moment, no hem canviat res, així que tots són iguals.

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

Primer, vaig canviar i actualitzar el llibre del Sr. / Ms. El procés d'actualització es UpdateToDatabase resumeix en un mètode i es mostra un missatge quan es produeix una excepció.

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

Com a resultat, s'ha actualitzat amb èxit i Price RowVersion s'ha actualitzat el Llibre A. A més, si llegiu el valor de la base de dades directament, el valor actualitzat serà el mateix que el valor actualitzat. Vaig poder actualitzar-lo perquè tant es va posar en RowVersion A com RowVersion en DB eren AAAAAAAAH2k= . La versió ha canviat AAAAAAAAH2o= a causa de l'actualització.

Després d'actualitzar A, actualitzeu B de la mateixa manera.

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

El resultat és una excepció DbUpdateConcurrencyException i l'actualització ha fallat. bookB.RowVersion AAAAAAAAH2k= és , però RowVersion com que ja AAAAAAAAH2o= és , es considera que és un desajust i es produeix un error d'actualització. Podeu veure que la Price variable local ha canviat, però RowVersion la variable bookB Podeu veure que els valors del costat de la base de dades no han canviat en absolut.

Resum

Aquest és el més fàcil d'implementar dels diversos tipus de bloqueig, ja que quan utilitzeu el codi generat automàticament a l'SQL Server i a l'Entity Framework Core, la simultaneïtat optimista s'implementa per defecte. No obstant això, com que només es tracta d'evitar que s'actualitzi la "corrupció de dades", és necessari gestionar adequadament les excepcions quan hi ha altres dades o operacions d'usuari.

A més, aquesta vegada no vaig gestionar res perquè ho vaig implementar RowVersion en una senzilla aplicació de consola. Si voleu inserir la pantalla d'edició després de carregar les dades en una aplicació web o aplicació client, RowVersion d'alguna manera perquè es pugui determinar correctament quan s'actualitza.

Tanmateix, Entity Framework Core té una funció de seguiment de canvis, de manera que si voleu definir l'antic RowVersion al valor llegit de la base de dades en el moment de l'actualització,

bookB.RowVersion = <古い RowVersion>;

Fins i tot si s'estableix de la següent manera, no es jutjarà correctament com a "control de concurrència optimista". RowVersion Fins i tot si establiu el valor a normalment, només es reconeixerà com el valor canviat, de manera que el següent és

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

Cal reescriure el valor abans del canvi.