EF Core’da Değişiklik İzleme

Her DbContext örneği, varlıklarda yapılan değişiklikleri izler. Bu izlenen varlıklar da SaveChanges çağrıldığında değişiklikleri veritabanına yönlendirir.

Bu belge Entity Framework Core (EF Core) ile değişiklik izlemenin yanı sıra sorgular ve güncelleştirmelerle ilişkisine genel bir bakış sunar.

Bahşiş

GitHub’dan örnek kodu indirerek bu belgedeki tüm kodları çalıştırabilir ve hataları ayıklayabilirsiniz.

Bahşiş

Kolaylık olması için, bu belgede SaveChangesAsync gibi zaman uyumsuz eşdeğerleri yerine SaveChanges gibi zaman uyumlu yöntemler kullanılır ve bunlara başvuru yapılır. Aksi belirtilmediği sürece çağrı ve bekleme örneklerinde zaman uyumsuz yöntemler de kullanılabilir.

Varlıkları izleme

Varlık örnekleri şu durumlarda izlenir:

  • Veritabanında yürütülen bir sorgu tarafından döndürüldüğünde
  • Add, Attach, Update veya benzer yöntemlerle DbContext ile açıkça iliştirildiğinde
  • Mevcut izlenen varlıklara bağlı yeni varlıklar olarak algılandığında

Varlık örnekleri şu durumlarda artık izlenmez:

  • DbContext atıldığında
  • Değişiklik izleyicisi temizlendi
  • Varlıklar açıkça ayrıldığında

DbContext, DbContext Başlatma ve Yapılandırma belgesinde açıklandığı gibi kısa süreli bir iş birimini temsil edecek şekilde tasarlanmıştır. Bu, DbContext’i atmanın, varlıkları izlemeyi durdurmanın normal yolu olduğu anlamına gelir. Başka bir deyişle DbContext’in ömrü şu şekilde olmalıdır:

  1. DbContext örneğini oluşturma
  2. Bazı varlıkları izleme
  3. Varlıklarda bazı değişiklikler yapma
  4. Veritabanını güncelleştirmek için SaveChanges çağrısı yapma
  5. DbContext örneğini atma

Bahşiş

Bu yaklaşım izlendiğinde değişiklik izleyicisini temizlemek veya varlık örneklerini açıkça ayırmak şart değildir. Ancak varlıkları ayırmanız gerekiyorsa ChangeTracker.Clear çağrısı yapmak varlıkları teker teker ayırmaktan daha verimlidir.

Varlık durumları

Her varlık belirli bir EntityState ile ilişkilendirilir:

  • Detached varlıkları DbContext tarafından izlenmiyor.
  • Added varlıkları yeni ve henüz veritabanına eklenmedi. Bu, SaveChanges çağrıldığında eklenecekleri anlamına gelir.
  • Unchanged varlıkları, veritabanından sorgulandıktan sonra değişmedi. Sorgulardan döndürülen tüm varlıklar başlangıçta bu durumdadır.
  • Modified varlıkları, veritabanından sorgulandıktan sonra değişti. Bu, SaveChanges çağrıldığında güncelleştirilecekleri anlamına gelir.
  • Deleted varlıkları veritabanında bulunuyor ancak SaveChanges çağrıldığında silinecek şekilde işaretlendi.

EF Core, değişiklikleri özellik düzeyinde izler. Örneğin yalnızca tek bir özellik değeri değiştirilirse veritabanı güncelleştirmesinde yalnızca bu değer değiştirilir. Ancak özellikler yalnızca varlığın kendisi Değiştirildi durumunda olduğunda değiştirilmiş olarak işaretlenebilir. (Alternatif olarak Değiştirildi durumu, en az bir özellik değerinin değiştirilmiş olarak işaretlendiği anlamına gelir.)

Aşağıdaki tabloda farklı durumlar özetlenmiştir:

Varlık durumu DbContext tarafından izleniyor Veritabanında var Özellikler değiştirildi SaveChanges eylemi
Detached No. - - -
Added Evet Hayır - Insert
Unchanged Evet Evet Hayır -
Modified Evet Evet Evet Güncelleştir
Deleted Evet Evet - Silme

Dekont

Bu metnin rahat anlaşılması için ilişkisel veritabanı terimlerini kullanılmıştır. NoSQL veritabanları genellikle benzer işlemleri destekler ancak büyük olasılıkla farklı adlar kullanılır. Daha fazla bilgi için veritabanı sağlayıcınızın belgelerine bakın.

Sorgulardan izleme

