EF Core'da (Entity Framework Core) veri kaydetmenin temel yolu şudur: varlıkları Add, Remove ile işaretler ya da sorguyla gelen varlıkların özelliklerini değiştirirsiniz, ardından SaveChanges veya SaveChangesAsync çağırırsınız. EF Core bu noktada neyin değiştiğini kendisi bulur ve veritabanına gereken INSERT, UPDATE ve DELETE komutlarını gönderir. Bu yazı ekleme, güncelleme, silme ve ilişkili verinin kaydedilmesini resmî belgelere dayanarak adım adım anlatır.
Kaydetme işleminin arkasındaki motor change tracker'dır; varlık durumlarını, Entry() API'sini ve DebugView çıktısını ayrıntılı olarak EF Core change tracker ve EntityState yazısında ele aldık. Burada odak, gündelik CRUD kodudur: hangi metot ne zaman kullanılır, tek bir SaveChanges çağrısında neler olur, ilişkiler nasıl kurulur ve koparılır, hangi hatalar sık yapılır.
EF Core'da veri kaydetmenin iki yolu

Belgeler kaydetme için iki temel yaklaşım tanımlar. Birincisi değişiklik takibi ve SaveChanges: veriyi sorgularsınız, nesneler üzerinde değişiklik yaparsınız ve EF Core bunları bir iş birimi (unit of work) olarak veritabanına yazar. İkincisi ExecuteUpdate ve ExecuteDelete: hiçbir varlığı belleğe yüklemeden, LINQ ile tarif ettiğiniz satırlar üzerinde doğrudan tek bir SQL UPDATE veya DELETE çalıştırır. İkinci yolu ExecuteUpdate ve ExecuteDelete yazısında işliyoruz.
Belgelerin önerdiği kaba kural şöyledir: hangi değişikliklerin olacağını önceden bilmiyorsanız (örneğin kullanıcı bir formda istediği alanı değiştirebiliyorsa) ya da birbirine bağlı nesnelerden oluşan bir grafiği değiştiriyorsanız SaveChanges kullanın. Bir ölçüte uyan çok sayıda satırı toplu değiştirecekseniz veya tam olarak hangi satırın hangi kolonunun değişeceğini zaten biliyorsanız ExecuteUpdate/ExecuteDelete daha uygundur. Bunlar kesin kurallar değil, yön gösteren ilkelerdir.
| Özellik | SaveChanges | ExecuteUpdate / ExecuteDelete |
|---|---|---|
| Varlıkları yüklemek gerekir mi | Evet, izlenen varlıklar üzerinde çalışır | Hayır, doğrudan SQL üretir |
| Değişen kolonları kim bulur | EF Core otomatik bulur | Siz SetProperty ile belirtirsiniz |
| Ekleme (INSERT) | Destekler | Desteklemez |
| Eşzamanlılık denetimi | Concurrency token ile otomatik | Where koşuluyla elle |
| Birden çok işlemi tek seferde gönderme | Batching ile evet | Her çağrı ayrı gidiş dönüş |
| Varsayılan transaction | Destekleyen veritabanlarında evet | Kendiliğinden başlatmaz |
SaveChanges çağrıldığında ne olur?
Belgelerdeki temel güncelleme örneği üç adımdan oluşur. Önce sıradan bir LINQ sorgusu bir Blog getirir. EF Core sorguları varsayılan olarak izlemeli (tracking) çalışır, yani yüklenen varlık change tracker'a kaydedilir ve o anki değerlerinin bir anlık görüntüsü (snapshot) saklanır. İkinci adımda siz yalnızca bir .NET özelliğine değer atarsınız; bu adımda EF Core devrede değildir. Üçüncü adımda SaveChangesAsync çağrılır ve EF Core varlığın şimdiki değerlerini anlık görüntüyle karşılaştırarak neyin değiştiğini tespit eder, ardından ilişkisel bir veritabanında ilgili satır için bir UPDATE gönderir.
using (var context = new BloggingContext())
{
var blog = await context.Blogs.SingleAsync(b => b.Url == "http://example.com");
blog.Url = "http://example.com/blog";
await context.SaveChangesAsync();
}Bu modelin sağladığı kolaylıkları belgeler şöyle sıralar: hangi varlığın ve hangi özelliğin değiştiğini izleyen kodu sizin yazmanız gerekmez ve yalnızca değişen kolonlar güncellenir. Yeni bir blog ile ona ait yazıları birlikte eklediğinizde, veritabanının ürettiği blog anahtarını alıp yazılara aktarmak gibi sıralama işlerini EF Core üstlenir. Concurrency token tanımladıysanız araya giren değişiklikleri yakalar. Destekleyen veritabanlarında birden çok değişikliği tek bir transaction içine sarar ve çoğu durumda komutları gruplayarak (batching) veritabanına giden gidiş dönüş sayısını azaltır.
Veri ekleme: Add ve AddRange
Yeni bir satır eklemek için varlık sınıfınızdan bir nesne oluşturup DbSet<TEntity>.Add ile context'e verirsiniz. Nesne o anda Added durumunda izlenmeye başlar ve veri ancak SaveChanges çağrıldığında veritabanına yazılır. Anahtar veritabanında üretiliyorsa (tamsayı ve GUID anahtarlar için varsayılan budur), kayıttan sonra üretilen değer nesnenin anahtar özelliğine yazılır.
using (var context = new BloggingContext())
{
var blog = new Blog { Url = "http://example.com" };
context.Blogs.Add(blog);
await context.SaveChangesAsync();
}Birden çok nesne eklerken AddRange kullanabilirsiniz. Belgelere göre AddRange, UpdateRange, AttachRange ve RemoveRange yalnızca kolaylık için vardır; tek tek çağrılarla aynı işlevi görür ve aralarında kayda değer bir performans farkı yoktur. EF6'da Add her çağrıda DetectChanges çalıştırdığı için AddRange daha verimliydi; EF Core'da bu metotların hiçbiri DetectChanges çağırmaz, dolayısıyla o eski alışkanlığın burada karşılığı yoktur.
Sık sorulan bir nokta Add ile AddAsync arasındaki farktır. Add normalde veritabanına hiç gitmez, yalnızca izlemeyi başlatır. Bazı değer üreticileri anahtar üretmek için veritabanına erişebilir; EF Core ile gelen ve bunu yapan tek üretici HiLoValueGenerator'dır ve varsayılan olarak hiçbir zaman yapılandırılmaz. Bu yüzden belgeler uygulamaların büyük çoğunluğunun AddAsync değil Add kullanması gerektiğini söyler. Update, Attach ve Remove ise anahtar üretmedikleri için asenkron sürüme sahip değildir.
Güncelleme: izlenen varlığı değiştirmek yeterli

