Le contrôle optimiste de la simultanéité empêche la perte de données due aux mises à jour Last-Win avec plusieurs accès (SQL Server)

Page mise à jour :
Date de création de la page :

Environnement d’exploitation

Studio visuel
  • Visual Studio 2022
.FILET
  • .NET 8
Noyau d’Entity Framework
  • Entity Framework Core 8.0
Serveur SQL
  • SQL Server 2022

* Ce qui précède est un environnement de vérification, mais il peut fonctionner avec d’autres versions.

À propos des mises à jour gagnantes après aucun contrôle

Dans une application Web ou une application client-serveur, plusieurs personnes peuvent accéder aux données d’une seule base de données et les mettre à jour. Si rien n’est fait en particulier, les données de la personne qui les a mises à jour ultérieurement seront reflétées dans la base de données au plus tard.

Normalement, il n’y a pas de problème particulier à ce que les données de la personne qui a mis à jour plus tard soient reflétées comme les plus récentes. Des problèmes peuvent survenir lorsque plusieurs personnes tentent d’accéder aux mêmes données et de les mettre à jour en même temps.

Par exemple, supposons que vous disposiez des données de livre suivantes.

Valeur du nom du paramètre
Nom du livre Livres de base de données
prix 1000

Si deux personnes ouvrent l’écran pour modifier ces données en même temps, la valeur ci-dessus s’affichera. M./Mme A essaie d’augmenter le prix de ce livre de 500 yens. M./Mme B est ensuite chargé d’augmenter le prix de ce livre de 300 yens supplémentaires.

Si les deux augmentent le prix séparément au lieu de la même chose, le prix du livre sera de 1800 yens. Si vous y accédez en même temps, il sera enregistré à 1500 yens et 1300 yens, respectivement, donc il ne sera pas à 1800 yens, peu importe lequel s’inscrit.

Le problème ici est que la personne qui a mis à jour plus tard peut le mettre à jour sans connaître les informations qui ont été précédemment mises à jour.

Simultanéité optimiste

Le problème susmentionné peut être résolu en effectuant cette fois un « contrôle de concurrence optimiste ». Pour expliquer simplement, quel type de contrôle est de « gagner en premier si vous essayez de modifier les données en même temps ». Ceux qui tentent de renouveler plus tard recevront une erreur au moment de la mise à jour et ne pourront pas s’inscrire.

Vous pensez peut-être que vous ne pouvez pas enregistrer de nouvelles données avec cela, mais ce n’est que « lorsque vous essayez de les modifier en même temps ». Il est possible pour deux personnes d’éditer à des moments complètement différents. Bien entendu, dans ce cas, les données de la dernière personne mise à jour seront les plus récentes.

Plus précisément, quel type de contrôle du traitement peut être réalisé en « disposant d’une version des données ». Par exemple, dans l’exemple ci-dessus, vous aurez les données suivantes.

Valeur du nom du paramètre
Nom du livre Livres de base de données
prix 1000
Version 1

La version est incrémentée de 1 pour chaque mise à jour de l’enregistrement. Par exemple, si M./Mme A fixe le prix à 1500 yens, la version sera 2. À ce moment-là, la condition pour que la mise à jour puisse être effectuée est que la version antérieure à la mise à jour soit la même que celle de la base de données. Lorsque M./Mme la met à jour, la version de la base de données est 1 et la version des données d’origine en cours de modification est 1, ce qui permet de la mettre à jour.

Sur la base de cette spécification, supposons une situation où M./Mme A et M./Mme B modifient les mêmes données. Lorsque M./Mme a fixé pour la première fois le prix à 1500 yens et a essayé de mettre à jour la base de données, la version était la même, elle a donc pu être mise à jour. Dans ce cas, la version de la base de données sera 2. M./Mme B essaie d’éditer les données de 1000 yens et de les mettre à jour dans la base de données à 1300 yens. La mise à jour échoue car la version en question est 1, mais la version de la base de données est déjà 2. C’est ainsi que fonctionne la concurrence optimiste.

Entity Framework Core inclut cette « concurrence optimiste » prête à l’emploi, ce qui la rend relativement facile à mettre en œuvre.

Soit dit en passant, la « concurrence optimiste » est également connue sous le nom de « verrou optimiste » ou « verrou optimiste », et elle est parfois examinée et discutée sous ce nom. Il s’agit d’une méthode de verrouillage qui fait que les données ne peuvent pas être mises à jour, mais qu’elles peuvent être lues. Il existe également un contrôle appelé « verrouillage pessimiste » comme une autre méthode de verrouillage autre que le « verrouillage optimiste ». Il s’agit d’une méthode qui verrouille le chargement des données lorsque la première personne lit les données et ne permet même pas les opérations d’édition. Il peut résoudre le problème selon lequel les données ne peuvent pas être mises à jour même si elles ont été modifiées, mais lorsque quelqu’un modifie les données, d’autres personnes ne peuvent pas ouvrir l’écran d’édition des données, et si le déverrouillage échoue, les données seront verrouillées pour toujours. Les deux ont des avantages et des inconvénients, cela dépend donc de l’opération à adopter.

Création d’une base de données

