Kontrol Konkurensi Optimis Mencegah Kehilangan Data Karena Pembaruan Kemenangan Terakhir dengan Beberapa Akses (SQL Server)

Halaman Diperbarui :
Tanggal pembuatan halaman :

Lingkungan operasi

Visual Studio
  • Visual Studio 2022
.JARING
  • .NET 8
Inti Kerangka Kerja Entitas
  • Inti Kerangka Kerja Entitas 8.0
SQL Server
  • Server SQL 2022

* Di atas adalah lingkungan verifikasi, tetapi mungkin berfungsi dengan versi lain.

Tentang Memenangkan Pembaruan Setelah Tanpa Kontrol

Dalam aplikasi web atau aplikasi client-server, beberapa orang dapat mengakses dan memperbarui data dari satu database. Jika tidak ada yang dilakukan secara khusus, data orang yang memperbaruinya nanti akan tercermin dalam database sebagai yang terbaru.

Biasanya, tidak ada masalah khusus bahwa data orang yang memperbarui nanti tercermin sebagai yang terbaru. Masalah dapat muncul ketika banyak orang mencoba mengakses dan memperbarui data yang sama pada saat yang sama.

Misalnya, Anda memiliki data buku berikut.

Nilai Nama Parameter
Nama buku Buku Database
harga 1000

Jika dua orang membuka layar untuk mengedit data ini secara bersamaan, nilai di atas akan ditampilkan. Bapak/Ibu A mencoba menaikkan harga buku ini sebesar 500 yen. Bapak/Ibu B kemudian diperintahkan untuk menaikkan harga buku ini sebesar 300 yen lagi.

Jika mereka berdua menaikkan harga secara terpisah alih-alih pada saat yang sama, harga buku akan menjadi 1800 yen. Jika Anda mengaksesnya pada saat yang sama, itu akan didaftarkan masing-masing sebagai 1500 yen dan 1300 yen, jadi tidak akan menjadi 1800 yen tidak peduli mana yang mendaftar.

Masalahnya di sini adalah orang yang memperbarui nanti dapat memperbaruinya tanpa mengetahui informasi yang sebelumnya telah diperbarui.

Konkurensi optimis

Masalah yang disebutkan di atas dapat diselesaikan dengan melakukan "kontrol konkurensi optimis" kali ini. Untuk menjelaskan secara sederhana, kontrol seperti apa yang dimaksud dengan "menang terlebih dahulu jika Anda mencoba mengedit data pada saat yang sama". Mereka yang mencoba memperbarui nanti akan menerima kesalahan pada saat pembaruan dan tidak akan dapat mendaftar.

Anda mungkin berpikir bahwa Anda tidak dapat mendaftarkan data baru dengan ini, tetapi ini hanya "ketika Anda mencoba mengubahnya pada saat yang sama". Dimungkinkan bagi dua orang untuk menyunting pada waktu yang sama sekali berbeda. Tentu saja, dalam hal ini, data orang yang terakhir diperbarui akan menjadi yang terbaru.

Secara khusus, kontrol pemrosesan seperti apa yang dapat dicapai dengan "memiliki versi data". Misalnya, dalam contoh di atas, Anda akan memiliki data berikut.

Nilai Nama Parameter
Nama buku Buku Database
harga 1000
versi 1

Versi bertambah 1 untuk setiap pembaruan rekaman. Misalnya, jika Mr./Ms. A menetapkan harga menjadi 1500 yen, versinya akan menjadi 2. Pada saat itu, syarat pembaruan dapat dilakukan adalah versi sebelum pembaruan sama dengan versi pada database. Ketika Mr./Ms. memperbaruinya, versi pada database adalah 1, dan versi data asli yang saat ini sedang diedit adalah 1, sehingga dapat diperbarui.

Berdasarkan spesifikasi ini, asumsikan situasi di mana Mr./Ms. A dan Mr./Ms. B mengedit data yang sama. Ketika Mr./Ms. pertama kali menetapkan harga menjadi 1500 yen dan mencoba memperbarui database, versinya sama, sehingga bisa diperbarui. Dalam hal ini, versi pada database akan menjadi 2. Mr./Ms. B mencoba mengedit data 1000 yen dan memperbaruinya ke database sebagai 1300 yen. Pembaruan gagal karena versi yang ada adalah 1, tetapi versi pada database sudah 2. Beginilah cara kerja konkurensi optimistis.

Entity Framework Core menyertakan "konkurensi optimistis" ini di luar kotak, sehingga relatif mudah diterapkan.

Ngomong-ngomong, "konkurensi optimis" juga dikenal sebagai "kunci optimis" atau "kunci optimis", dan kadang-kadang diperiksa dan dibicarakan dengan nama ini. Ini adalah metode penguncian bahwa data tidak dapat diperbarui, tetapi data dapat dibaca. Ada juga kontrol yang disebut "kunci pesimis" sebagai metode penguncian lain selain "kunci optimis". Ini adalah metode yang mengunci pemuatan data saat orang pertama membaca data dan bahkan tidak mengizinkan operasi pengeditan. Ini dapat memecahkan masalah bahwa data tidak dapat diperbarui meskipun telah diubah, tetapi ketika seseorang sedang mengedit data, orang lain tidak dapat membuka layar pengeditan data, dan jika buka kunci gagal, data akan terkunci selamanya. Keduanya memiliki kelebihan dan kekurangan, sehingga tergantung pada operasi mana yang akan diadopsi.