Bir varlık aynı context tarafından sorgulanmış ya da daha önce eklenip kaydedilmişse, onu güncellemek için özel bir metot çağırmanız gerekmez. Özelliğe yeni değeri atarsınız ve SaveChanges çağırırsınız. EF Core değişikliği özellik düzeyinde takip eder; bir varlığın on kolonundan yalnızca biri değiştiyse üretilen UPDATE yalnızca o kolonu içerir. Değişmeyen varlıklar ise Unchanged durumunda kalır ve komuta hiç girmez.
Burada yapılan en yaygın hata, zaten izlenen bir varlık için gereksiz yere context.Update(blog) çağırmaktır. Update bağlantısız (disconnected) senaryolar için tasarlanmıştır ve varlığın tüm özelliklerini değişmiş olarak işaretler; sonuçta yalnızca değişen kolon yerine bütün kolonlar güncellenir. Sorgudan gelen ve hâlâ aynı context'te duran bir nesnede buna gerek yoktur. Web uygulamasında olduğu gibi nesne başka bir context'ten geliyorsa doğru yaklaşım EF Core disconnected entity: Attach ve Update yazısında anlatılıyor.
Silme: Remove
Silmek istediğiniz varlığı DbSet<TEntity>.Remove veya DbContext.Remove ile işaretlersiniz; varlık Deleted durumuna geçer ve SaveChanges sırasında silinir. Kayıt tamamlandıktan sonra silinen varlık context'ten ayrılır (detach), çünkü artık veritabanında yoktur.
using (var context = new BloggingContext())
{
var blog = await context.Blogs.SingleAsync(b => b.Url == "http://example.com/blog");
context.Blogs.Remove(blog);
await context.SaveChangesAsync();
}Belgelerin altını çizdiği bir ayrıntı var: Remove çağırdığınız varlık henüz veritabanına kaydedilmemişse, yani Added durumunda izleniyorsa, silme komutu üretilmez. Varlık yalnızca context'ten çıkarılır ve SaveChanges sırasında artık eklenmez. Ayrıca izlenmeyen bir varlık için Remove çağırırsanız EF Core onu önce iliştirir (attach), sonra Deleted olarak işaretler. Bir ana (principal) varlığı sildiğinizde bağımlı satırlara ne olacağı ise ilişkinin zorunlu ya da isteğe bağlı oluşuna ve yapılandırılmış silme davranışına bağlıdır; bu konuyu EF Core cascade delete yazısında ayrıntılı tablolarla anlattık.
Tek SaveChanges içinde birden çok işlem
Ekleme, güncelleme ve silme işlemlerini aynı iş biriminde biriktirip tek bir SaveChanges çağrısıyla gönderebilirsiniz. Belgelerdeki örnek iki yeni blog ekler, ilk blogun adresini değiştirir ve sonuncuyu siler; hepsi tek çağrıda kaydedilir.
using (var context = new BloggingContext())
{
// add
context.Blogs.Add(new Blog { Url = "http://example.com/blog_one" });
context.Blogs.Add(new Blog { Url = "http://example.com/blog_two" });
// update
var firstBlog = await context.Blogs.FirstAsync();
firstBlog.Url = "";
// remove
var lastBlog = await context.Blogs.OrderBy(e => e.BlogId).LastAsync();
context.Blogs.Remove(lastBlog);
await context.SaveChangesAsync();
}Çoğu sağlayıcıda SaveChanges transaction içinde çalışır: işlemlerin hepsi başarılı olur ya da hiçbiri uygulanmaz, veritabanı yarım kalmış bir durumda bırakılmaz. Birden çok SaveChanges çağrısını ya da sorguları aynı transaction'a almak, savepoint ve harici transaction kullanmak gibi konular EF Core transaction yönetimi yazısının konusudur.
Performans tarafında belgeler EF Core'un değişiklikleri tek bir gidiş dönüşte gruplayarak gönderdiğini anlatır. Bir grupta kaç komut olacağı sağlayıcıya bağlıdır; SQL Server için yapılan ölçümlerde dörtten az komutta gruplamanın genelde daha az verimli olduğu, yaklaşık kırk komuttan sonra da faydasının azaldığı görülmüş, bu yüzden EF Core SQL Server'da varsayılan olarak bir grupta en fazla 42 komut çalıştırır, fazlasını ayrı gidiş dönüşlerde gönderir. MinBatchSize ve MaxBatchSize ile bu eşikler değiştirilebilir, ancak belgeler bunu ancak dikkatli ölçümden sonra yapmanızı önerir. Genel performans önerileri için EF Core performans optimizasyonu yazısına bakabilirsiniz.
İlişkili veriyi kaydetmek

