Bagikan melalui


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

ExecuteDeleteSeperti , 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

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/ExecuteUpdateExecuteDelete .

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

SaveChangesmenyediakan 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 dan ExecuteDelete.
  • 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 dan ExecuteDelete.