El control de simultaneidad optimista evita la pérdida de datos debido a las últimas actualizaciones con varios accesos (SQL Server)

Actualización de la página :
Fecha de creación de la página :

Entorno operativo

Estudio visual
  • Visual Studio 2022
.RED
  • .NET 8
Núcleo de Entity Framework
  • Entity Framework Core 8.0
Servidor SQL
  • SQL Server 2022

* Lo anterior es un entorno de verificación, pero puede funcionar con otras versiones.

Acerca de ganar actualizaciones después de No Control

En una aplicación web o en una aplicación cliente-servidor, varias personas pueden acceder y actualizar datos de una sola base de datos. Si no se hace nada en particular, los datos de la persona que lo actualizó posteriormente se reflejarán en la base de datos como los más recientes.

Normalmente, no hay ningún problema particular de que los datos de la persona que actualizó posteriormente se reflejen como los últimos. Pueden surgir problemas cuando varias personas intentan acceder y actualizar los mismos datos al mismo tiempo.

Por ejemplo, supongamos que tiene los siguientes datos de libro.

Valor
Nombre del parámetro
Nombre del libro Libros de bases de datos
precio 1000

Si dos personas abren la pantalla para editar estos datos al mismo tiempo, se mostrará el valor anterior. El Sr./Sra. A está tratando de aumentar el precio de este libro en 500 yenes. Más tarde, se instruye al Sr./Sra. B para que aumente el precio de este libro en otros 300 yenes.

Si los dos suben el precio por separado en lugar de al mismo tiempo, el precio del libro será de 1800 yenes. Si accedes a él al mismo tiempo, se registrará como 1500 yenes y 1300 yenes, respectivamente, por lo que no será de 1800 yenes sin importar cuál se registre.

El problema aquí es que la persona que actualizó más tarde puede actualizarlo sin conocer la información que se actualizó anteriormente.

Simultaneidad optimista

El problema antes mencionado se puede resolver realizando un "control de concurrencia optimista" esta vez. Para explicarlo de forma sencilla, qué tipo de control es "ganar primero si intentas editar los datos al mismo tiempo". Aquellos que intenten renovar más tarde recibirán un error en el momento de la actualización y no podrán registrarse.

Puede pensar que no puede registrar nuevos datos con esto, pero esto es solo "cuando intenta cambiarlo al mismo tiempo". Es posible que dos personas editen en momentos completamente diferentes. Por supuesto, en ese caso, los datos de la última persona actualizada serán los últimos.

En concreto, qué tipo de control de tratamiento se puede conseguir "teniendo una versión de los datos". Por ejemplo, en el ejemplo anterior, tendrá los siguientes datos.

Valor
Nombre del parámetro
Nombre del libro Libros de bases de datos
precio 1000
Versión 1

La versión se incrementa en 1 para cada actualización de registro. Por ejemplo, si el Sr./Sra. A establece el precio en 1500 yenes, la versión será 2. En ese momento, la condición para que se pueda realizar la actualización es que la versión anterior a la actualización sea la misma que la versión de la base de datos. Cuando el Sr./Sra. lo actualiza, la versión de la base de datos es 1 y la versión de los datos originales que se están editando actualmente es 1, por lo que se puede actualizar.

Sobre la base de esta especificación, suponga una situación en la que el Sr./Sra. A y el Sr./Sra. B editan los mismos datos. Cuando el Sr./Sra. fijó por primera vez el precio en 1500 yenes e intentó actualizar la base de datos, la versión era la misma, por lo que se podía actualizar. En ese caso, la versión de la base de datos será 2. El Sr./Sra. B intenta editar los datos de 1000 yenes y actualizarlos a la base de datos como 1300 yenes. Se produce un error en la actualización porque la versión en cuestión es 1, pero la versión de la base de datos ya es 2. Así es como funciona la simultaneidad optimista.

Entity Framework Core incluye esta "simultaneidad optimista" lista para usar, lo que hace que sea relativamente fácil de implementar.

Por cierto, la "concurrencia optimista" también se conoce como "bloqueo optimista" o "bloqueo optimista", y a veces se examina y se habla de ella con este nombre. Es un método de bloqueo en el que los datos no se pueden actualizar, pero los datos se pueden leer. También existe un control llamado "bloqueo pesimista" como otro método de bloqueo distinto al "bloqueo optimista". Este es un método que bloquea la carga de datos cuando la primera persona lee los datos y ni siquiera permite operaciones de edición. Puede resolver el problema de que los datos no se pueden actualizar aunque se hayan cambiado, pero cuando alguien está editando los datos, otras personas no pueden abrir la pantalla de edición de datos y, si falla el desbloqueo, los datos se bloquearán para siempre. Ambos tienen ventajas y desventajas, por lo que depende de la operación cuál adoptar.

