O controle de simultaneidade otimista evita a perda de dados devido a atualizações de última vitória com vários acessos (SQL Server)

Página atualizada :
Data de criação de página :

Ambiente operacional

Visual Studio
  • Visual Studio 2022
.REDE
  • .NET 8
Núcleo do Entity Framework
  • Núcleo do Entity Framework 8.0
Servidor SQL
  • SQL Server 2022

* O acima é um ambiente de verificação, mas pode funcionar com outras versões.

Sobre as atualizações vencedoras após nenhum controle

Em um aplicativo Web ou em um aplicativo cliente-servidor, várias pessoas podem acessar e atualizar dados de um único banco de dados. Se nada for feito em particular, os dados da pessoa que os atualizou posteriormente serão refletidos no banco de dados como os mais recentes.

Normalmente, não há nenhum problema específico de que os dados da pessoa que atualizou posteriormente sejam refletidos como os mais recentes. Podem surgir problemas quando várias pessoas tentam acessar e atualizar os mesmos dados ao mesmo tempo.

Por exemplo, suponha que você tenha os seguintes dados de livro.

Nome do parâmetro Valor
Nome do livro Livros de banco de dados
preço 1000

Se duas pessoas abrirem a tela para editar esses dados ao mesmo tempo, o valor acima será exibido. O Sr./Sra. A está tentando aumentar o preço deste livro em 500 ienes. O Sr. / Sra. B é posteriormente instruído a aumentar o preço deste livro em mais 300 ienes.

Se os dois aumentarem o preço separadamente em vez de ao mesmo tempo, o preço do livro será de 1800 ienes. Se você acessá-lo ao mesmo tempo, ele será registrado como 1500 ienes e 1300 ienes, respectivamente, portanto, não será 1800 ienes, independentemente de qual deles for registrado.

O problema aqui é que a pessoa que atualizou posteriormente pode atualizá-lo sem saber as informações que foram atualizadas anteriormente.

Simultaneidade otimista

O problema mencionado acima pode ser resolvido executando o "controle de simultaneidade otimista" desta vez. Para explicar de forma simples, que tipo de controle é "ganhar primeiro se você tentar editar os dados ao mesmo tempo". Aqueles que tentarem renovar mais tarde receberão um erro no momento da atualização e não poderão se registrar.

Você pode pensar que não pode registrar novos dados com isso, mas isso é apenas "quando você tenta alterá-los ao mesmo tempo". É possível que duas pessoas editem em momentos completamente diferentes. Obviamente, nesse caso, os dados da última pessoa atualizada serão os mais recentes.

Especificamente, que tipo de controle de processamento pode ser alcançado "tendo uma versão dos dados". Por exemplo, no exemplo acima, você terá os seguintes dados.

Nome do parâmetro Valor
Nome do livro Livros de banco de dados
preço 1000
Versão 1

A versão é incrementada em 1 para cada atualização de registro. Por exemplo, se o Sr./Sra. A definir o preço para 1500 ienes, a versão será 2. Nesse momento, a condição para que a atualização possa ser feita é que a versão anterior à atualização seja a mesma que a versão no banco de dados. Quando o Sr./Sra. o atualiza, a versão no banco de dados é 1 e a versão dos dados originais que estão sendo editados no momento é 1, para que possam ser atualizados.

Com base nessa especificação, suponha uma situação em que o Sr./Sra. A e o Sr./Sra. B editam os mesmos dados. Quando o Sr./Sra. definiu o preço para 1500 ienes e tentou atualizar o banco de dados, a versão era a mesma, para que pudesse ser atualizada. Nesse caso, a versão no banco de dados será 2. O Sr./Sra. B tenta editar os dados de 1000 ienes e atualizá-los para o banco de dados como 1300 ienes. A atualização falha porque a versão em questão é 1, mas a versão no banco de dados já é 2. É assim que funciona a simultaneidade otimista.

O Entity Framework Core inclui essa "simultaneidade otimista" pronta para uso, tornando-a relativamente fácil de implementar.

A propósito, "simultaneidade otimista" também é conhecida como "bloqueio otimista" ou "bloqueio otimista" e às vezes é examinada e falada por esse nome. É um método de bloqueio que os dados não podem ser atualizados, mas os dados podem ser lidos. Há também um controle chamado "bloqueio pessimista" como outro método de bloqueio diferente do "bloqueio otimista". Este é um método que bloqueia o carregamento de dados quando a primeira pessoa lê os dados e nem mesmo permite operações de edição. Ele pode resolver o problema de que os dados não podem ser atualizados mesmo que tenham sido alterados, mas quando alguém está editando os dados, outras pessoas não podem abrir a tela de edição de dados e, se o desbloqueio falhar, os dados serão bloqueados para sempre. Ambos têm vantagens e desvantagens, por isso depende da operação qual adotar.

