ExecuteUpdate dan ExecuteDelete
Catatan
Fitur ini diperkenalkan dalam EF Core 7.0.
ExecuteUpdate dan ExecuteDelete merupakan cara untuk menyimpan data ke database tanpa menggunakan pelacakan dan SaveChanges() metode perubahan tradisional EF. Untuk perbandingan pengantar kedua teknik ini, lihat halaman Gambaran Umum tentang menyimpan data.
ExecuteDelete
Mari kita asumsikan bahwa Anda perlu menghapus semua Blog dengan peringkat di bawah ambang batas tertentu. Pendekatan tradisional SaveChanges() mengharuskan Anda untuk melakukan hal berikut:
await foreach (var blog in context.Blogs.Where(b => b.Rating < 3).AsAsyncEnumerable())
{
context.Blogs.Remove(blog);
}
await context.SaveChangesAsync();
Ini adalah cara yang cukup tidak efisien untuk melakukan tugas ini: kami mengkueri database untuk semua Blog yang cocok dengan filter kami, dan kemudian kami mengkueri, mewujudkan, dan melacak semua instans tersebut; jumlah entitas yang cocok bisa sangat besar. Kami kemudian memberi tahu pelacak perubahan EF bahwa setiap Blog perlu dihapus, dan menerapkan perubahan tersebut dengan memanggil SaveChanges(), yang menghasilkan DELETE
pernyataan untuk masing-masing dan setiap blog.
Berikut adalah tugas yang sama yang dilakukan melalui ExecuteDelete API:
await context.Blogs.Where(b => b.Rating < 3).ExecuteDeleteAsync();
Ini menggunakan operator LINQ yang akrab untuk menentukan Blog mana yang harus terpengaruh - seolah-olah kita mengkuerinya - dan kemudian memberi tahu EF untuk menjalankan SQL DELETE
terhadap database:
DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Selain lebih sederhana dan lebih pendek, ini dijalankan dengan sangat efisien dalam database, tanpa memuat data apa pun dari database atau melibatkan pelacak perubahan EF. Perhatikan bahwa Anda dapat menggunakan operator LINQ arbitrer untuk memilih Blog mana yang ingin Anda hapus - ini diterjemahkan ke SQL untuk eksekusi di database, sama seperti jika Anda mengkueri Blog tersebut.
ExecuteUpdate
Daripada menghapus Blog ini, bagaimana jika kita ingin mengubah properti untuk menunjukkan bahwa mereka harus disembunyikan sebagai gantinya? ExecuteUpdate menyediakan cara serupa untuk mengekspresikan pernyataan SQL UPDATE
:
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.IsVisible, false));
ExecuteDelete
Seperti , pertama-tama kami menggunakan LINQ untuk menentukan Blog mana yang harus terpengaruh; tetapi dengan ExecuteUpdate
kami juga perlu mengekspresikan perubahan yang akan diterapkan pada Blog yang cocok. Ini dilakukan dengan memanggil SetProperty
dalam ExecuteUpdate
panggilan, dan menyediakannya dengan dua argumen: properti yang akan diubah (IsVisible
), dan nilai baru yang harus dimiliki (false
). Hal ini menyebabkan SQL berikut dijalankan:
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Memperbarui beberapa properti
ExecuteUpdate
memungkinkan pembaruan beberapa properti dalam satu pemanggilan. Misalnya, untuk mengatur IsVisible
ke false dan untuk mengatur Rating
ke nol, cukup rantai panggilan tambahan SetProperty
bersama-sama:
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.IsVisible, false)
.SetProperty(b => b.Rating, 0));
Ini menjalankan SQL berikut:
UPDATE [b]
SET [b].[Rating] = 0,
[b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Mereferensikan nilai properti yang ada
Contoh di atas memperbarui properti ke nilai konstanta baru. ExecuteUpdate
juga memungkinkan referensi nilai properti yang ada saat menghitung nilai baru; misalnya, untuk meningkatkan peringkat semua Blog yang cocok satu per satu, gunakan yang berikut ini:
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
Perhatikan bahwa argumen kedua untuk SetProperty
sekarang adalah fungsi lambda, dan bukan konstanta seperti sebelumnya. Parameternya b
mewakili Blog yang diperbarui; dalam lambda tersebut, b.Rating
sehingga berisi peringkat sebelum perubahan terjadi. Ini menjalankan SQL berikut:
UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Navigasi dan entitas terkait
ExecuteUpdate
saat ini tidak mendukung navigasi referensi dalam SetProperty
lambda. Misalnya, kita ingin memperbarui semua peringkat Blog sehingga peringkat baru setiap Blog adalah rata-rata dari semua peringkat Postingannya. Kami dapat mencoba menggunakan ExecuteUpdate
sebagai berikut:
await context.Blogs.ExecuteUpdateAsync(
setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));
Namun, EF memungkinkan melakukan operasi ini dengan terlebih dahulu menggunakan Select
untuk menghitung peringkat rata-rata dan memproyeksikannya ke jenis anonim, lalu menggunakannya ExecuteUpdate
:
await context.Blogs
.Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));
Ini menjalankan SQL berikut:
UPDATE [b]
SET [b].[Rating] = CAST((
SELECT AVG(CAST([p].[Rating] AS float))
FROM [Post] AS [p]
WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]
Pelacakan perubahan
Pengguna yang terbiasa SaveChanges
dengan digunakan untuk melakukan beberapa perubahan, lalu memanggil SaveChanges
untuk menerapkan semua perubahan ini ke database; ini dimungkinkan oleh pelacak perubahan EF, yang mengumpulkan - atau melacak - perubahan ini.
ExecuteUpdate
dan ExecuteDelete
bekerja dengan sangat berbeda: mereka segera berlaku, pada titik di mana mereka dipanggil. Ini berarti bahwa meskipun satu ExecuteUpdate
atau ExecuteDelete
operasi dapat memengaruhi banyak baris, tidak mungkin untuk mengakumulasi beberapa operasi tersebut dan menerapkannya sekaligus, misalnya saat memanggil SaveChanges
. Bahkan, fungsinya sama sekali tidak menyadari pelacak perubahan EF, dan tidak memiliki interaksi dengannya apa pun. Ini memiliki beberapa konsekuensi penting.
Pertimbangkan gambar berikut:
// 1. Query the blog with the name `SomeBlog`. Since EF queries are tracking by default, the Blog is now tracked by EF's change tracker.
var blog = await context.Blogs.SingleAsync(b => b.Name == "SomeBlog");
// 2. Increase the rating of all blogs in the database by one. This executes immediately.
await context.Blogs.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
// 3. Increase the rating of `SomeBlog` by two. This modifies the .NET `Rating` property and is not yet persisted to the database.
blog.Rating += 2;
// 4. Persist tracked changes to the database.
await context.SaveChangesAsync();
Sangat penting, ketika ExecuteUpdate
dipanggil dan semua Blog diperbarui dalam database, pelacak perubahan EF tidak diperbarui, dan instans .NET yang dilacak masih memiliki nilai peringkat aslinya, dari titik di mana ia dikueri. Mari kita asumsikan bahwa peringkat Blog awalnya adalah 5; setelah baris ke-3 dijalankan, peringkat dalam database sekarang adalah 6 (karena ExecuteUpdate
), sedangkan peringkat dalam instans .NET yang dilacak adalah 7. Ketika SaveChanges
dipanggil, EF mendeteksi bahwa nilai baru 7 berbeda dari nilai asli 5, dan mempertahankan perubahan tersebut. Perubahan yang dilakukan oleh ExecuteUpdate
ditimpa dan tidak diperhitungkan.
Akibatnya, biasanya merupakan ide yang baik untuk menghindari pencampuran modifikasi terlacak SaveChanges
dan modifikasi yang tidak terlacak melalui/ExecuteUpdate
ExecuteDelete
.
Transaksi
Melanjutkan hal di atas, penting untuk memahami bahwa ExecuteUpdate
dan ExecuteDelete
tidak secara implisit memulai transaksi yang mereka panggil. Pertimbangkan gambar berikut:
await context.Blogs.ExecuteUpdateAsync(/* some update */);
await context.Blogs.ExecuteUpdateAsync(/* another update */);
var blog = await context.Blogs.SingleAsync(b => b.Name == "SomeBlog");
blog.Rating += 2;
await context.SaveChangesAsync();
Setiap ExecuteUpdate
panggilan menyebabkan satu SQL UPDATE
dikirim ke database. Karena tidak ada transaksi yang dibuat, jika ada kegagalan yang mencegah yang kedua ExecuteUpdate
berhasil diselesaikan, efek yang pertama masih bertahan ke database. Bahkan, empat operasi di atas - dua pemanggilan ExecuteUpdate
, kueri dan SaveChanges
- masing-masing dijalankan dalam transaksinya sendiri. Untuk membungkus beberapa operasi dalam satu transaksi, mulai transaksi secara eksplisit dengan DatabaseFacade:
using (var transaction = context.Database.BeginTransaction())
{
context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);
...
}
Untuk informasi selengkapnya tentang penanganan transaksi, lihat Menggunakan Transaksi.
Kontrol konkurensi dan baris yang terpengaruh
SaveChanges
menyediakan Kontrol Konkurensi otomatis, menggunakan token konkurensi untuk memastikan bahwa baris tidak diubah antara saat Anda memuatnya dan saat Anda menyimpan perubahan padanya. Karena ExecuteUpdate
dan ExecuteDelete
tidak berinteraksi dengan pelacak perubahan, mereka tidak dapat menerapkan kontrol konkurensi secara otomatis.
Namun, kedua metode ini mengembalikan jumlah baris yang terpengaruh oleh operasi; ini dapat sangat berguna untuk menerapkan kontrol konkurensi sendiri:
// (load the ID and concurrency token for a Blog in the database)
var numUpdated = await context.Blogs
.Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
.ExecuteUpdateAsync(/* ... */);
if (numUpdated == 0)
{
throw new Exception("Update failed!");
}
Dalam kode ini, kami menggunakan operator LINQ Where
untuk menerapkan pembaruan ke Blog tertentu, dan hanya jika token konkurensinya memiliki nilai tertentu (misalnya yang kami lihat saat mengkueri Blog dari database). Kami kemudian memeriksa berapa banyak baris yang benar-benar diperbarui oleh ExecuteUpdate
; jika hasilnya nol, tidak ada baris yang diperbarui dan token konkurensi kemungkinan diubah sebagai akibat dari pembaruan bersamaan.
Batasan
- Hanya memperbarui dan menghapus yang saat ini didukung; penyisipan harus dilakukan melalui DbSet<TEntity>.Add dan SaveChanges().
- Meskipun pernyataan SQL UPDATE dan DELETE memungkinkan pengambilan nilai kolom asli untuk baris yang terpengaruh, ini saat ini tidak didukung oleh
ExecuteUpdate
danExecuteDelete
. - Beberapa pemanggilan metode ini tidak dapat di-batch. Setiap pemanggilan melakukan perjalanan pulang perginya sendiri ke database.
- Database biasanya hanya mengizinkan satu tabel untuk dimodifikasi dengan UPDATE atau DELETE.
- Metode ini saat ini hanya berfungsi dengan penyedia database relasional.
Sumber Daya Tambahan:
- Sesi Standup Komunitas Akses Data .NET tempat kita berdiskusi
ExecuteUpdate
danExecuteDelete
.