Membuat database

Pada artikel ini, saya akan menjelaskan cara membuat database untuk SQL Server terlebih dahulu dan kemudian secara otomatis menghasilkan kode. Jika Anda ingin menerapkannya dengan cara yang mengutamakan kode, silakan lihat kode yang dibuat secara otomatis kali ini dan terapkan dalam prosedur terbalik.

Membuat database

Anda juga dapat membuatnya di SQL, tetapi lebih mudah membuatnya dengan GUI, jadi saya membuatnya dengan GUI kali ini. Kecuali untuk nama database, itu dibuat secara default.

Membuat tabel

Buat 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 khawatir tentang sebagian besar parameter karena hanya untuk pembaruan data. Parameter yang menarik kali ini adalah RowVersion kolom yang menjelaskan . Ini adalah versi rekaman. Dengan menentukan sebagai timestamp jenis, versi secara otomatis bertambah setiap kali rekaman diperbarui. Selain itu, karena versi ini dikelola berdasarkan per tabel, pada dasarnya tidak ada catatan versi yang sama kecuali Anda mengaturnya secara manual.

Menambahkan rekaman

Anda dapat menambahkannya 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 mengatur kolom karena diatur secara otomatis.

Buat proyek dan buat kode secara otomatis

Kali ini, kita akan memeriksa operasinya dengan aplikasi konsol. Langkah-langkah untuk membuat proyek dan menghasilkan kode secara otomatis dijelaskan dalam tips berikut, jadi saya tidak akan membahasnya di sini.

Kode yang dihasilkan adalah sebagai 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 pengoperasian kontrol konkurensi optimis

Karena kami menjalankannya dalam satu aplikasi kali ini, kami tidak secara ketat mengaksesnya pada saat yang sama, tetapi kami ingin menerapkannya dalam bentuk yang dekat dengannya.

Seperti pada contoh di awal, dua potong data diperoleh, dan ketika masing-masing diperbarui berdasarkan data pertama, periksa apakah pembaruan terakhir akan mendapatkan kesalahan.

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

Ini cerita yang panjang, tetapi sebagian besar ditulis ke konsol.

Hasil eksekusi adalah sebagai 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 memecahnya menjadi beberapa bagian.

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 membuat dua konteks database, Jika Anda berbagi satu konteks database, itu akan di-cache saat data dibaca dan itu akan menjadi instance yang sama. Dengan asumsi bahwa masing-masing diakses secara terpisah, dua konteks database dibuat. dbContextC adalah untuk memeriksa nilai dalam database. Saya tidak terlalu membutuhkannya karena A atau B bisa 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 asumsi bahwa mereka diakses secara terpisah, mereka membaca satu dari konteks Book database yang berbeda . Pada titik ini, kami belum mengubah apa pun, 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 mengubah dan memperbarui buku Mr./Ms.. Proses pembaruan dirangkum UpdateToDatabase dalam metode, dan pesan ditampilkan saat terjadi pengecualian.

// 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, telah berhasil diperbarui, dan Price RowVersion dan Buku A telah diperbarui. Selain itu, jika Anda membaca nilai DB secara langsung, nilai yang diperbarui akan sama dengan nilai yang diperbarui. Saya dapat memperbaruinya karena keduanya masuk ke RowVersion A dan RowVersion di DB adalah AAAAAAAAH2k= . Versi telah berubah AAAAAAAAH2o= karena pembaruan.

Setelah memperbarui A, perbarui 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 pembaruan telah gagal. bookB.RowVersion AAAAAAAAH2k= adalah , tetapi RowVersion karena sudah , AAAAAAAAH2o= itu dinilai sebagai ketidakcocokan dan terjadi kesalahan pembaruan. Anda dapat melihat bahwa Price variabel bookB lokal telah berubah, tetapi RowVersion Anda dapat melihat bahwa nilai di sisi database tidak berubah sama sekali.

Ringkasan

Ini adalah yang paling mudah dari beberapa jenis kunci untuk diterapkan, karena saat Anda menggunakan kode yang dibuat secara otomatis di SQL Server dan Entity Framework Core, konkurensi optimis diimplementasikan secara default. Namun, karena ini hanya untuk mencegah "kerusakan data yang akan diperbarui", perlu untuk menangani pengecualian dengan benar ketika data lain terlibat atau operasi pengguna terlibat.

Juga, kali ini saya tidak mengelola apa pun karena saya menerapkannya RowVersion dalam aplikasi konsol sederhana. Jika Anda ingin menyisipkan layar edit setelah memuat data di aplikasi web atau aplikasi klien, RowVersion dalam beberapa cara sehingga dapat ditentukan dengan benar saat diperbarui.

Namun, Entity Framework Core memiliki fungsi pelacakan perubahan, jadi jika Anda ingin mengatur yang lama RowVersion ke nilai yang dibaca dari DB pada saat pembaruan,

bookB.RowVersion = <古い RowVersion>;

Bahkan jika diatur sebagai berikut, itu tidak akan dinilai dengan benar sebagai "kontrol konkurensi optimis". RowVersion Bahkan jika Anda mengatur nilainya ke normal, itu hanya akan dikenali sebagai nilai yang diubah, jadi berikut ini adalah

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

Penting untuk menulis ulang nilai sebelum perubahan.