EF Core'da toplu ekleme, güncelleme ve silme: SaveChanges batch'lemeden SqlBulkCopy'ye
EF Core'un batch'lenen SaveChanges'ı EF6 dönemi yaklaşımdan nasıl ayrılır; ExecuteUpdate/ExecuteDelete nasıl çalışır, nerede ısırır; EF Core 10'un koşullu setter'ları, map'lenmemiş tipler için SqlQuery ve ne zaman SqlBulkCopy ya da PostgreSQL COPY'ye geçmeli.
50.000 entity'yi tek tek silmek için belleğe yükleyin ya da bir milyon satırı change tracker üzerinden eklemeye çalışın: EF tam olarak istediğinizi yapar, ama yavaş, bol bellekle ve bol round trip'le. Bilinen geçici çözümler (change detection'ı kapatmak, her N satırda context'i yeniden oluşturmak, raw SQL'e inmek) EF6'dan geliyor ve orada anlamlıydı.
EF Core farklı bir motor. O hilelerin bir kısmı artık EF Core'da büyük ölçüde var olmayan sorunları çözüyor ve EF Core'un tam da bu işler için eklediği araçları atlıyor. Bu yazıda o araçları tek tek ele alıyorum: batch'lenen SaveChanges, küme bazlı güncelleme ve silme, istediğiniz tipe raw SQL ve EF'in kendisinin darboğaz olduğu durumlar için veritabanının kendi toplu yükleme API'leri.
Araç kutusu
- Ekleme: EF Core'da
AddRange+SaveChangesAsyncbatch'lenir ve on binlerce satıra kadar gayet yeterlidir. Büyük import'larda provider'ın toplu yükleme API'sini kullanın: SQL Server içinSqlBulkCopy, PostgreSQL için Npgsql üzerinden binaryCOPY. - Koşula göre güncelleme/silme:
ExecuteUpdateAsyncveExecuteDeleteAsync(EF Core 7+). Tek bir SQL cümlesi çalışır, hiçbir entity belleğe yüklenmez. - Koşullu setter'lar: EF Core 10'dan beri
ExecuteUpdateAsyncdüz bir lambda da kabul ediyor, yani içindeifyazabiliyorsunuz. - İstediğiniz tipe sorgu: map'lenmemiş tiplerle
Database.SqlQuery<T>(EF Core 8+).
Ekleme: EF6 alışkanlıklarından EF Core batch'lemesine
EF6 döneminde hızlı ekleme için klasik tarif, AutoDetectChangesEnabled'ı kapatmak, her N kayıtta bir SaveChanges çağırmak ve context'i belli aralıklarla dispose edip yeniden oluşturmaktı. EF6 için yerinde bir tavsiyeydi. EF6'da change detection, takip edilen her entity ile biraz daha pahalılaşıyordu ve SaveChanges her satır için ayrı bir round trip yapıyordu.
EF Core farklı çalışıyor. SaveChanges birden fazla INSERT, UPDATE ve DELETE komutunu daha az round trip'e sığacak şekilde batch'liyor, EF Core 7 de ekleme işlemlerini ayrıca belirgin şekilde hızlandırdı. Büyük bir döngüde geriye kalan asıl maliyet, change tracker'ın eklediğiniz her entity'yi tutmaya devam etmesi. "Context'i yeniden oluştur" tavsiyesinin bugünkü karşılığı, onu temizlemek:
foreach (var chunk in incoming.Chunk(5_000))
{
db.Products.AddRange(chunk);
await db.SaveChangesAsync(ct);
db.ChangeTracker.Clear();
}
Batch boyutunu provider bazında ayarlayabilirsiniz (UseSqlServer(cs, o => o.MaxBatchSize(...))), ama dokunmadan önce ölçün. Varsayılanlar makul.
Güncelleme ve silme: ExecuteUpdate ve ExecuteDelete
Çok sayıda satırı silmenin eski yolları ya her entity'yi yükleyip tek tek Remove çağırıyor, ya raw SQL'e iniyor ya da extension kütüphaneleri kullanıyordu. EF Core 7'den beri bu yerleşik:
var cutoff = DateTime.UtcNow.AddDays(-90);
int deleted = await db.AuditLogs
.Where(l => l.CreatedAt < cutoff)
.ExecuteDeleteAsync(ct);
int repriced = await db.Products
.Where(p => p.CategoryId == categoryId)
.ExecuteUpdateAsync(s => s
.SetProperty(p => p.Price, p => p.Price * 1.10m)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), ct);
Her çağrı, Where koşulunuz SQL'e çevrilmiş tek bir DELETE ya da UPDATE olur. Belleğe hiçbir şey yüklenmez, iş olması gereken yerde, veritabanında yapılır.
EF Core 10: expression tree yazmadan koşullu setter'lar
EF Core 10'dan önce setter argümanı bir expression tree'ydi. Bunu koşullu kurmak (mesela sadece client gönderdiyse adı güncellemek) Expression düğümlerini elle birleştirmek demekti. Bundan keyif alan pek yoktu. Kasım 2025'te çıkan ve güncel LTS sürüm olan EF Core 10 normal bir lambda kabul ediyor:
await db.Products
.Where(p => p.Id == id)
.ExecuteUpdateAsync(s =>
{
s.SetProperty(p => p.UpdatedAt, DateTime.UtcNow);
if (patch.Name is not null)
s.SetProperty(p => p.Name, patch.Name);
if (patch.Price is not null)
s.SetProperty(p => p.Price, patch.Price.Value);
}, ct);
PATCH tarzı endpoint'lerde bu epey hantal kodu ortadan kaldırıyor.
Dikkat edilmesi gerekenler, doğrudan dokümandan
Bu metotlar change tracker'ı tamamen atlar ve bunun sonuçları var. EF Core dokümantasyonu bunları açıkça sıralıyor:
- Change tracker'ın haberi olmaz. Bir entity'yi yükleyip sonra üzerinde
ExecuteUpdateçalıştırdıysanız, takip edilen instance hâlâ eski değerleri taşır. Sonraki birSaveChanges,ExecuteUpdate'in yaptığını ezebilir. Aynı context'te aynı satırlar üzerinde iki yaklaşımı karıştırmayın. - Çağrılar arasında örtük transaction yok. Her
ExecuteUpdate/ExecuteDeletekendi başına çalışır. İki çağrıdan ikincisi hata verirse birincisi commit edilmiş olarak kalır. Transaction'ı kendiniz açın:
await using var tx = await db.Database.BeginTransactionAsync(ct);
await db.OrderLines.Where(l => l.OrderId == orderId).ExecuteDeleteAsync(ct);
await db.Orders.Where(o => o.Id == orderId).ExecuteDeleteAsync(ct);
await tx.CommitAsync(ct);
EnableRetryOnFailure gibi tekrar deneyen bir execution strategy açtıysanız, kendi başlattığınız transaction'ı db.Database.CreateExecutionStrategy() üzerinden çalıştırın. Yoksa EF transaction'ı başlatmayı reddeder.
- Otomatik concurrency kontrolü yok. Bunun yerine dönen satır sayısını kullanın:
int updated = await db.Orders
.Where(o => o.Id == orderId && o.Version == expectedVersion)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.Status, OrderStatus.Shipped)
.SetProperty(o => o.Version, o => o.Version + 1), ct);
if (updated == 0)
throw new DbUpdateConcurrencyException("Order was modified or deleted by someone else.");
- Batch'leme yok. Her çağrı ayrı bir round trip'tir. On
ExecuteUpdateçağrısı on ayrı cümle demek. - Ekleme yok.
ExecuteInsertdiye bir şey yok. Eklemeler hâlâAdd+SaveChangesya da provider'ın toplu yükleme API'si üzerinden yapılıyor.
Entity olmayan tiplere raw SQL
Yaygın bir geçici çözüm, sırf bir rapor sorgusu çalıştırmak için keyless entity type kaydetmekti. EF Core 8'den beri buna gerek yok. Database.SqlQuery<T>, sonuçları property'leri eşleşen herhangi bir tipe map'liyor:
public sealed class CategoryStats
{
public int CategoryId { get; init; }
public int ProductCount { get; init; }
public decimal AveragePrice { get; init; }
}
var stats = await db.Database
.SqlQuery<CategoryStats>($"""
SELECT CategoryId, COUNT(*) AS ProductCount, AVG(Price) AS AveragePrice
FROM Products
WHERE IsActive = {onlyActive}
GROUP BY CategoryId
""")
.OrderByDescending(s => s.ProductCount)
.ToListAsync(ct);
Interpolasyonla verilen değer string birleştirme değil, gerçek bir SQL parametresi olur. Sonuç bir IQueryable olduğu için üzerine LINQ ekleyebilirsiniz. Satır döndürmeyen komutlar için aynı şekilde parametrelendiren Database.ExecuteSqlAsync var.
Gerçek toplu ekleme: veritabanının kendi aracını kullanın
Yüz binlerce ya da milyonlarca satır için batch'lenmiş INSERT'ler bile yanlış araç. Ciddi her veritabanının bir toplu yükleme yolu var ve aradaki fark büyük.
SQL Server'da, EF'in zaten elindeki bağlantıyla:
var table = new DataTable();
table.Columns.Add("Sku", typeof(string));
table.Columns.Add("Name", typeof(string));
table.Columns.Add("Price", typeof(decimal));
foreach (var p in incoming)
table.Rows.Add(p.Sku, p.Name, p.Price);
await db.Database.OpenConnectionAsync(ct);
try
{
var connection = (SqlConnection)db.Database.GetDbConnection();
using var bulk = new SqlBulkCopy(connection)
{
DestinationTableName = "dbo.Products",
BatchSize = 10_000
};
bulk.ColumnMappings.Add("Sku", "Sku");
bulk.ColumnMappings.Add("Name", "Name");
bulk.ColumnMappings.Add("Price", "Price");
await bulk.WriteToServerAsync(table, ct);
}
finally
{
await db.Database.CloseConnectionAsync();
}
Aktif bir EF transaction'ı varsa onu SqlBulkCopy constructor'ına geçin ((SqlTransaction)tx.GetDbTransaction()). Şunu da bilin: bulk copy varsayılan olarak trigger'ları tetiklemez ve constraint'leri kontrol etmez. Bunu SqlBulkCopyOptions ile ayrıca istemeniz gerekir.
PostgreSQL'de, Npgsql'in binary COPY'si ile:
await db.Database.OpenConnectionAsync(ct);
var conn = (NpgsqlConnection)db.Database.GetDbConnection();
await using (var writer = await conn.BeginBinaryImportAsync(
"COPY products (sku, name, price) FROM STDIN (FORMAT BINARY)", ct))
{
foreach (var p in incoming)
{
await writer.StartRowAsync(ct);
await writer.WriteAsync(p.Sku, NpgsqlDbType.Text, ct);
await writer.WriteAsync(p.Name, NpgsqlDbType.Text, ct);
await writer.WriteAsync(p.Price, NpgsqlDbType.Numeric, ct);
}
await writer.CompleteAsync(ct); // bu çağrılmazsa import geri alınır
}
İkisi de EF'i tamamen devre dışı bırakır. Üretilen key'ler geri gelmez, takip edilen entity olmaz. Upsert gerektiğinde ben önce bir staging tablosuna toplu yükleme yapıyorum, sonra ExecuteSqlAsync ile tek bir MERGE (SQL Server) ya da INSERT ... ON CONFLICT (PostgreSQL) çalıştırıyorum.
Bunu her provider için ayrı yazmak istemiyorsanız, bu API'leri EF tarzı extension metotların arkasına saran üçüncü parti kütüphaneler var. EFCore.BulkExtensions, linq2db.EntityFrameworkCore ve Entity Framework Extensions en sık duyulanlar. Birini seçmeden önce lisans koşullarını okuyun ve hangi provider'ları desteklediğine bakın.
Özet
- EF6 hilelerini unutun. EF Core'da
AddRange+SaveChangesAsynczaten batch'leniyor. Uzun döngülerde parçalar arasındaChangeTracker.Clear()çağırın. - Küme bazlı değişiklikler için
ExecuteDeleteAsync/ExecuteUpdateAsynckullanın. Change tracker'ı atladıklarını, siz açmadıkça ortak bir transaction'da çalışmadıklarını ve concurrency kontrolünü sizin yerinize yapmadıklarını unutmayın. - EF Core 10'da koşullu güncellemeler için expression tree kurmak yerine
ExecuteUpdateAsync'in lambda overload'unu kullanın. - Raporlar ve diğer okuma modelleri için
Database.SqlQuery<T>kullanın. Sahte entity'lere gerek yok. - Büyük import'larda
SqlBulkCopyya da PostgreSQLCOPYkullanın, mümkünse bir staging tablosuna. - Kendi verinizle ve kendi veritabanınızla ölçün. Başkasının ortamında ölçülmüş benchmark sayıları, eski ya da yeni, sizin ortamınıza taşınmaz.
- ---
- baslik: "EF Core'da toplu ekleme, güncelleme ve silme: SaveChanges batch'lemeden SqlBulkCopy'ye"
- ozet: "EF Core'un batch'lenen SaveChanges'ı EF6 dönemi yaklaşımdan nasıl ayrılır; ExecuteUpdate/ExecuteDelete nasıl çalışır, nerede ısırır; EF Core 10'un koşullu setter'ları, map'lenmemiş tipler için SqlQuery ve ne zaman SqlBulkCopy ya da PostgreSQL COPY'ye geçmeli."
- tarih: 2026-08-18
- etiketler:
- - dotnet
- - efcore
- - performance
- kategoriler:
- - dotnet
- ---
- 50.000 entity'yi tek tek silmek için belleğe yükleyin ya da bir milyon satırı change tracker üzerinden eklemeye çalışın: EF tam olarak istediğinizi yapar, ama yavaş, bol bellekle ve bol round trip'le. Bilinen geçici çözümler (change detection'ı kapatmak, her N satırda context'i yeniden oluşturmak, raw SQL'e inmek) EF6'dan geliyor ve orada anlamlıydı.
- EF Core farklı bir motor. O hilelerin bir kısmı artık EF Core'da büyük ölçüde var olmayan sorunları çözüyor ve EF Core'un tam da bu işler için eklediği araçları atlıyor. Bu yazıda o araçları tek tek ele alıyorum: batch'lenen `SaveChanges`, küme bazlı güncelleme ve silme, istediğiniz tipe raw SQL ve EF'in kendisinin darboğaz olduğu durumlar için veritabanının kendi toplu yükleme API'leri.
- ## Araç kutusu
- - **Ekleme**: EF Core'da `AddRange` + `SaveChangesAsync` batch'lenir ve on binlerce satıra kadar gayet yeterlidir. Büyük import'larda provider'ın toplu yükleme API'sini kullanın: SQL Server için `SqlBulkCopy`, PostgreSQL için Npgsql üzerinden binary `COPY`.
- - **Koşula göre güncelleme/silme**: `ExecuteUpdateAsync` ve `ExecuteDeleteAsync` (EF Core 7+). Tek bir SQL cümlesi çalışır, hiçbir entity belleğe yüklenmez.
- - **Koşullu setter'lar**: EF Core 10'dan beri `ExecuteUpdateAsync` düz bir lambda da kabul ediyor, yani içinde `if` yazabiliyorsunuz.
- - **İstediğiniz tipe sorgu**: map'lenmemiş tiplerle `Database.SqlQuery<T>` (EF Core 8+).
- ## Ekleme: EF6 alışkanlıklarından EF Core batch'lemesine
- EF6 döneminde hızlı ekleme için klasik tarif, `AutoDetectChangesEnabled`'ı kapatmak, her N kayıtta bir `SaveChanges` çağırmak ve context'i belli aralıklarla dispose edip yeniden oluşturmaktı. EF6 için yerinde bir tavsiyeydi. EF6'da change detection, takip edilen her entity ile biraz daha pahalılaşıyordu ve `SaveChanges` her satır için ayrı bir round trip yapıyordu.
- EF Core farklı çalışıyor. `SaveChanges` birden fazla INSERT, UPDATE ve DELETE komutunu daha az round trip'e sığacak şekilde batch'liyor, EF Core 7 de ekleme işlemlerini ayrıca belirgin şekilde hızlandırdı. Büyük bir döngüde geriye kalan asıl maliyet, change tracker'ın eklediğiniz her entity'yi tutmaya devam etmesi. "Context'i yeniden oluştur" tavsiyesinin bugünkü karşılığı, onu temizlemek:
- ```csharp
- foreach (var chunk in incoming.Chunk(5_000))
- {
- db.Products.AddRange(chunk);
- await db.SaveChangesAsync(ct);
- db.ChangeTracker.Clear();
- }
- ```
- Batch boyutunu provider bazında ayarlayabilirsiniz (`UseSqlServer(cs, o => o.MaxBatchSize(...))`), ama dokunmadan önce ölçün. Varsayılanlar makul.
- ## Güncelleme ve silme: ExecuteUpdate ve ExecuteDelete
- Çok sayıda satırı silmenin eski yolları ya her entity'yi yükleyip tek tek `Remove` çağırıyor, ya raw SQL'e iniyor ya da extension kütüphaneleri kullanıyordu. EF Core 7'den beri bu yerleşik:
- ```csharp
- var cutoff = DateTime.UtcNow.AddDays(-90);
- int deleted = await db.AuditLogs
- .Where(l => l.CreatedAt < cutoff)
- .ExecuteDeleteAsync(ct);
- int repriced = await db.Products
- .Where(p => p.CategoryId == categoryId)
- .ExecuteUpdateAsync(s => s
- .SetProperty(p => p.Price, p => p.Price * 1.10m)
- .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), ct);
- ```
- Her çağrı, `Where` koşulunuz SQL'e çevrilmiş tek bir `DELETE` ya da `UPDATE` olur. Belleğe hiçbir şey yüklenmez, iş olması gereken yerde, veritabanında yapılır.
- ### EF Core 10: expression tree yazmadan koşullu setter'lar
- EF Core 10'dan önce setter argümanı bir expression tree'ydi. Bunu koşullu kurmak (mesela sadece client gönderdiyse adı güncellemek) `Expression` düğümlerini elle birleştirmek demekti. Bundan keyif alan pek yoktu. Kasım 2025'te çıkan ve güncel LTS sürüm olan EF Core 10 normal bir lambda kabul ediyor:
- ```csharp
- await db.Products
- .Where(p => p.Id == id)
- .ExecuteUpdateAsync(s =>
- {
- s.SetProperty(p => p.UpdatedAt, DateTime.UtcNow);
- if (patch.Name is not null)
- s.SetProperty(p => p.Name, patch.Name);
- if (patch.Price is not null)
- s.SetProperty(p => p.Price, patch.Price.Value);
- }, ct);
- ```
- PATCH tarzı endpoint'lerde bu epey hantal kodu ortadan kaldırıyor.
- ### Dikkat edilmesi gerekenler, doğrudan dokümandan
- Bu metotlar change tracker'ı tamamen atlar ve bunun sonuçları var. [EF Core dokümantasyonu](https://learn.microsoft.com/en-us/ef/core/saving/execute-insert-update-delete) bunları açıkça sıralıyor:
- 1. **Change tracker'ın haberi olmaz.** Bir entity'yi yükleyip sonra üzerinde `ExecuteUpdate` çalıştırdıysanız, takip edilen instance hâlâ eski değerleri taşır. Sonraki bir `SaveChanges`, `ExecuteUpdate`'in yaptığını ezebilir. Aynı context'te aynı satırlar üzerinde iki yaklaşımı karıştırmayın.
- 2. **Çağrılar arasında örtük transaction yok.** Her `ExecuteUpdate`/`ExecuteDelete` kendi başına çalışır. İki çağrıdan ikincisi hata verirse birincisi commit edilmiş olarak kalır. Transaction'ı kendiniz açın:
- ```csharp
- await using var tx = await db.Database.BeginTransactionAsync(ct);
- await db.OrderLines.Where(l => l.OrderId == orderId).ExecuteDeleteAsync(ct);
- await db.Orders.Where(o => o.Id == orderId).ExecuteDeleteAsync(ct);
- await tx.CommitAsync(ct);
- ```
- `EnableRetryOnFailure` gibi tekrar deneyen bir execution strategy açtıysanız, kendi başlattığınız transaction'ı `db.Database.CreateExecutionStrategy()` üzerinden çalıştırın. Yoksa EF transaction'ı başlatmayı reddeder.
- 3. **Otomatik concurrency kontrolü yok.** Bunun yerine dönen satır sayısını kullanın:
- ```csharp
- int updated = await db.Orders
- .Where(o => o.Id == orderId && o.Version == expectedVersion)
- .ExecuteUpdateAsync(s => s
- .SetProperty(o => o.Status, OrderStatus.Shipped)
- .SetProperty(o => o.Version, o => o.Version + 1), ct);
- if (updated == 0)
- throw new DbUpdateConcurrencyException("Order was modified or deleted by someone else.");
- ```
- 4. **Batch'leme yok.** Her çağrı ayrı bir round trip'tir. On `ExecuteUpdate` çağrısı on ayrı cümle demek.
- 5. **Ekleme yok.** `ExecuteInsert` diye bir şey yok. Eklemeler hâlâ `Add` + `SaveChanges` ya da provider'ın toplu yükleme API'si üzerinden yapılıyor.
- ## Entity olmayan tiplere raw SQL
- Yaygın bir geçici çözüm, sırf bir rapor sorgusu çalıştırmak için keyless entity type kaydetmekti. EF Core 8'den beri buna gerek yok. `Database.SqlQuery<T>`, sonuçları property'leri eşleşen herhangi bir tipe map'liyor:
- ```csharp
- public sealed class CategoryStats
- {
- public int CategoryId { get; init; }
- public int ProductCount { get; init; }
- public decimal AveragePrice { get; init; }
- }
- var stats = await db.Database
- .SqlQuery<CategoryStats>($"""
- SELECT CategoryId, COUNT(*) AS ProductCount, AVG(Price) AS AveragePrice
- FROM Products
- WHERE IsActive = {onlyActive}
- GROUP BY CategoryId
- """)
- .OrderByDescending(s => s.ProductCount)
- .ToListAsync(ct);
- ```
- Interpolasyonla verilen değer string birleştirme değil, gerçek bir SQL parametresi olur. Sonuç bir `IQueryable` olduğu için üzerine LINQ ekleyebilirsiniz. Satır döndürmeyen komutlar için aynı şekilde parametrelendiren `Database.ExecuteSqlAsync` var.
- ## Gerçek toplu ekleme: veritabanının kendi aracını kullanın
- Yüz binlerce ya da milyonlarca satır için batch'lenmiş INSERT'ler bile yanlış araç. Ciddi her veritabanının bir toplu yükleme yolu var ve aradaki fark büyük.
- SQL Server'da, EF'in zaten elindeki bağlantıyla:
- ```csharp
- var table = new DataTable();
- table.Columns.Add("Sku", typeof(string));
- table.Columns.Add("Name", typeof(string));
- table.Columns.Add("Price", typeof(decimal));
- foreach (var p in incoming)
- table.Rows.Add(p.Sku, p.Name, p.Price);
- await db.Database.OpenConnectionAsync(ct);
- try
- {
- var connection = (SqlConnection)db.Database.GetDbConnection();
- using var bulk = new SqlBulkCopy(connection)
- {
- DestinationTableName = "dbo.Products",
- BatchSize = 10_000
- };
- bulk.ColumnMappings.Add("Sku", "Sku");
- bulk.ColumnMappings.Add("Name", "Name");
- bulk.ColumnMappings.Add("Price", "Price");
- await bulk.WriteToServerAsync(table, ct);
- }
- finally
- {
- await db.Database.CloseConnectionAsync();
- }
- ```
- Aktif bir EF transaction'ı varsa onu `SqlBulkCopy` constructor'ına geçin (`(SqlTransaction)tx.GetDbTransaction()`). Şunu da bilin: bulk copy varsayılan olarak trigger'ları tetiklemez ve constraint'leri kontrol etmez. Bunu `SqlBulkCopyOptions` ile ayrıca istemeniz gerekir.
- PostgreSQL'de, Npgsql'in binary COPY'si ile:
- ```csharp
- await db.Database.OpenConnectionAsync(ct);
- var conn = (NpgsqlConnection)db.Database.GetDbConnection();
- await using (var writer = await conn.BeginBinaryImportAsync(
- "COPY products (sku, name, price) FROM STDIN (FORMAT BINARY)", ct))
- {
- foreach (var p in incoming)
- {
- await writer.StartRowAsync(ct);
- await writer.WriteAsync(p.Sku, NpgsqlDbType.Text, ct);
- await writer.WriteAsync(p.Name, NpgsqlDbType.Text, ct);
- await writer.WriteAsync(p.Price, NpgsqlDbType.Numeric, ct);
- }
- await writer.CompleteAsync(ct); // bu çağrılmazsa import geri alınır
- }
- ```
- İkisi de EF'i tamamen devre dışı bırakır. Üretilen key'ler geri gelmez, takip edilen entity olmaz. Upsert gerektiğinde ben önce bir staging tablosuna toplu yükleme yapıyorum, sonra `ExecuteSqlAsync` ile tek bir `MERGE` (SQL Server) ya da `INSERT ... ON CONFLICT` (PostgreSQL) çalıştırıyorum.
- Bunu her provider için ayrı yazmak istemiyorsanız, bu API'leri EF tarzı extension metotların arkasına saran üçüncü parti kütüphaneler var. EFCore.BulkExtensions, linq2db.EntityFrameworkCore ve Entity Framework Extensions en sık duyulanlar. Birini seçmeden önce lisans koşullarını okuyun ve hangi provider'ları desteklediğine bakın.
- ## Özet
- - EF6 hilelerini unutun. EF Core'da `AddRange` + `SaveChangesAsync` zaten batch'leniyor. Uzun döngülerde parçalar arasında `ChangeTracker.Clear()` çağırın.
- - Küme bazlı değişiklikler için `ExecuteDeleteAsync` / `ExecuteUpdateAsync` kullanın. Change tracker'ı atladıklarını, siz açmadıkça ortak bir transaction'da çalışmadıklarını ve concurrency kontrolünü sizin yerinize yapmadıklarını unutmayın.
- - EF Core 10'da koşullu güncellemeler için expression tree kurmak yerine `ExecuteUpdateAsync`'in lambda overload'unu kullanın.
- - Raporlar ve diğer okuma modelleri için `Database.SqlQuery<T>` kullanın. Sahte entity'lere gerek yok.
- - Büyük import'larda `SqlBulkCopy` ya da PostgreSQL `COPY` kullanın, mümkünse bir staging tablosuna.
- - Kendi verinizle ve kendi veritabanınızla ölçün. Başkasının ortamında ölçülmüş benchmark sayıları, eski ya da yeni, sizin ortamınıza taşınmaz.