Kawalan serentak Optimis Menghalang Kehilangan Data Akibat Kemas Kini Kemenangan Terakhir dengan Berbilang Akses (SQL Server)

Laman dikemaskini :
Tarikh penciptaan halaman :

Persekitaran operasi

Visual Studio
  • Visual Studio 2022
.BERSIH
  • .NET 8
Teras Rangka Kerja Entiti
  • Teras Rangka Kerja Entiti 8.0
Pelayan SQL
  • SQL Server 2022

* Di atas ialah persekitaran pengesahan, tetapi ia mungkin berfungsi dengan versi lain.

Mengenai Kemas Kini Memenangi Selepas Tiada Kawalan

Dalam aplikasi web atau aplikasi pelayan klien, berbilang orang boleh mengakses dan mengemas kini data daripada pangkalan data tunggal. Jika tiada apa-apa yang dilakukan khususnya, data orang yang mengemas kininya kemudian akan ditunjukkan dalam pangkalan data sebagai yang terkini.

Biasanya, tidak ada masalah tertentu bahawa data orang yang dikemas kini kemudian dicerminkan sebagai yang terkini. Masalah boleh timbul apabila berbilang orang cuba mengakses dan mengemas kini data yang sama pada masa yang sama.

Sebagai contoh, katakan anda mempunyai data buku berikut.

Nilai Nama Parameter
Nama buku Buku Pangkalan Data
Harga 1000

Jika dua orang membuka skrin untuk mengedit data ini pada masa yang sama, nilai di atas akan dipaparkan. En./Cik A cuba menaikkan harga buku ini sebanyak 500 yen. En./Cik B kemudiannya diarahkan untuk menaikkan harga buku ini sebanyak 300 yen lagi.

Sekiranya mereka berdua menaikkan harga secara berasingan dan bukannya pada masa yang sama, harga buku itu akan menjadi 1800 yen. Jika anda mengaksesnya pada masa yang sama, ia akan didaftarkan sebagai 1500 yen dan 1300 yen, masing-masing, jadi ia tidak akan menjadi 1800 yen tidak kira yang mana satu mendaftar.

Masalahnya di sini ialah orang yang mengemas kini kemudian boleh mengemas kininya tanpa mengetahui maklumat yang telah dikemas kini sebelum ini.

Serentak optimistik

Masalah yang disebutkan di atas boleh diselesaikan dengan melakukan "kawalan serentak optimistik" kali ini. Untuk menerangkan secara ringkas, apakah jenis kawalan untuk "menang dahulu jika anda cuba mengedit data pada masa yang sama". Mereka yang cuba memperbaharui kemudian akan menerima ralat pada masa kemas kini dan tidak akan dapat mendaftar.

Anda mungkin berfikir bahawa anda tidak boleh mendaftarkan data baharu dengan ini, tetapi ini hanya "apabila anda cuba mengubahnya pada masa yang sama". Ada kemungkinan dua orang menyunting pada masa yang sama sekali berbeza. Sudah tentu, dalam kes itu, data orang yang terakhir dikemas kini akan menjadi yang terkini.

Khususnya, jenis kawalan pemprosesan yang boleh dicapai dengan "mempunyai versi data". Sebagai contoh, dalam contoh di atas, anda akan mempunyai data berikut.

Nilai Nama Parameter
Nama buku Buku Pangkalan Data
Harga 1000
Versi 1

Versi dinaikkan sebanyak 1 untuk setiap kemas kini rekod. Sebagai contoh, jika Encik / Cik A menetapkan harga kepada 1500 yen, versinya ialah 2. Pada masa itu, syarat bahawa kemas kini boleh dibuat ialah versi sebelum kemas kini adalah sama dengan versi pada pangkalan data. Apabila Encik / Cik mengemas kininya, versi pada pangkalan data ialah 1, dan versi data asal yang sedang disunting ialah 1, jadi ia boleh dikemas kini.

