Pelacakan Perubahan di EF Core

Setiap instans DbContext melacak perubahan yang dilakukan pada entitas. Entitas yang dilacak ini pada gilirannya mendorong perubahan ke database saat SaveChanges dipanggil.

Dokumen ini menyajikan gambaran umum pelacakan perubahan Entity Framework Core (EF Core) dan kaitannya dengan kueri dan pembaruan.

Tip

Anda dapat menjalankan dan men-debug ke semua kode dalam dokumen ini dengan mengunduh kode sampel dari GitHub.

Tip

Untuk kesederhanaan, dokumen ini menggunakan dan merujuk metode sinkron seperti SaveChanges daripada padanan asinkronnya seperti SaveChangesAsync. Memanggil dan menunggu metode asinkron dapat diganti kecuali dinyatakan lain.

Cara melacak entitas

Instans entitas dilacak ketika:

  • Dikembalikan dari kueri yang dijalankan terhadap database
  • Dilampirkan secara eksplisit ke DbContext dengan Add, Attach, Update, atau metode serupa
  • Terdeteksi sebagai entitas baru yang terhubung ke entitas terlacak yang ada

Instans entitas tidak lagi dilacak saat:

  • DbContext dibuang
  • Pelacak perubahan dibersihkan
  • Entitas secara eksplisit dilepas

DbContext dirancang untuk mewakili unit kerja yang berumur pendek, seperti yang dijelaskan dalam Inisialisasi dan Konfigurasi DbContext. Ini berarti bahwa membuang DbContext adalah cara normal untuk menghentikan entitas pelacakan. Dengan kata lain, masa pakai DbContext harus:

  1. Membuat instans DbContext
  2. Melacak beberapa entitas
  3. Membuat beberapa perubahan pada entitas
  4. Memanggil SaveChanges untuk memperbarui database
  5. Membuang instans DbContext

Tip

Tidak perlu menghapus pelacak perubahan atau secara eksplisit melepaskan instans entitas saat mengambil pendekatan ini. Namun, jika Anda perlu melepaskan entitas, maka panggilan ChangeTracker.Clear lebih efisien daripada melepaskan entitas satu per satu.

Status entitas

Setiap entitas dikaitkan dengan EntityState yang diberikan:

  • Entitas Detached tidak dilacak oleh DbContext.
  • Entitas Added adalah entitas baru dan belum disisipkan ke dalam database. Ini berarti mereka akan dimasukkan ketika SaveChanges dipanggil.
  • Entitas Unchangedbelum diubah sejak dikueri dari database. Semua entitas yang dikembalikan dari kueri awalnya dalam status ini.
  • Entitas Modified telah diubah sejak dikueri dari database. Ini berarti mereka akan diperbarui ketika SaveChanges dipanggil.
  • Entitas Deleted ada di database, tetapi ditandai untuk dihapus saat SaveChanges dipanggil.

EF Core melacak perubahan pada tingkat properti. Misalnya, jika hanya satu nilai properti yang dimodifikasi, pembaruan database hanya akan mengubah nilai tersebut. Namun, properti hanya dapat ditandai sebagai dimodifikasi ketika entitas itu sendiri dalam status Modified. (Atau, dari perspektif alternatif, status Modified berarti bahwa setidaknya satu nilai properti telah ditandai sebagai dimodifikasi.)

Tabel berikut merangkum berbagai status:

Status entitas Dilacak oleh DbContext Ada dalam database Properti dimodifikasi Tindakan pada SaveChanges
Detached Tidak - - -
Added Ya Tidak - Sisipkan
Unchanged Ya Ya Tidak -
Modified Ya Ya Ya Pembaruan
Deleted Ya Ya - Hapus

Catatan

Teks ini menggunakan istilah database relasional untuk kejelasan. Database NoSQL biasanya mendukung operasi serupa tetapi mungkin dengan nama yang berbeda. Lihat dokumentasi penyedia database Anda untuk informasi selengkapnya.

Pelacakan dari kueri

Pelacakan perubahan EF Core berfungsi paling baik ketika instans DbContext yang sama digunakan untuk kueri entitas dan memperbaruinya dengan memanggil SaveChanges. Ini karena EF Core secara otomatis melacak status entitas yang ditanyakan dan kemudian mendeteksi setiap perubahan yang dibuat pada entitas ini saat SaveChanges dipanggil.

