Makale Özeti

Bu yazımızda LINQ to SQL projelerinde yapılan Bulk Delete işlemlerinin performans analizini yapıp bu tarz işlemlerin daha etkili olarak gerçekleşmesini sağlayacak bir metot hazırlayacağız.

Makale

LINQ to SQL’in temel hedef noktası projelerdeki Select işlemleridir. Tabi ki bunun yanında diğer CRUD işlemleri olan Insert, Update ve Delete sorguları da DataContext nesneleri üzerinden gerçekleştirilebiir. Bu işlemlerle ilgili daha önceden yazdığım blog girdimi bu linkten okuyabilirsiniz. Bu yazımda LINQ to SQL yapısı dahilinde veritabanında çalıştıracağımız Bulk Delete(birden fazla kaydı etkileyen delete işlemleri diyelim) işlemlerini inceleyeceğiz. Eğer daha önceden LINQ to SQL’de yaptığınız delete işlemlerini SQL Profiler’dan gözlemlemediyseniz yazımızın ilerleyen kısımlarında(hatta hemen devamında) ilginç bir detayla karşılaşacaksınız.

Öncelikli olarak LINQ to SQL üzerinde basit bir delete işlemi için gerekli kodları yazalım ve SQL Profiler’da bizi nasıl bir süpriz bekliyormuş görelim. Northwind veritabanındaki Product tablosunu içeren bir LINQ to SQL sınıfı(.dbml dosyası) oluşturuyor ve basit bir konsol uygulaması üzerinden CategoryID değeri 10 olan ürünleri siliyorum(Veritabanında Id değeri 10 olan bir kategori yok, eğer Constraint’lere takılmadan bu örneği yazmak istiyorsanız yeni bir kategori ve bu kategoriye ait birkaç ürünü Product tablosuna eklemenizi tavsiye ederim).

Program.cs

NorthwindDataContext ctx = new NorthwindDataContext();

var p = from urun in ctx.Products

        where urun.CategoryID == 10

        select urun;

 

ctx.Products.DeleteAllOnSubmit(p);

ctx.SubmitChanges();

Şu an üzerinde çalıştığım tabloda 10 numaralı kategoriye ait 4 tane kayıt var. Uygulamayı çalıştırıp SQL Profiler’dan sorguların nasıl çalıştığını izliyoruz.

Sonuç biraz enteresan olsa da temel olarak Select sorguları için hazırlanmış bir ortamın bundan daha fazlasını yapması beklenmez sanırım. Şekile bakıp da hala nedir burada enteresan olan diye soranlarınız varsa; 4 tane farklı delete sorgusunun çalışmasıdır enteresan olan. Böyle bir işlem sonucunda çoğumuz arka planda Delete From Products Where CategoryID=10 gibi bir sorgunun çalışmasını bekleriz, çünkü veriye erişimi biz yazdığımız zaman delete sorgumuz böyle olacaktır. Ama unutmamak lazım ki artık biz yazmıyoruz! LINQ to SQL bu tarz bir delete ifadesini arka planda her bir satırı etkileyecek şekilde tasarlar ve SQL Server’a gönderir. Dolayısıyla aynı anda yüzlerce-binlerce kayıdı etkileyecek delete işlemi yapıyorsak, resimde gördüğümüz çıktı bizi performans kuşkularına düşürebilir. Hem satırların tek tek silinmesi, hem de network trafiği açısından bu kadar sorgunun taşınması sorun oluşturabilir. O zaman bu durumu iyileştirmek için nasıl bir yol izlemek lazım?

Bu sorunun cevabını bulmak için ben yaklaşık 3 saatimi harcadım. Ancak uzunca zaman harcamanın sonunda bir sonuca ulaşamayınca dünyayı yeniden keşfetmeye gerek duymayıp arama motorlarından faydalı bir blog girdisine ulaştım. Bu linkten orjinaline ulaşabileceğiniz yazıda gayet güzel ve basit bir metot ile bu işin üstesinden gelinmiş. Aşağıda bu metodu görebilirsiniz. Metodu extension metot olarak uygulayacağımız için DataContext’ten eriştiğimiz Table nesneleri üzerinden çağırmamız daha kolay olacak. Metotla ilgili açıklamaları ilgili satırlara yorum olarak ekliyorum.

TableExtensions.cs

public static class TableExtensions

{

    //DataContext'te yer alan Table<TEntity> nesneleri uzerinden cagrilabilmesi icin Extension Method olusturuyoruz

    public static int BulkDelete<TEntity>(this Table<TEntity> table, IQueryable idsToDelete) where TEntity : class