Berdasarkan spesifikasi ini, andaikan situasi di mana Encik / Cik A dan Encik / Cik B mengedit data yang sama. Apabila Encik / Cik mula-mula menetapkan harga kepada 1500 yen dan cuba mengemas kini pangkalan data, versinya sama, jadi ia boleh dikemas kini. Dalam kes itu, versi pada pangkalan data ialah 2. Encik / Cik B cuba mengedit data 1000 yen dan mengemas kininya ke pangkalan data sebagai 1300 yen. Kemas kini gagal kerana versi yang ada ialah 1, tetapi versi pada pangkalan data sudah 2. Beginilah cara serentak optimistik berfungsi.

Teras Rangka Kerja Entiti termasuk "serentak optimistik" ini di luar kotak, menjadikannya agak mudah untuk dilaksanakan.

Dengan cara ini, "serentak optimistik" juga dikenali sebagai "kunci optimistik" atau "kunci optimistik", dan kadangkala diperiksa dan dibincangkan dengan nama ini. Ia adalah kaedah penguncian bahawa data tidak boleh dikemas kini, tetapi data boleh dibaca. Terdapat juga kawalan yang dipanggil "kunci pesimis" sebagai kaedah penguncian lain selain daripada "kunci optimistik". Ini ialah kaedah yang mengunci pemuatan data apabila orang pertama membaca data dan tidak membenarkan operasi penyuntingan. Ia boleh menyelesaikan masalah bahawa data tidak boleh dikemas kini walaupun ia telah diubah, tetapi apabila seseorang mengedit data, orang lain tidak boleh membuka skrin penyuntingan data, dan jika buka kunci gagal, data akan dikunci selama-lamanya. Kedua-duanya mempunyai kelebihan dan kekurangan, jadi ia bergantung kepada operasi yang mana satu untuk diterima pakai.

Membuat pangkalan data

Dalam artikel ini, saya akan menerangkan cara membuat pangkalan data untuk SQL Server terlebih dahulu dan kemudian menjana kod secara automatik. Jika anda ingin melaksanakannya dengan cara yang mengutamakan kod, sila rujuk kod yang dijana secara automatik kali ini dan laksanakannya dalam prosedur terbalik.

Membuat pangkalan data

Anda juga boleh membuatnya dalam SQL, tetapi lebih mudah untuk membuatnya dengan GUI, jadi saya membuatnya dengan GUI kali ini. Kecuali untuk nama pangkalan data, ia dicipta secara lalai.

Buat jadual

Ciptanya dengan SQL berikut:

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

Anda tidak perlu risau tentang kebanyakan parameter kerana ia hanya untuk kemas kini data. Parameter yang menarik kali ini ialah RowVersion lajur yang menerangkan . Ini ialah versi rekod. Dengan menentukan sebagai timestamp jenis, versi dinaikkan secara automatik setiap kali rekod dikemas kini. Selain itu, memandangkan versi ini diuruskan berdasarkan setiap jadual, pada asasnya tidak ada rekod versi yang sama melainkan anda menetapkannya secara manual.

Tambah rekod

Anda boleh menambahnya dengan SQL berikut:

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 Anda tidak perlu menetapkan lajur kerana ia ditetapkan secara automatik.

Buat projek dan jana kod secara automatik

Kali ini, kami akan menyemak operasi dengan aplikasi konsol. Langkah-langkah untuk membuat projek dan menjana kod secara automatik diterangkan dalam petua berikut, jadi saya tidak akan membincangkannya di sini.

Kod yang dijana adalah seperti berikut:

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

Memeriksa operasi kawalan serentak optimistik

Memandangkan kami menjalankannya dalam satu aplikasi kali ini, kami tidak mengaksesnya dengan ketat pada masa yang sama, tetapi kami ingin melaksanakannya dalam bentuk yang hampir dengannya.