Pendekatan ini memiliki beberapa dibandingkan dengan pelacakan entitas instance secara eksplisit:

  • Hal ini sederhana. Status entitas jarang perlu dimanipulasi secara eksplisit--EF Core menangani perubahan status.
  • Pembaruan terbatas hanya pada nilai-nilai yang benar-benar berubah.
  • Nilai properti bayangan dipertahankan dan digunakan sesuai kebutuhan. Ini sangat relevan ketika kunci asing disimpan dalam keadaan bayangan.
  • Nilai asli properti dipertahankan secara otomatis dan digunakan untuk pembaruan yang efisien.

Kueri dan pembaruan sederhana

Misalnya, pertimbangkan model blog/posting sederhana:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Kita dapat menggunakan model ini untuk mengkueri blog dan posting, lalu membuat beberapa pembaruan pada database:

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

blog.Name = ".NET Blog (Updated!)";

foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
    post.Title = post.Title.Replace("5", "5.0");
}

context.SaveChanges();

Memanggil SaveChanges menghasilkan pembaruan database berikut, menggunakan SQLite sebagai database contoh:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();

Tampilan debug pelacak perubahan adalah cara yang bagus untuk memvisualisasikan entitas mana yang sedang dilacak dan apa statusnya. Misalnya, menyisipkan kode berikut ke dalam sampel di atas sebelum memanggil SaveChanges:

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Menghasilkan output berikut:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
  Blog: {Id: 1}

Perhatikan secara khusus:

  • Properti Blog.Name ditandai sebagai dimodifikasi (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'), dan ini menyebabkan blog berada dalam status Modified.
  • Properti Post.Title postingan 2 ditandai sebagai dimodifikasi (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'), dan ini menghasilkan postingan ini berada dalam status Modified.
  • Nilai properti lain dari postingan 2 tidak berubah dan oleh karena itu tidak ditandai sebagai dimodifikasi. Inilah sebabnya mengapa nilai-nilai ini tidak disertakan dalam pembaruan database.
  • Postingan lainnya tidak dimodifikasi dengan cara apa pun. Inilah sebabnya mengapa masih dalam status Unchanged dan tidak disertakan dalam pembaruan database.

Kueri lalu sisipkan, perbarui, dan hapus

Pembaruan seperti pada contoh sebelumnya dapat digabungkan dengan sisipan dan penghapusan di unit kerja yang sama. Contohnya:

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Modify property values
blog.Name = ".NET Blog (Updated!)";

// Insert a new Post
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Dalam contoh ini:

  • Blog dan posting terkait dikueri dari database dan dilacak
  • Properti Blog.Name diubah
  • Postingan baru ditambahkan ke koleksi postingan yang ada untuk blog
  • Postingan yang sudah ada ditandai untuk dihapus dengan memanggil DbContext.Remove

Melihat kembali tampilan debug pelacak perubahan sebelum memanggil SaveChanges menunjukkan bagaimana EF Core melacak perubahan ini:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
  Id: -2147482638 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Perhatikan bahwa:

  • Blog ditandai sebagai Modified. Ini akan menghasilkan pembaruan database.
  • Posting 2 ditandai sebagai Deleted. Ini akan menghasilkan penghapusan database.
  • Postingan baru dengan ID sementara dikaitkan dengan blog 1 dan ditandai sebagai Added. Ini akan menghasilkan sisipan database.

Ini menghasilkan perintah database berikut (menggunakan SQLite) saat SaveChanges dipanggil:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Lihat Melacak Entitas secara eksplisit untuk informasi selengkapnya tentang menyisipkan dan menghapus entitas. Lihat Deteksi Perubahan dan Pemberitahuan untuk informasi selengkapnya tentang bagaimana EF Core secara otomatis mendeteksi perubahan seperti ini.

Tip

Panggil ChangeTracker.HasChanges() untuk menentukan apakah ada perubahan yang telah dilakukan yang akan menyebabkan SaveChanges membuat pembaruan pada database. Jika HasChanges menghasilkan false, maka SaveChanges akan menjadi no-op.