Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Kiat
Anda dapat melihat contoh artikel ini di GitHub.
Dalam sebagian besar skenario, database digunakan secara bersamaan oleh beberapa instans aplikasi, masing-masing melakukan modifikasi pada data secara independen satu sama lain. Ketika data yang sama dimodifikasi pada saat yang sama, inkonsistensi dan kerusakan data dapat terjadi, misalnya ketika dua klien memodifikasi kolom yang berbeda dalam baris yang sama yang terkait dalam beberapa cara. Halaman ini membahas mekanisme untuk memastikan bahwa data Anda tetap konsisten dalam menghadapi perubahan bersamaan tersebut.
Konkurensi optimis
EF Core menerapkan konkurensi optimis, yang mengasumsikan bahwa konflik konkurensi relatif jarang terjadi. Berbeda dengan pendekatan pesimis - yang mengunci data di depan dan hanya kemudian melanjutkan untuk memodifikasinya - konkurensi optimis tidak mengambil kunci, tetapi mengatur agar modifikasi data gagal disimpan jika data telah berubah sejak dikueri. Kegagalan konsistensi ini dilaporkan ke aplikasi, yang menanganinya dengan tepat, mungkin dengan mencoba kembali seluruh operasi pada data baru.
Di EF Core, konkurensi optimis diimplementasikan dengan menyetel properti sebagai token konkurensi. Token konkurensi dimuat dan dilacak ketika entitas di-query - sama seperti properti lainnya. Kemudian, ketika operasi pembaruan atau penghapusan dilakukan selama SaveChanges(), nilai token konkurensi pada database dibandingkan dengan nilai asli yang dibaca oleh EF Core.
Untuk memahami cara kerjanya, mari kita asumsikan kita berada di SQL Server, dan menentukan jenis entitas Orang yang khas dengan properti khusus Version
:
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[Timestamp]
public byte[] Version { get; set; }
}
Di SQL Server, ini mengonfigurasi token konkurensi yang secara otomatis berubah dalam database setiap kali baris diubah (detail selengkapnya tersedia di bawah). Dengan konfigurasi ini, mari kita periksa apa yang terjadi dengan operasi pembaruan sederhana:
var person = await context.People.SingleAsync(b => b.FirstName == "John");
person.FirstName = "Paul";
await context.SaveChangesAsync();
- Pada langkah pertama, entitas Person dimuat dari basis data; ini mencakup token kesesuaian, yang kini dilacak secara normal oleh EF bersama dengan properti lainnya.
- Instans Person kemudian dimodifikasi dengan cara tertentu - kami mengubah properti
FirstName
. - Kami kemudian menginstruksikan EF Core untuk mempertahankan modifikasi. Karena token konkurensi dikonfigurasi, EF Core mengirimkan SQL berikut ke database:
UPDATE [People] SET [FirstName] = @p0
WHERE [PersonId] = @p1 AND [Version] = @p2;
Perhatikan bahwa selain PersonId
dalam klausa WHERE, EF Core juga telah menambahkan kondisi Version
; ini hanya memodifikasi baris jika Version
kolom tidak berubah sejak saat kami mengkuerinya.
Dalam kasus normal ("optimis"), tidak ada pembaruan bersamaan yang terjadi dan pembaruan berhasil diselesaikan, memodifikasi baris; database melaporkan ke EF Core bahwa satu baris terpengaruh oleh pembaruan, seperti yang diharapkan. Namun, jika terjadi pembaruan bersamaan, update tersebut gagal menemukan baris mana pun yang cocok dan melaporkan bahwa tidak ada baris yang terpengaruh. Akibatnya, EF Core SaveChanges() melemparkan DbUpdateConcurrencyException, dan aplikasi harus menangkap serta menanganinya dengan tepat. Teknik untuk melakukan ini dirinci di bawah, di bawah Menyelesaikan konflik keserentakan.
Sementara contoh di atas membahas pembaruan untuk entitas yang ada. EF juga melemparkan DbUpdateConcurrencyException saat mencoba menghapus baris yang telah dimodifikasi secara bersamaan. Namun, pengecualian ini umumnya tidak pernah dilemparkan saat menambahkan entitas; sementara database mungkin memang menimbulkan pelanggaran batasan unik jika baris dengan kunci yang sama sedang dimasukkan, ini menghasilkan pengecualian khusus penyedia yang dilemparkan, dan bukan DbUpdateConcurrencyException.
Token konkurensi asli yang dihasilkan database
Dalam kode di atas, kami menggunakan [Timestamp]
atribut untuk memetakan properti ke kolom SQL Server rowversion
. Karena rowversion
secara otomatis berubah ketika baris diperbarui, ini sangat berguna sebagai token konkurensi upaya minimum yang melindungi seluruh baris. Mengonfigurasi kolom SQL Server rowversion
sebagai token konkurensi dilakukan sebagai berikut:
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[Timestamp]
public byte[] Version { get; set; }
}
Jenis rowversion
yang ditunjukkan di atas adalah fitur khusus SQL Server; detail tentang mengonfigurasi token konkruensi yang memperbarui secara otomatis berbeda di setiap database, dan beberapa database tidak mendukungnya sama sekali, seperti SQLite. Lihat dokumentasi penyedia Anda untuk detail yang tepat.
Token konkurensi yang dikelola aplikasi
Daripada meminta database mengelola token konkurensi secara otomatis, Anda dapat mengelolanya dalam kode aplikasi. Ini memungkinkan penggunaan konkurensi optimis pada database - seperti SQLite - di mana tidak ada jenis pembaruan asli yang diperbarui secara otomatis. Tetapi bahkan di SQL Server, token konkurensi yang dikelola aplikasi dapat memberikan kontrol terperinci tentang perubahan kolom mana yang menyebabkan token diregenerasi. Misalnya, Anda mungkin memiliki properti yang berisi beberapa nilai yang di-cache atau tidak penting, dan tidak ingin perubahan pada properti tersebut memicu konflik konkurensi.
Berikut ini mengonfigurasi properti GUID untuk menjadi token konkurensi:
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
[ConcurrencyCheck]
public Guid Version { get; set; }
}
Karena properti ini tidak dihasilkan database, Anda harus menetapkannya dalam aplikasi setiap kali terus berubah:
var person = await context.People.SingleAsync(b => b.FirstName == "John");
person.FirstName = "Paul";
person.Version = Guid.NewGuid();
await context.SaveChangesAsync();
Jika Anda ingin nilai GUID baru selalu ditetapkan, Anda dapat melakukan ini melalui SaveChanges
interceptor. Namun, salah satu keuntungan mengelola token konkurensi secara manual adalah Anda dapat mengontrol dengan tepat ketika diregenerasi, untuk menghindari konflik konkurensi yang tidak perlu.
Mengatasi konflik konkurensi
Terlepas dari bagaimana token konkurensi Anda disiapkan, untuk menerapkan konkurensi optimis, aplikasi Anda harus menangani kasus dengan benar di mana konflik konkurensi terjadi dan DbUpdateConcurrencyException dilemparkan; ini disebut menyelesaikan konflik konkurensi.
Salah satu opsinya adalah hanya memberi tahu pengguna bahwa pembaruan gagal karena perubahan yang bertentangan; pengguna kemudian dapat memuat data baru dan mencoba lagi. Atau jika aplikasi Anda melakukan pembaruan otomatis, aplikasi hanya dapat mengulang dan mencoba kembali segera, setelah mengkueri ulang data.
Cara yang lebih canggih untuk mengatasi konflik konkurensi adalah dengan menggabungkan perubahan yang tertunda dengan nilai baru dalam database. Detail yang tepat dari nilai mana yang digabungkan tergantung pada aplikasi, dan prosesnya dapat diarahkan oleh antarmuka pengguna, di mana kedua set nilai ditampilkan.
Ada tiga set nilai yang tersedia untuk membantu mengatasi konflik konkurensi:
- Nilai saat ini adalah nilai yang coba ditulis aplikasi ke database.
- Nilai asli adalah nilai yang awalnya diambil dari database, sebelum pengeditan apa pun dibuat.
- Nilai database adalah nilai yang saat ini disimpan dalam database.
Pendekatan umum untuk menangani konflik konkurensi adalah:
- Tangkap
DbUpdateConcurrencyException
selamaSaveChanges
. - Gunakan
DbUpdateConcurrencyException.Entries
untuk menyiapkan serangkaian perubahan baru untuk entitas yang terpengaruh. - Refresh nilai asli token konkurensi untuk mencerminkan nilai saat ini dalam database.
- Coba lagi proses hingga tidak ada konflik yang terjadi.
Dalam contoh berikut, Person.FirstName
dan Person.LastName
disiapkan sebagai token konkurensi. Ada // TODO:
komentar di lokasi tempat Anda menyertakan logika khusus aplikasi untuk memilih nilai yang akan disimpan.
using var context = new PersonContext();
// Fetch a person from database and change phone number
var person = await context.People.SingleAsync(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";
// Change the person's name in the database to simulate a concurrency conflict
await context.Database.ExecuteSqlRawAsync(
"UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");
var saved = false;
while (!saved)
{
try
{
// Attempt to save changes to the database
await context.SaveChangesAsync();
saved = true;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is Person)
{
var proposedValues = entry.CurrentValues;
var databaseValues = await entry.GetDatabaseValuesAsync();
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
var databaseValue = databaseValues[property];
// TODO: decide which value should be written to database
// proposedValues[property] = <value to be saved>;
}
// Refresh original values to bypass next concurrency check
entry.OriginalValues.SetValues(databaseValues);
}
else
{
throw new NotSupportedException(
"Don't know how to handle concurrency conflicts for "
+ entry.Metadata.Name);
}
}
}
}
Menggunakan tingkat isolasi untuk kontrol konkurensi
Konkurensi optimis melalui token konkurensi bukanlah satu-satunya cara untuk memastikan bahwa data tetap konsisten dalam menghadapi perubahan bersamaan.
Salah satu mekanisme untuk memastikan konsistensi adalah tingkat isolasi transaksi bacaan yang dapat diulang. Di sebagian besar database, tingkat ini menjamin bahwa transaksi melihat data dalam database seperti saat transaksi dimulai, tanpa terpengaruh oleh aktivitas bersamaan berikutnya. Mengambil sampel dasar kami dari atas, ketika kami meminta Person
untuk memperbaruinya dalam beberapa cara, database harus memastikan tidak ada transaksi lain yang mengganggu baris database tersebut sampai transaksi selesai. Bergantung pada implementasi database Anda, ini terjadi dengan salah satu dari dua cara:
- Saat baris dikueri, transaksi Anda mengambil kunci bersama di atasnya. Setiap transaksi eksternal yang mencoba memperbarui baris akan diblokir hingga transaksi Anda selesai. Ini adalah bentuk penguncian pesimis dan diterapkan pada tingkat isolasi SQL Server yang disebut "repeatable read".
- Alih-alih melakukan penguncian, database memungkinkan transaksi eksternal untuk memperbarui baris, tetapi ketika transaksi Anda sendiri mencoba melakukan pembaruan, akan muncul kesalahan "serialisasi" yang menunjukkan bahwa terjadi konflik konkurensi. Ini adalah bentuk penguncian optimis - serupa dengan fitur token konkurensi EF - dan diimplementasikan oleh level isolasi snapshot SQL Server, serta oleh level isolasi baca berulang PostgreSQL.
Perhatikan bahwa tingkat isolasi "serializable" memberikan jaminan yang sama seperti repeatable read (dan menambahkan jaminan tambahan), sehingga berfungsi dengan cara yang sama sehubungan dengan yang disebutkan di atas.
Menggunakan tingkat isolasi yang lebih tinggi untuk mengelola konflik konkurensi lebih sederhana, tidak memerlukan token konkurensi, dan memberikan keuntungan lainnya; misalnya, pembacaan berulang menjamin bahwa transaksi Anda selalu melihat data yang sama di seluruh kueri di dalam transaksi, menghindari inkonsistensi. Namun, pendekatan ini memang memiliki kelemahannya.
Pertama, jika implementasi database Anda menggunakan penguncian untuk menerapkan tingkat isolasi, maka transaksi lain yang mencoba memodifikasi baris yang sama harus memblokir seluruh transaksi. Ini bisa berdampak buruk pada performa konkuren (jaga agar transaksi Anda tetap pendek!), meskipun perhatikan bahwa mekanisme Entity Framework melemparkan pengecualian dan memaksa Anda untuk melakukan percobaan ulang sebagai gantinya, yang juga berdampak. Ini berlaku untuk tingkat baca yang dapat diulang SQL Server, tetapi tidak ke tingkat rekam jepret, yang tidak mengunci baris yang dikueri.
Lebih penting lagi, pendekatan ini memerlukan transaksi untuk menjangkau semua operasi. Jika Anda, katakanlah, melakukan kueri Person
untuk menampilkan detailnya kepada pengguna, lalu menunggu pengguna membuat perubahan, maka transaksi harus berlangsung dalam waktu yang mungkin lama, yang sebaiknya dihindari dalam banyak kasus. Akibatnya, mekanisme ini biasanya sesuai ketika semua operasi yang terkandung segera dijalankan dan transaksi tidak bergantung pada input eksternal yang dapat meningkatkan durasinya.
Sumber Daya Tambahan:
Lihat Deteksi konflik di EF Core untuk sampel inti ASP.NET dengan deteksi konflik.