Creación de una base de datos

En este artículo, explicaré cómo crear primero una base de datos para SQL Server y luego generar código automáticamente. Si desea implementarlo de una manera que priorice el código, consulte el código generado automáticamente esta vez e impleméntelo en el procedimiento inverso.

Creación de una base de datos

También puedes hacerlo en SQL, pero es más fácil hacerlo con una GUI, así que esta vez lo estoy haciendo con una GUI. Excepto por el nombre de la base de datos, se crea de forma predeterminada.

Crear una tabla

Créelo con el siguiente 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 tiene que preocuparse por la mayoría de los parámetros porque son solo para actualizaciones de datos. El parámetro de interés esta vez es RowVersion la columna que describe . Se trata de un control de versiones de registros. Al especificar como tipo timestamp , la versión se incrementa automáticamente cada vez que se actualiza el registro. Además, dado que esta versión se administra por tabla, básicamente no hay registro de la misma versión a menos que la configure manualmente.

Agregar un registro

Puede agregarlo con el siguiente 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 es necesario establecer las columnas porque se establecen automáticamente.

Crear un proyecto y generar código automáticamente

En esta ocasión, comprobaremos el funcionamiento con la aplicación de consola. Los pasos para crear un proyecto y generar código automáticamente se describen en los siguientes consejos, por lo que no entraré en ellos aquí.

El código generado es el siguiente:

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

Comprobación del funcionamiento del control de simultaneidad optimista

Dado que esta vez lo estamos ejecutando en una aplicación, no estamos accediendo estrictamente a ella al mismo tiempo, pero nos gustaría implementarla de una forma cercana a ella.

Al igual que en el ejemplo del principio, se adquieren dos datos y, cuando cada uno se actualiza en función de los primeros datos, compruebe si el último actualizador recibirá 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);
  }
}

Es una larga historia, pero la mayor parte está escrita para la consola.

El resultado de la ejecución es el siguiente.

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

処理を終了します。

Lo dividiré en secciones.

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

Estoy creando dos contextos de base de datos, Si comparte un contexto de base de datos, se almacenará en caché cuando se lean los datos y será la misma instancia. Suponiendo que se accede a cada uno por separado, se crean dos contextos de base de datos. dbContextC es para comprobar los valores de la base de datos. Realmente no lo necesito porque A o B pueden ser sustituidos.

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

Suponiendo que se accede a ellos por separado, están leyendo uno de diferentes contextos Book de base de datos. En este punto, no hemos cambiado nada, por lo que todos son iguales.

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

Primero, cambié y actualicé el libro del Sr./Sra. El proceso de actualización se UpdateToDatabase resume en un método y se muestra un mensaje cuando se produce una excepción.

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

Como resultado, se ha actualizado con éxito y Price RowVersion , según el Libro A, se ha actualizado. Además, si lee el valor de la base de datos directamente, el valor actualizado será el mismo que el valor actualizado. Pude actualizarlo porque tanto se puso en RowVersion A como RowVersion en DB fueron AAAAAAAAH2k= . La versión ha cambiado AAAAAAAAH2o= debido a la actualización.

Después de actualizar A, actualice B de la misma 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 resultado es una excepción DbUpdateConcurrencyException y se ha producido un error en la actualización. bookB.RowVersion AAAAAAAAH2k= es , pero RowVersion dado que ya es , AAAAAAAAH2o= se juzga que es una discrepancia y se produce un error de actualización. Puede ver que la Price variable bookB local ha cambiado, pero RowVersion el Puede ver que los valores en el lado de la base de datos no han cambiado en absoluto.

Resumen

Este es el más fácil de implementar de los varios tipos de bloqueo, ya que cuando se usa el código generado automáticamente en SQL Server y Entity Framework Core, la simultaneidad optimista se implementa de forma predeterminada. Sin embargo, dado que es solo para evitar que "se actualicen los datos", es necesario manejar adecuadamente las excepciones cuando se trata de otros datos o de operaciones de usuario.

Además, esta vez no logré nada porque lo implementé RowVersion en una simple aplicación de consola. Si desea insertar la pantalla de edición después de cargar los datos en una aplicación web o una aplicación cliente, RowVersion de alguna manera para que se pueda determinar adecuadamente cuando se actualice.

Sin embargo, Entity Framework Core tiene una función de seguimiento de cambios, por lo que si desea establecer el antiguo RowVersion en el valor leído de la base de datos en el momento de la actualización,

bookB.RowVersion = <古い RowVersion>;

Incluso si se establece de la siguiente manera, no se juzgará correctamente como "control de concurrencia optimista". RowVersion Incluso si establece el valor en normalmente, solo se reconocerá como el valor modificado, por lo que lo siguiente es

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

Es necesario volver a escribir el valor antes del cambio.