EF8 İle Performanslı Toplu İşlemler
Bu makalede, özelikle büyük datalar üzerinde yapılan Bulk Operasyonları, EF Core 8.0 ile gelen yeni özellikler ile nasıl daha performanslı yapabileceğinizden bahsedeceğim.
Öncelikle çalışacağımız Database, Northwind database’i olacaktır. Buradan indirebilirsiniz.
Aşağıda görüldüğü gibi, .Net 8.0 “EntityBulkOperations” adında bir Console Application yaratılır.
Şimdi sıra geldi DAL Projesini yaratmaya:
DAL adında yeni bir Class Library aşağıda görüldüğü gibi yaratılır. İçine Models/DB şeklinde iki tane folder açılır. Bu DAL uygulaması için DB First yöntemini kullanacağız.
DAL Projesine aşağıdaki Kütüphaneler eklenir.
Terminal açılıp DAL projesinin altına gelinir. Ve aşağıdaki komut çalıştırılır. Mevcut Northwind DB’den, DBContext ve Entityleri oluşturmak için scaffold tool’u kullanılmıştır.
1 |
dotnet ef dbcontext scaffold "Data Source=.;initial catalog=Northwind;Trusted_Connection=True;Encrypt=False" Microsoft.EntityFrameworkCore.SqlServer --output-dir Models/DB |
Not: Eğer “scaffold” komutunuz, bilgisayarınızda yüklü değil ise, EntityFrameworkCore.Tools’u aşağıdaki komut ile kurmanız gerekmektedir.
1 |
“Install-Package Microsoft.EntityFrameworkCore.Tools” |
Artık yukarıda görüldüğü gibi DAL projemiz ve NorthwindContext’imiz hazır :)
Bulk Insert:
Aşağıda görüldüğü gibi 2 tablo ile ilişkili olan Territories ve Region üzerinde bulk işlemler yapılacaktır.
Territories tablosuna Insert işlemi yapılırken, aynı anda ilişkisel olan Region tablosuna da aynı anda Performanslı bir şekilde kayıt atabilmek için, aşağıdaki ücretsiz kütüphane kullanılmıştır. EF Core 8.0 ile bunun doğrudan yapılabileceği standart bir yol henüz bulunmamaktadır. Ayrı ayrı Entityleri kaydedip, “DbContext.SaveChanges” tabi ki kullanabilirsiniz :)
Aşağıda görüldüğü gibi “Territory” tablosuna üç kayıt atılmıştır Ayrıca herbir Territory kaydına karşılık, yeni bir “Region” kaydı oluşturulmuş ve ID’si de Territory tablosuna aynı anda kaydedilmiştir. Buradaki önemli option config parametresi => “bulkConfig.IncludeGraph = true“‘dur. Bu parametre ile child tablolara da kayıt atılmaktadır.
Program.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using DAL.Models.DB; using EFCore.BulkExtensions; using (var dbContext = new NorthwindContext()) { using (var transaction = dbContext.Database.BeginTransaction()) { try { var territories = new List() { new Territory{TerritoryId="100000", TerritoryDescription="Bulk Territoy",Region=new Region(){RegionDescription="Bulk Region",RegionId=5}}, new Territory{TerritoryId="100001",TerritoryDescription="Bulk Territoy2",Region=new Region(){RegionDescription="Bulk Region2",RegionId=6}}, new Territory{TerritoryId="100002",TerritoryDescription="Bulk Territoy3",Region=new Region(){RegionDescription="Bulk Region3",RegionId=7}}, }; dbContext.BulkInsert(territories, bulkConfig => { bulkConfig.IncludeGraph = true; bulkConfig.SetOutputIdentity = true; bulkConfig.EnableStreaming = true; }); transaction.Commit(); } catch (Exception ex) { transaction.Rollback(); Console.WriteLine(ex.Message); } } } |
Bulk Update:
Aşağıda görüldüğü gibi Customer ve Orders birbiri ile relational iki tablodur. Biz de belli koşullara göre seçtiğimiz Orders tablosunu, Bulk bir şekilde güncelleyeceğiz.
Öncelikle Order tarihi 1998 olan ve ülkesi ‘Finland’ olan siparişler listelenir.
1 2 3 4 5 |
var ordersQuery = from ord in dbContext.Orders join cus in dbContext.Customers on ord.CustomerId equals cus.CustomerId where cus.Country == "Finland" && ord.OrderDate.Value.Year == 1998 select ord; var filterOrders = ordersQuery.ToList(); |
Yukarıdaki Query ile sadece Order çekilecektir. Toplu güncelleme işleminde Order yanında Customer da çekilmek istenir ise, aşağıdaki gibi bir Query yazılmalıdır.
1 |
var orders = dbContext.Orders.Include(o=>o.Customer).Where(o => o.Customer.Country == "Finland" && o.OrderDate.Value.Year == 1998).ToList(); |
Yukarıdaki Queryler çağrıldığında Data null hatası alınmaktadır. Sorunu biraz araştırdığımda, aşağıda görülen alanların null geldiğini gördüm. Ama bizim “scaffold” ile oluşturduğumuz Entitylerde, bu string alanlar Nullable olarak tanımlanmamışlardır!
Yapılması gereken, aynen aşağıda olduğu gibi Customer ve Order tablolarının ilgili alanlarının “string?” nullable olarak güncellenmesidir.
İlgili güncelleme yapıldıktan sonra sorgu çalıştırıldığında, aşağıdaki gibi kayıtlara erişilir. Aşağıda görüldüğü gibi “Orderlar” ve ilişkili oldukları “Customerlar” gelmektedir.
Şimdi gelin Order Tablosundaki ilgili koşullara göre null olan ShipRegion alanın, “XXX” olarak Bulk bir şekilde güncelleyelim. Aşağıda görüldüğü gibi, önce ilgili koşullara göre Orderslar çekilmiş, daha sonrada “ExecuteUpdateAsync()” methodu ile => “ShipRegion” filed’ina “XXX” değeri topluca, asenkron bir şekilde set edilmiştir.
1 2 |
await dbContext.Orders.Include(o => o.Customer).Where(o => o.Customer.Country == "Finland" && o.OrderDate.Value.Year == 1998).ExecuteUpdateAsync(x => x.SetProperty(o => o.ShipRegion, "XXX")); |
Result: Aşağıda görüldüğü gibi ilgili Orderlar’ın “ShipRegion”‘ı “XXX” olmuştur.
Tekrar istenen field’a null değerinin atanması için “x.SetProperty(o => o.ShipRegion, (string?)null))” şeklinde bir atamanın yapılması gerekmektedir.
Aşağıdaki örnekte Order tablosuna ait iki Field “ShipRegion, ShipAddress”, aynı anda Bulk olarak asenkron şekilde güncellenmiştir. Burada kritik keyword “ExecuteUpdateAsync()” methodudur. .Net 8.0 ile performanslı bulk güncelleme, özellikle büyük datalarda “ExecuteUpdateAsync()” ile sağlanmaktadır.
1 2 3 |
await dbContext.Orders.Include(o => o.Customer).Where(o => o.Customer.Country == "Finland" && o.OrderDate.Value.Year == 1998).ExecuteUpdateAsync(x => x.SetProperty(o => o.ShipRegion, "XXX") .SetProperty(o => o.ShipAddress, o => o.ShipAddress + 'X')); |
NOT: Orders tablosnun bağlı olduğu Customer tablosu, “SetProperty()” methodu ile Bulk bir şekilde güncellenememektedir. ExecuteUpdateAsync() child methodlarda hata vermektedir.
Bulk Delete:
Aşağıda görüldüğü gibi başta toplu olarak Insert ettiğimiz üç Territories kaydını, şimdi Bulk olarak sileceğiz. Gene burada .Net 8.0 ile hayatımız giren “ExecuteDeleteAsync()” methodu, performanslı bir şekilde toplu silme işlemi sağlamaktadır.
1 2 |
List filter = new List { "100000", "100001", "100002" }; await dbContext.Territories.Where(t => filter.Contains(t.TerritoryId)).ExecuteDeleteAsync(); |
Performance:
Yapılan Benchmark testleri gösteriyor ki, Entity sayısı arttıkça performans misli ile artmaktadır.
1-) EF Extension’ın paylaştığı bu test de bize, performans konusunda kesinlikle bir fikir vermektedir.
2-) EFCore.
Sonuç:
EF Core Bulk işlemler için, AddRange(), Add(), EFCore.BulkExtensions gibi birçok methodun kendine özgü avantajları ve sınırlamaları vardır. Küçük ile orta ölçekli veriler için Add veya AddRange yöntemleri gayet yeterli olabilir. Ancak daha büyük veri kümeleri için, EFCore.BulkExtensions gibi Bulk işlemler yapabileceğiniz methodları kullanmak, hem verimlilik hem de performans açısından mutlaka değerlendirilmesi gereken yeniliklerden bazılarıdır. Seçiminizi, her zaman yapacağınız işlemlerin büyüklüğüne ve uygulamanızın ihtiyaçlarına göre belirleyin.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Source:
- https://devblogs.microsoft.com/dotnet/announcing-ef8-rc2/
- https://www.nuget.org/packages/EFCore.BulkExtensions
- https://entityframework-extensions.net/bulk-insert-optimized
- https://servicestack.net/posts/bulk-insert-performance
- https://medium.com/codenx/ef-core8-bulk-operations-bd008a98004e
Son Yorumlar