Dans cet article, je vais vous expliquer comment créer d’abord une base de données pour SQL Server, puis générer automatiquement du code. Si vous souhaitez l’implémenter d’une manière code-first, veuillez vous référer cette fois au code généré automatiquement et l’implémenter dans la procédure inverse.

Création d’une base de données

Vous pouvez également le faire en SQL, mais il est plus facile de le faire avec une interface graphique, donc je le fais avec une interface graphique cette fois. À l’exception du nom de la base de données, elle est créée par défaut.

Créer une table

Créez-le avec le code SQL suivant :

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

Vous n’avez pas à vous soucier de la plupart des paramètres, car ils ne servent qu’à la mise à jour des données. Le paramètre qui vous intéresse cette fois est RowVersion la colonne qui décrit . Il s’agit de la gestion des versions d’enregistrement. En spécifiant le timestamp type, la version est automatiquement incrémentée à chaque mise à jour de l’enregistrement. De plus, comme cette version est gérée par table, il n’y a pratiquement aucun enregistrement de la même version, à moins que vous ne la définissiez manuellement.

Ajouter un enregistrement

Vous pouvez l’ajouter avec le code SQL suivant :

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 Vous n’avez pas besoin de définir les colonnes, car elles sont définies automatiquement.

Créer un projet et générer automatiquement du code

Cette fois, nous allons vérifier le fonctionnement avec l’application console. Les étapes de création d’un projet et de génération automatique de code sont décrites dans les conseils suivants, je ne les aborderai donc pas ici.

Le code généré est le suivant :

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

Vérification du fonctionnement du contrôle de concurrence optimiste

Comme nous l’exécutons cette fois dans une seule application, nous n’y accédons pas strictement en même temps, mais nous aimerions l’implémenter sous une forme proche de celle-ci.

Comme dans l’exemple du début, deux données sont acquises, et lorsque chacune est mise à jour sur la base des premières données, vérifiez si le dernier programme de mise à jour recevra une erreur.

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

C’est une longue histoire, mais la plupart d’entre elles sont écrites sur la console.

Le résultat de l’exécution est le suivant.

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

処理を終了します。

Je vais le décomposer en sections.

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

Je crée deux contextes de base de données, Si vous partagez un contexte de base de données, il sera mis en cache lors de la lecture des données et il s’agira de la même instance. En supposant que chacun est accessible séparément, deux contextes de base de données sont créés. dbContextC permet de vérifier les valeurs dans la base de données. Je n’en ai pas vraiment besoin car A ou B peuvent être substitués.

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

En supposant qu’ils sont accessibles séparément, ils en lisent un à partir de contextes Book de base de données différents. À ce stade, nous n’avons rien changé, donc ils sont tous les mêmes.

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

Tout d’abord, j’ai modifié et mis à jour le livre de M./Mme. Le processus de mise à jour est UpdateToDatabase résumé dans une méthode et un message s’affiche lorsqu’une exception se produit.

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

En conséquence, il a été mis à jour avec succès, et Price RowVersion le livre A ont été mis à jour. De plus, si vous lisez directement la valeur DB, la valeur mise à jour sera la même que la valeur mise à jour. J’ai pu le mettre à jour parce que les deux sont entrés dans RowVersion A et RowVersion dans DB étaient AAAAAAAAH2k= . La version a changé AAAAAAAAH2o= en raison de la mise à jour.

Après la mise à jour A, mettez à jour B de la même manière.

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

Il s’agit d’une exception DbUpdateConcurrencyException et la mise à jour a échoué. bookB.RowVersion AAAAAAAAH2k= est , mais RowVersion comme est déjà AAAAAAAAH2o= , il est considéré comme une incompatibilité et une erreur de mise à jour se produit. Vous pouvez voir que la variable locale a changé, mais RowVersion que la Price variable bookB Vous pouvez voir que les valeurs du côté de la base de données n’ont pas du tout changé.

Résumé

Il s’agit du type de verrou le plus simple à implémenter, car lorsque vous utilisez le code généré automatiquement dans SQL Server et Entity Framework Core, la simultanéité optimiste est implémentée par défaut. Cependant, comme il ne s’agit que d’éviter « la corruption des données à mettre à jour », il est nécessaire de bien gérer les exceptions lorsque d’autres données sont impliquées ou que des opérations utilisateur sont impliquées.

De plus, cette fois-ci, je n’ai rien réussi car je l’ai implémenté RowVersion dans une simple application console. Si vous souhaitez insérer l’écran d’édition après avoir chargé les données dans une application web ou une application cliente, RowVersion d’une manière ou d’une autre afin qu’il puisse être correctement déterminé lors de sa mise à jour.

Toutefois, Entity Framework Core dispose d’une fonction de suivi des modifications, donc si vous souhaitez définir l’ancien RowVersion sur la valeur lue dans la base de données au moment de la mise à jour,

bookB.RowVersion = <古い RowVersion>;

Même s’il est défini comme suit, il ne sera pas correctement jugé comme un « contrôle de concurrence optimiste ». RowVersion Même si vous définissez la valeur sur normalement, elle ne sera reconnue que comme la valeur modifiée, donc ce qui suit est

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

Il est nécessaire de réécrire la valeur avant le changement.