EF Core değişiklik izleme özelliği aynı DbContext örneği hem varlıkları sorgulamak hem de SaveChanges çağrısıyla onları güncelleştirmek için kullanıldığında en iyi performansı sunar. Bunun nedeni EF Core’un sorgulanan varlıkların durumunu otomatik olarak izlemesi ve SaveChanges çağrıldığında bu varlıklarda yapılan değişiklikleri algılamasıdır.

Bu yaklaşımın varlık örneklerini açıkça izlemeye kıyasla çeşitli avantajları vardır:

  • Basit bir işlemdir. Varlık durumlarının nadiren açıkça değiştirilmesi gerekir. Durum değişiklikleri EF Core tarafından yapılır.
  • Güncelleştirmeler yalnızca gerçekten değişmiş olan değerlerle sınırlıdır.
  • Gölge özelliklerin değerleri korunur ve gerektiğinde kullanılır. Bu özellikle yabancı anahtarlar gölge durumunda depolandığında geçerlidir.
  • Özelliklerin özgün değerleri otomatik olarak korunur ve verimli güncelleştirmeler için kullanılır.

Basit sorgu ve güncelleştirme

Örneğin, basit bir blog/gönderiler modelini düşünün:

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

Bu modeli blogları ve gönderileri sorgulamak ve ardından veritabanında bazı güncelleştirmeler yapmak için kullanabiliriz:

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

SaveChanges çağrısının neden olduğu veritabanı güncelleştirmeleri, aşağıdaki örnek SQLite veritabanında gösterilmiştir:

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

Değişiklik izleyicisi hata ayıklama görünümü, hangi varlıkların izlendiğini ve durumlarının ne olduğunu görselleştirmenin harika bir yoludur. Örneğin SaveChanges çağrısı yapmadan önce yukarıdaki örneğin içine aşağıdaki kodu ekleyin:

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

Aşağıdaki çıkışı oluşturur:

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}

Özellikle şu noktalara dikkat edin:

  • Blog.Name özelliği değiştirildi (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog') olarak işaretlenmiştir ve bu da blogun Modified durumunda olmasına neden olur.
  • 2 numaralı gönderinin Post.Title özelliği değiştirildi (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5') olarak işaretlenmiştir ve bu da gönderinin Modified durumunda olmasına neden olur.
  • 2 numaralı gönderinin diğer özellik değerleri değiştirilmediğinden değiştirildi olarak işaretlenmez. Bu nedenle bu değerler veritabanı güncelleştirmesine dahil edilmez.
  • Diğer gönderide hiçbir değişiklik yapılmamıştır. Bu nedenle hâlâ Unchanged durumundadır ve veritabanı güncelleştirmesine dahil edilmemiştir.

Sorgulama ve ardından ekleme, güncelleştirme ve silme

Yukarıdaki örnektekine benzer güncelleştirmeler, ekleme ve silme işlemleriyle aynı iş biriminde birleştirilebilir. Örnek:

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

Bu örnekte:

  • Blog ve ilgili gönderiler veritabanından sorgulandı ve izlendi
  • Blog.Name özelliği değiştirildi
  • Blog için mevcut gönderilerin koleksiyonuna yeni bir gönderi eklendi
  • Mevcut bir gönderi DbContext.Remove çağrısıyla silinmek üzere işaretlendi

SaveChanges çağrısından önce değişiklik izleyicisi hata ayıklama görünümüne yeniden bakın. EF Core’un bu değişiklikleri nasıl izlediğini görebilirsiniz:

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}

Şu noktalara dikkat edin:

  • Blog Modified olarak işaretlendi. Bu işlem bir veritabanı güncelleştirme işlemi oluşturur.
  • 2. gönderi Deleted olarak işaretlendi işaretlenir. Bu işlem bir veritabanı silme işlemi oluşturur.
  • Geçici kimliği olan yeni bir gönderi blog 1 ile ilişkilendirildi ve Added olarak işaretlendi. Bu işlem bir veritabanı ekleme işlemi oluşturur.

Tüm bunların sonucunda SaveChanges çağrıldığında aşağıdaki veritabanı komutları (SQLite kullanarak) çalıştırılır:

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

Varlıkları ekleme ve silme hakkında daha fazla bilgi için bkz. Varlıkları Açıkça İzleme. EF Core’un bu tür değişiklikleri otomatik olarak nasıl algıladığı hakkında daha fazla bilgi için bkz. Değişiklik Algılama ve Bildirimler.

Bahşiş

SaveChanges çağrısının veritabanında güncelleştirmeler yapmasına neden olacak değişikliklerin yapılıp yapılmadığını belirlemek için ChangeTracker.HasChanges() çağrısı yapın. “HasChanges” için “false” değeri döndürülürse SaveChanges işlem yapmayacak demektir.