Yeni bir varlık grafiğini eklemek
Birbirine bağlı birden çok yeni nesne oluşturduğunuzda yalnızca kökü eklemeniz yeterlidir. Aşağıdaki örnekte blog ve üç yazı birlikte eklenir; yazılar Blog.Posts navigation'ı üzerinden erişilebilir oldukları için EF Core onları da bulur ve Added olarak işaretler. Belgelerin ifadesiyle Add, Attach ve Update kendisine verilen grafiğin tamamı üzerinde çalışır.
using (var context = new BloggingContext())
{
var blog = new Blog
{
Url = "http://blogs.msdn.com/dotnet",
Posts = new List<Post>
{
new Post { Title = "Intro to C#" },
new Post { Title = "Intro to VB.NET" },
new Post { Title = "Intro to F#" }
}
};
context.Blogs.Add(blog);
await context.SaveChangesAsync();
}Bu sırada EF Core blogu önce ekler, veritabanının ürettiği anahtarı alır ve yazıların yabancı anahtar (foreign key) değerine yerleştirir. Sizin anahtar değerleriyle uğraşmanız gerekmez. Yalnızca tek bir varlığın durumunu değiştirmek istiyorsanız, grafik yerine context.Entry(blog).State = EntityState.Modified gibi düşük seviyeli bir atama kullanılabilir; bu atama bağlı nesneleri izlemeye almaz.
İzlenen bir varlığa yeni bir varlık bağlamak
Zaten izlenen bir varlığın navigation'ına yeni bir nesne eklerseniz, EF Core bir sonraki değişiklik tespitinde onu keşfeder ve ekler. Aşağıda blog yazılarıyla birlikte yüklenir, koleksiyona yeni bir yazı eklenir ve SaveChanges bu yazı için bir INSERT üretir. Yazıları yüklemek için kullanılan Include EF Core Include ve eager loading yazısında anlatılıyor.
using (var context = new BloggingContext())
{
var blog = await context.Blogs.Include(b => b.Posts).FirstAsync();
var post = new Post { Title = "Intro to EF Core" };
blog.Posts.Add(post);
await context.SaveChangesAsync();
}İlişkiyi değiştirmek ve relationship fixup
Bir navigation'ı başka bir varlığa yönlendirdiğinizde EF Core veritabanındaki yabancı anahtar kolonunu buna göre günceller. Örneğin mevcut bir yazının Blog navigation'ına yeni bir blog atarsanız, yeni blog eklenir ve yazının BlogId değeri yeni bloğu gösterecek şekilde güncellenir. EF Core navigation'ları ve yabancı anahtarları sürekli birbiriyle uyumlu tutar; buna relationship fixup denir. Bir yazıyı bir blogun koleksiyonundan çıkarıp diğerinin koleksiyonuna eklediğinizde, siz yazının Blog özelliğine dokunmasanız bile o da düzeltilir ve BlogId değişir.
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post); // eski koleksiyondan çıkarmak gerekmez
await context.SaveChangesAsync();Belgelere göre yeni bir koleksiyona eklenen bağımlı varlık başka bir ana varlığa bağlıysa o ilişki önce koparılır, sonra yeni ilişki kurulur. Bu yüzden yazıyı eski koleksiyondan çıkarmak zorunlu değildir. Belgelerin SQLite ile gösterdiği örnek çıktıda kayıt sırasında tek bir komut gider:
-- Belgelerdeki örnek çıktı (SQLite)
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();İlişkiyi kaldırmak
Bir referans navigation'ını null yapmak ya da bağımlı varlığı koleksiyondan çıkarmak ilişkiyi koparır. Sonrası ilişkinin türüne bağlıdır. Zorunlu ilişkilerde (yabancı anahtar null olamıyorsa) varsayılan davranış bağımlı varlığın silinmesidir; buna yetim kayıt silme (delete orphans) denir. İsteğe bağlı ilişkilerde ise varsayılan olarak bağımlı varlık silinmez, yabancı anahtarı null yapılır ve varlık Modified durumuna geçer. Aşağıdaki örnekte ilişki zorunlu olduğu için koleksiyondan çıkarılan yazı veritabanından silinir:
using (var context = new BloggingContext())
{
var blog = await context.Blogs.Include(b => b.Posts).FirstAsync();
var post = blog.Posts.First();
blog.Posts.Remove(post);
await context.SaveChangesAsync();
}İlişkinin zorunlu mu isteğe bağlı mı olduğu ve bire çok yapılandırma EF Core one-to-many ilişki yazısında, kavramların kendisi ise EF Core ilişkiler, navigation ve foreign key yazısında anlatılıyor.
Uygulamalı örnek: tek iş biriminde güncelle, ekle, sil
Belgelerdeki change tracking örneği bu yazıda anlatılanları tek senaryoda birleştirir: bir blog yazılarıyla birlikte yüklenir, blogun adı değiştirilir, koleksiyona yeni bir yazı eklenir ve mevcut bir yazı Remove ile silinmek üzere işaretlenir. Hepsi tek SaveChangesAsync çağrısıyla kaydedilir.
using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");
// Özellik değişikliği
blog.Name = ".NET Blog (Updated!)";
// Yeni yazı ekleme
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..."
});
// Mevcut yazıyı silinmek üzere işaretleme
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);
await context.SaveChangesAsync();Belgelerde SQLite ile gösterilen sonuçta üç komut gider: blog için yalnızca Name kolonunu içeren bir UPDATE, silinen yazı için bir DELETE ve yeni yazı için bir INSERT. Yeni yazının BlogId değeri relationship fixup sayesinde otomatik olarak 1 olur; yazının anahtarı ise kayıt öncesinde geçici (temporary) bir değerdir ve veritabanının ürettiği değerle değiştirilir. Belgelerdeki örnek çıktının özü şöyledir:
-- Belgelerdeki örnek çıktı (SQLite), parametre satırları kısaltıldı
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();Kaydetmeden önce neyin gönderileceğini görmek isterseniz context.ChangeTracker.HasChanges() ile bekleyen değişiklik olup olmadığını sorabilirsiniz; false dönüyorsa SaveChanges hiçbir şey yapmaz. Varlık durumlarının dökümünü almak için DebugView kullanılır; üretilen SQL'i görmek için ise EF Core SQL loglama, LogTo ve interceptor yazısındaki yöntemlere başvurabilirsiniz.
Sık yapılan hatalar
- İzlenen varlıkta
Updateçağırmak: gereksizdir ve yalnızca değişen kolon yerine tüm kolonların güncellenmesine yol açar. - Sorguyu
AsNoTrackingile yapıp sonra değiştirmek: izlenmeyen varlıktaki değişikliğiSaveChangesgörmez. İzlemeli ve izlemesiz sorgu farkı AsNoTracking ve tracking sorguları yazısında. - Yüzlerce satırı tek tek yükleyip silmek: belli bir ölçüte uyan toplu silme ve güncelleme için
ExecuteDelete/ExecuteUpdateçok daha verimlidir. - Ana varlığı bağımlıları yüklemeden silmek: veritabanında cascade tanımlı değilse yabancı anahtar ihlali alırsınız.
- Uzun ömürlü tek bir context ile çalışmak: context kısa ömürlü bir iş birimi için tasarlanmıştır; uzun süre yaşayan context aynı anahtara sahip ikinci bir nesneyi izlemeye çalışırken hata verir ve eski veriyi göstermeye devam eder. Yaşam döngüsü için DbContext yapılandırma ve yaşam döngüsü yazısına bakın.
- Eşzamanlı güncellemeleri hesaba katmamak: iki kullanıcı aynı satırı değiştirdiğinde sonuncusu öncekini sessizce ezer; bunu önlemek için EF Core concurrency ve rowversion yazısındaki concurrency token yaklaşımını kullanın.
Sık Sorulan Sorular
SaveChanges ile SaveChangesAsync arasında ne fark var?
İkisi de aynı değişiklikleri aynı şekilde kaydeder; fark, asenkron sürümün await ile beklenmesidir. Güncel belgelerdeki örnekler asenkron sürümü kullanır. Asenkron sürümü mutlaka await edin ve aynı context üzerinde bir işlem bitmeden ikincisini başlatmayın; ayrıntılar async yazısında.
Yalnızca değişen kolonlar mı güncellenir?
Sorguyla yüklenip aynı context'te değiştirilen varlıklar için evet; EF Core özellik düzeyinde takip yapar. Update ile iliştirilen bağlantısız varlıklarda ise tüm özellikler değişmiş sayılır.
Kayıttan sonra yeni satırın Id değerini nasıl alırım?
Anahtar veritabanında üretiliyorsa SaveChanges tamamlandığında üretilen değer nesnenin anahtar özelliğine yazılır; blog.BlogId gibi doğrudan okuyabilirsiniz. Kayıttan önce change tracker'da görülen negatif değerler geçicidir.
Bir yazıyı başka bloga taşımak için önce eski koleksiyondan çıkarmam gerekir mi?
Hayır. Yazıyı yeni blogun koleksiyonuna eklediğinizde EF Core eski ilişkiyi koparır, yabancı anahtarı ve eski koleksiyonu kendisi düzeltir. Aynı sonucu yazının Blog navigation'ını ya da BlogId değerini değiştirerek de alırsınız.
SaveChanges hata verirse veritabanı yarım mı kalır?
Transaction destekleyen sağlayıcılarda hayır; tek bir SaveChanges çağrısındaki değişiklikler ya tamamen uygulanır ya da hiç uygulanmaz.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: EF Core öğrenme rehberi · Change tracker ve EntityState · Disconnected entity · ExecuteUpdate ve ExecuteDelete · Cascade delete · Concurrency ve rowversion