    {

        //Uzerinde sorgu calistirilacak tablonun metadata bilgileri cekiliyor

        MetaTable metaTable = table.Context.Mapping.GetTable(typeof(TEntity));

        string tableName = metaTable.TableName;

 

        //Bu arada bulk delete metodumuz sadece bir tane PK kolonu tasiyan tablolarda kullanilabilir!

        if (metaTable.RowType.IdentityMembers.Count != 1)

        {

            var keyFields = from im in metaTable.RowType.IdentityMembers

                            select im.MappedName;

 

            if (keyFields == null || keyFields.Count() < 1)

            {

                keyFields = new List<string>();

            }

            //Eger tabloda birden fazla PK varsa Exception firlatiliyor

            throw new ApplicationException(string.Format("Error in TableExtensions.BulkDelete<TEntity>: Table {0} has a compound key.  BulkDelete can only operate on tables with a single key field.  (key fields found: {1}", tableName, string.Join(",", keyFields.ToArray())));

        }

        string primaryKey = metaTable.RowType.IdentityMembers[0].MappedName; //PK kolonu bulduk

 

        System.Data.Common.DbCommand origCmd = table.Context.GetCommand(idsToDelete);

        string toDelete = origCmd.CommandText;

        //Iste bu kisimda Delete sorgusu olusturuluyor ve

        //subquery ile Select cumlesinden donen kayitlar Delete ifadesine dahil ediliyor

        string sql = string.Format("delete from {0} where {1} in ({2})", tableName, primaryKey, toDelete);

 

        origCmd.CommandText = sql;

 

        bool openedConn = false;

 

        //Baglanti acma-kapama ve sorgunu calismasi islemleri burada yapiliyor

        if (origCmd.Connection.State != ConnectionState.Open)

        {

            openedConn = true;

            origCmd.Connection.Open();

        }

 

        try

        {

            //Sorgu burada dogrudan calistiriliyor. Dolayisiyla bu metotdan sonra

            //DataContext.SubmitChanges() metodunu cagirmaya gerek kalmiyor

            return origCmd.ExecuteNonQuery();

        }

        finally

        {

            if (openedConn)

            {

                origCmd.Connection.Close();

            }

        }

    }

}

 

Metot DataContext nesnemiz içerisinde yer alan entity tablolarımız için hazırlanmış bir extension metottur. Yani projemizin herhangi bir yerinden DataContext.TabloIsmi.BulkDelete() şeklinde çağrılabilir. Parametre olarakta sorgulanabilir bir koleksiyon alıyor, yani LINQ sorgumuzun sonucu parametre olarak bu metoda gönderilecek. Metot içerisinde ele alınan entity tablosunun yapısı çözümlenerek tablo ismi ve primary key kolonu elde ediliyor. Böylece DELETE FROM TabloAdi WHERE PrimayKeyKolonu IN (Silinecek satırların PK değerleri) şeklindeki bir sorguyu hazırlamamız mümkün olacaktır. Dikkat ettiyseniz buradaki sorgu sayesinde tüm delete işlemlerini tek bir sorgu ile yapabileceğiz.

Metot ile ilgili bir diğer dikkat edilmesi gereken nokta ise yukarıda koyu puntolarla yazdığım DELETE ifadesinin sonunda yer alan subquery’nin döndüreceği değerler. Sorguyu biraz daha açacak olursak; burada incelediğimiz Product tablosu için DELETE FROM Product WHERE ProductID IN (SELECT ProductID FROM Product WHERE …) şeklinde bir sorgumuz olmalı. Yani subquery olarak saklanan SELECT sorgusu geriye sadece primary key kolonun değerlerinden oluşan satırları döndürmelidir. O zaman BulkDelete metoduna bir LINQ sorgusundan gelen tüm bilgileri değil de sadece primary key kolonundan oluşan kümeyi göndermemiz gerekecektir. İzah etmek istediğim bu durum anlaşılmadıysa aşağıdaki kod parçalarını dikkatlice incelemenizi tavsiye edeceğim.

Program.cs

NorthwindDataContext ctx = new NorthwindDataContext();

var p = from urun in ctx.Products

        where urun.CategoryID == 10

        select urun;

 

//BulkDelete metoduna sadece PK kolonundaki bilgileri gondermemiz gerekecegi icin

//sorgu sonucundaki var degiskeninin sadece ProductID alanini seciyoruz

ctx.Products.BulkDelete(p.Select(pr => pr.ProductID));

 

Görüldüğü gibi Lambda ifadesi kullanılarak LINQ sorgusu sonucunda gelmesi beklenen kayıtların sadece ProductID bilgilerini metoda gönderdik. Yine dikkat çekmek istediğim bir nokta da bu sorgulama sonunda ctx nesnesinin SubmitChanges metodunu çağırmamıza gerek olmadığıdır. Çünkü extension metot içerisinde yeni bir sorgu nesnesi oluşturulup bunun çalıştırılması sağlanıyor ve DataContext nesnesi üzerinden ek bir işlem daha yapmamıza gerek kalmıyor. Metodun çalışması sonucunda SQL Server’a gönderilecek bilgileri SQL Profiler adlı aracımızdan izlediğimizde aşağıdaki gibi bir sonuçla karşılaşacağız.

Ekran çıktısında da görüleceği gibi SQL Server’a gönderilen sorgu sayısı artık bire inmiş durumda. Eğer LINQ to SQL kullandığınız projelerinizde aynı anda çok fazla sayıda kayıt silmek gibi bir ihtiyacınız olursa böyle bir metot işinizi görecektir.

Bir başka makalede görüşmek dileğiyle.


Uğur UMUTLUOĞLU
Microsoft MVP (ASP.NET)
www.umutluoglu.com
www.nedirtv.com