Criando um banco de dados

Neste artigo, explicarei como criar um banco de dados para o SQL Server primeiro e depois gerar código automaticamente. Se você quiser implementá-lo de maneira code-first, consulte o código gerado automaticamente desta vez e implemente-o no procedimento inverso.

Criando um banco de dados

Você também pode fazer isso em SQL, mas é mais fácil fazer isso com uma GUI, então estou fazendo isso com uma GUI desta vez. Exceto pelo nome do banco de dados, ele é criado por padrão.

Criar uma tabela

Crie-o com o seguinte 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

Você não precisa se preocupar com a maioria dos parâmetros porque eles são apenas para atualizações de dados. O parâmetro de interesse desta vez é RowVersion a coluna que descreve . Isso é controle de versão de registro. Ao especificar como o timestamp tipo, a versão é incrementada automaticamente sempre que o registro é atualizado. Além disso, como essa versão é gerenciada por tabela, basicamente não há registro da mesma versão, a menos que você a defina manualmente.

Adicionar um registro

Você pode adicioná-lo com o seguinte 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 Você não precisa definir as colunas porque elas são definidas automaticamente.

Criar um projeto e gerar código automaticamente

Desta vez, verificaremos a operação com o aplicativo do console. As etapas para criar um projeto e gerar código automaticamente são descritas nas dicas a seguir, portanto, não vou entrar nelas aqui.

O código gerado é o seguinte:

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

Verificando a operação do controle de simultaneidade otimista

Como estamos executando-o em um aplicativo desta vez, não o estamos acessando estritamente ao mesmo tempo, mas gostaríamos de implementá-lo de uma forma próxima a ele.

Como no exemplo no início, dois dados são adquiridos e, quando cada um é atualizado com base nos primeiros dados, verifique se o último atualizador receberá um erro.

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

É uma longa história, mas a maior parte dela é escrita no console.

O resultado da execução é o seguinte.

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

処理を終了します。

Vou dividi-lo em seções.

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

Estou criando dois contextos de banco de dados, Se você compartilhar um contexto de banco de dados, ele será armazenado em cache quando os dados forem lidos e será a mesma instância. Supondo que cada um seja acessado separadamente, dois contextos de banco de dados são criados. dbContextC é para verificar os valores no banco de dados. Eu realmente não preciso disso porque A ou B podem ser substituídos.

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

Supondo que eles sejam acessados separadamente, eles estão lendo um de diferentes contextos Book de banco de dados. Neste ponto, não mudamos nada, então eles são todos iguais.

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

Primeiro, mudei e atualizei o livro do Sr. / Sra. O processo de atualização é UpdateToDatabase resumido em um método e uma mensagem é exibida quando ocorre uma exceção.

// 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, ele foi atualizado com sucesso e Price RowVersion o Livro A foi atualizado. Além disso, se você ler o valor do banco de dados diretamente, o valor atualizado será o mesmo que o valor atualizado. Consegui atualizá-lo porque ambos entraram em RowVersion A e RowVersion em DB foram AAAAAAAAH2k= . A versão foi alterada AAAAAAAAH2o= devido à atualização.

Depois de atualizar A, atualize B da mesma maneira.

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

O resultado é uma exceção DbUpdateConcurrencyException e a atualização falhou. bookB.RowVersion AAAAAAAAH2k= é , mas RowVersion como já AAAAAAAAH2o= é , é considerado uma incompatibilidade e ocorre um erro de atualização. Você pode ver que a Price variável bookB local foi alterada, mas RowVersion o Você pode ver que os valores no lado do banco de dados não foram alterados.

Resumo

Esse é o mais fácil dos vários tipos de bloqueio de implementar, pois quando você usa o código gerado automaticamente no SQL Server e no Entity Framework Core, a simultaneidade otimista é implementada por padrão. No entanto, como é apenas para evitar "corrupção de dados a serem atualizados", é necessário lidar adequadamente com exceções quando outros dados estão envolvidos ou operações do usuário estão envolvidas.

Além disso, desta vez não consegui nada porque o implementei RowVersion em um aplicativo de console simples. Se você deseja inserir a tela de edição após carregar os dados em uma aplicação Web ou aplicação cliente, RowVersion de alguma forma para que possa ser determinado corretamente quando for atualizado.

No entanto, o Entity Framework Core tem uma função de controle de alterações, portanto, se você quiser definir o antigo RowVersion para o valor lido do banco de dados no momento da atualização,

bookB.RowVersion = <古い RowVersion>;

Mesmo que seja definido da seguinte forma, não será julgado corretamente como "controle de simultaneidade otimista". RowVersion Mesmo se você definir o valor como normalmente, ele só será reconhecido como o valor alterado, portanto, o seguinte é

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

É necessário reescrever o valor antes da alteração.