Seperti dalam contoh pada mulanya, dua keping data diperolehi, dan apabila setiap satu dikemas kini berdasarkan data pertama, semak sama ada pengemaskini yang terakhir akan mendapat ralat.

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

Ia adalah cerita yang panjang, tetapi kebanyakannya ditulis ke konsol.

Hasil pelaksanaan adalah seperti berikut.

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

処理を終了します。

Saya akan membahagikannya kepada beberapa bahagian.

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

Saya mencipta dua konteks pangkalan data, Jika anda berkongsi satu konteks pangkalan data, ia akan dicache apabila data dibaca dan ia akan menjadi contoh yang sama. Dengan mengandaikan bahawa masing-masing diakses secara berasingan, dua konteks pangkalan data dibuat. dbContextC adalah untuk menyemak nilai dalam pangkalan data. Saya tidak begitu memerlukannya kerana A atau B boleh diganti.

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

Dengan mengandaikan bahawa mereka diakses secara berasingan, mereka membaca satu daripada konteks Book pangkalan data yang berbeza . Pada ketika ini, kami tidak mengubah apa-apa, jadi semuanya sama.

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

Pertama, saya menukar dan mengemas kini buku Encik / Cik. Proses kemas kini diringkaskan UpdateToDatabase dalam kaedah dan mesej dipaparkan apabila pengecualian berlaku.

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

Akibatnya, ia telah berjaya dikemas kini, dan Price RowVersion dan Buku A telah dikemas kini. Selain itu, jika anda membaca nilai DB secara langsung, nilai yang dikemas kini akan sama dengan nilai yang dikemas kini. Saya dapat mengemas kininya kerana kedua-duanya masuk A RowVersion dan RowVersion dalam DB adalah AAAAAAAAH2k= . Versi telah berubah AAAAAAAAH2o= kerana kemas kini.

Selepas mengemas kini A, kemas kini B dengan cara yang sama.

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

Hasilnya adalah pengecualian DbUpdateConcurrencyException dan kemas kini telah gagal. bookB.RowVersion AAAAAAAAH2k= ialah , tetapi RowVersion kerana sudah , AAAAAAAAH2o= ia dinilai sebagai ketidakpadanan dan ralat kemas kini berlaku. Anda boleh melihat bahawa Price pembolehubah bookB tempatan telah berubah, tetapi RowVersion Anda boleh melihat bahawa nilai pada bahagian pangkalan data tidak berubah sama sekali.

Ringkasan

Ini adalah yang paling mudah daripada beberapa jenis kunci untuk dilaksanakan, kerana apabila anda menggunakan kod yang dijana secara automatik dalam SQL Server dan Entity Framework Core, serentak optimistik dilaksanakan secara lalai. Walau bagaimanapun, kerana ia hanya untuk mengelakkan "kerosakan data untuk dikemas kini", adalah perlu untuk mengendalikan pengecualian dengan betul apabila data lain terlibat atau operasi pengguna terlibat.

Selain itu, kali ini saya tidak menguruskan apa-apa kerana saya melaksanakannya RowVersion dalam aplikasi konsol yang mudah. Jika anda ingin memasukkan skrin edit selepas memuatkan data dalam aplikasi web atau aplikasi pelanggan, RowVersion dalam beberapa cara supaya ia boleh ditentukan dengan betul apabila ia dikemas kini.

Walau bagaimanapun, Teras Rangka Kerja Entiti mempunyai fungsi penjejakan perubahan, jadi jika anda ingin menetapkan yang lama RowVersion kepada nilai yang dibaca daripada DB pada masa kemas kini,

bookB.RowVersion = <古い RowVersion>;

Walaupun ia ditetapkan seperti berikut, ia tidak akan dinilai dengan betul sebagai "kawalan serentak optimistik". RowVersion Walaupun anda menetapkan nilai kepada biasa, ia hanya akan diiktiraf sebagai nilai yang diubah, jadi berikut ialah

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

Ia adalah perlu untuk menulis semula nilai sebelum perubahan.