Microsoft Entity Framework’ü geliştirmeye devam etsin, birçok kimse çoktan Linq To Sql’i kullanmaya başladı ya da başlıyor. Linq To Sql öldü, diriliyor, iyi idi kötü idi, ORM midir değilmidir diye tartışılırken kullananların ilk sıkıntısı Linq To Sql’in performans sorunları oldu (Tüm ORM’lerin olduğu gibi). Linq To Sql ile geliştirme hızlı ve bir o kadar da kolay olmasına karşın sayıca çok ve kompleks tablolar üzerinde ağır sorgular yazılmaya başlandığında çalışan sorguların süre olarak zaman alması birçok kimsenin istemediği bir durum olmuştur. Bu yazımızda da bu sorun ile ilgili ne yapabileceğimizi inceleyeceğiz. Öncelikle kısa bir hatırlatma ile Linq To Sql’in aslında nasıl çalıştığını hatırlayalım. Böylelikle soruna hangi noktada müdahale edebileceğimizi daha net belirleyebiliriz.
Yukardaki şekilde de görüldüğü üzere, bir Linq To Sql sorgusu çalıştırılmak istendiğinde yazılan sorgu ifadesi her zaman söz dizimi(syntax) kontrolünden geçtikten sonra veritabanı sunucusuna özel bir şekilde SQL sorgusuna çevrilmektedir. Burdaki can sıkıcı nokta ise bu işlemin “her zaman” gerçekleşmedir. Örneğin bir müşteri verilerinin filitrelenmesi sırasında belirli kriterlere göre filitreleme yapılacağını düşünelim (ya da bir alışveriş sitesinde belirli bir markaya ait ürünleri listelemediğimizi de düşünebiliriz). Bu durumda her defasında veritabanı sunucusuna gönderilecek SQL sorgusunun oluşturulması için yazılan Linq To Sql sorgusunun yeniden sözdizimi kontrolü ve çevrim işlemlerinin yapılmasına ihtiyaç yoktur. Belki böyle bir sorun şu anda yaşamıyor olabiliriz ama bazen bu işlemlerin “her zaman” gerçekleşmesi önemli performans sorunlarına neden olabilmektedir.
Yukardaki kaba taslağa bakıldığında Linq To Sql yazımına müdahale edilemeyecektir.Özelleştirilmiş Linq To Sql motoru (Linq To X) yazılmadıkdan sonra çıkacak sonuca da (kısmen) müdahale edilemeyeceğine göre en azından “her zaman” gerçekleşen olayları devre dışı bırakabilirsek yani, Linq To Sql sorgusunun kontrol ve çevrim işlemlerinden sonraki oluşan sonucu global bir alanda saklayabilir (bir nevi önbellekleme –caching- yapılabilinirse)ve ihtiyaç duyulduğunda bu global alandan alıp kullanılırsa birçok iş devre dışı bırakılacağı için performans artışı sağlanılabilinir.
Linq To Sql sorgusu ilk defa işleme alındığındLinq To Sql sorgusu ilk defa işleme alındığında 1 ile numaralandırlmış okları takip ederek işlemlerden geçecektir. Bu işlemler sırasında Linq To Sql motoru ürettiği sonucu önbelleğe alacaktır. İkinci defa aynı yapıya sahip benzer bir Linq To Sql sorgusu işleme 2 ile numaralandırılmıs oklar takip edilecek yani önbellekden sonuç alınarak birçok işlem atlanılıp doğrudan veritabanına çalışması gereken SQL ifadesi gönderilecektir. Bu şekilde kazanılması düşünülen performans artımının ne ölçülerde olduğunu yukardaki bahsi geçen tekniği uygulamaya aldıkdan sonra test edeceğiz.
Daha önce de bahsedildiği gibi işlemlerden sonra oluşan sonucun saklanması ve tekrar kullanılması konusunda Linq To Sql kütüphanesi geliştiriciler için olanaklar sunuyor. System.Data.Linq isimalanı altındaki CompiledQuery isimli sealed sınıf sayesinde Linq To Sql sorgularını önbelleklenebilir.Oldukça basit bir yapıya sahip olan sınıfın geri dönüş değeri generic (şablon) bir delegate (temsilci) olan Compile isimli static ve üç farklı versiyonu bulunan (overloaded) metoda sahip (.NET Framework 3.5 gerekmektedir). Metodun geri dönüş değerinin bir temsilci olması istediğimiz kriterlere göre hazırlayacağımız metodun çağrılamasına olanak sağlamaktadır.
public static Func<TArg0, TArg1, TResult> Compile<TArg0, TArg1, TResult>(Expression<FuncTArg0, TArg1, TResult>> query) where TArg0 : DataContext;
Northwind veritabanı üzerindeki Products tablosundaki CategotyID’si 2 olan ürünleri listeleyecek bir örnek göz önüne alındığında Compile metodunun nasıl kullanılacağına bakalım.
İlk başta çok karışık gibi görünse de kısaca;
Targ0 DataContext nesnesini - zaten bu tip üzerine bir kısıtlayıcı (constraint) konulmuş -
Targ1 filitreleme işleminde kullanılacak parametreyi
TResult işlem sonucunda geriye dönecek verinin türünü
query ise Linq To Sql sorgusunu temsil etmektedir.
Northwind veritabanı üzerinde basit bir Linq To Sql sorgusu hazırlayarak nasıl kullanılacağını inceleyelim. Oncelikle derlenmiş sorgumuzu oluşturacak ifademizi hazırlayalım.
public static class BTADataOperations
{
public static Func<nwDataContext, int, IQueryable<Product>> GetProductsByCatId = CompiledQuery.Compile((nwDataContext nwDc, int catId) =>
from p in nwDc.Products
where p.CategoryID == catId select p // bu ifade sonucu IQueryable
türünden nesnedir!
);
}
Compile metodunun geri dönüş değeri olan GetProductsByCatId temsilcisini kullanabilmek için DataContext nesnemizin bir örneğini ve ürüne ait kategori id’sini vermek yeterli olacaktır. Temsilcinin statik olması tüm uygulama içerisinden rahatça erişilebilmesini, uygulama boyunca tekil olmasını ve uygulama başlamadan önce hazır bir şekilde kullanıma hazır olmasını sağlamaktadır.
private void button1_Click(object sender, EventArgs e)
{
nwDataContext dc = new nwDataContext();
foreach( var p in BTADataOperations.GetProductsByCatId(dc, 2))
{
MessageBox.Show(p.ProductName);
}
}
Yukardaki kod bloğu çalıştırıldığında kategori id’si 2 olan ürünlerin isimlerinin geldiği görülecektir. Test değerlendiemesine geçmeden önce GetProductsByCatId ile çalışmasını tasarladığımız Linq To Sql sorgusu birden fazla parametre alması durumunda ise şu şekilde bir çözüm geliştirilebilir.
public static class BTADataOperations
{
public static Func<nwDataContext, ProductParameters, IQueryable<Product>> GetProducts= CompiledQuery.Compile((nwDataContext nwDc, ProductParameters parameters) =>
from p in nwDc.Products
where p.CategoryID == parameters.CatId && p.ProductName == parameters.ProductName
select p );
}
public class ProductParameters
{
public int CatId { get; set; }
public string ProductName { get; set; }
public int UnitsInStock { get; set; }
}
Daha öncede bahsedildiği gibi Compile metodunun ikinci parametresi Linq To Sql sorugusuna geçirilecek parametreyi temsil etmektedir. Aşağıdaki gibi birden fazla parametreyi ProductParameters türünden nesne oluşturup nesnenin özelliklerine değerlerini vererek Linq To Sql sorgusuna geçişi sağlanabilir.
nwDataContext dc = new nwDataContext();
foreach (var p in BTADataOperations.GetProducts(dc, new ProductParameters() { CatId=2, ProductName="Chef Anton's Cajun Seasoning" }))
{
MessageBox.Show(p.ProductName);
}
Şimdi ise bu çalışmaların sonucunda ne kadar performans artışı sağlandığını inceleyelim. Bunun için aşağıdaki gibi bir yapılan işlemin süresini ölçen test kodu kullanılabilir. Ilk denemede tek bir sorgunun çalışmasını test edelim.
public Form1()
{
InitializeComponent();
var v = from s in dc.Products select s;
foreach (var x in v)
;
}
Yukardaki kodun yazılmasının amacı DataContext nesnesinin her iki durum için kullanıma hazır hale gelmesini sağlamaktır. Aksi durumda DataContext nesnesinin ilk kullanımında birçok işlemin gerçekleşeceği için zaman almasından dolayı test sürelerinin yanlış çıkmasının önüne geçilmek istenmiştir.
Test Makinası :
.NET Framework 3.5 SP1
Visual Studio 2008 SP1
SQL Server 2005 SP2
Windows 7 RTM
3GB Bellek, 2.1 Ghz T8100 (32 bit) işlemci ve 300GB 7200RPM HDD
int times = 1;
nwDataContext dc = new nwDataContext();
private void button1_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < times; i++)
{
foreach (var p in BTADataOperations.GetProductsByCatId(dc, 2))
{ }
}
sw.Stop();
MessageBox.Show(sw.ElapsedMilliseconds.ToString());
}
private void button2_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < times; i++)
{
foreach (var p in from product in dc.Products where product.CategoryID == 2 select product)
{ }
}
sw.Stop();
MessageBox.Show(sw.ElapsedMilliseconds.ToString());
}
Sorgu Sayısı |
Complied Query |
Normal Query |
1 |
0 ms |
3 ms |
10 |
3 ms |
22 ms |
100 |
30 ms |
174 ms |
1000 |
284 ms |
1786 ms |
Yukardaki sonuçlardan anlaşılacağı üzere CompiledQuery kullanarak hazırlanan Linq To Sql sorguları 5 – 6 kata kadar daha hızlı çalışmaktadır.Benzer yapıda Linq To Sql sorguların yoğun olarak kullanıldığı sistemlerde CompiledQuery sınıfının kullanılması performans açısından oldukça önem kazanmaktadır.
Örnek uygulamayı indimek için tıklayınız.