Burak Selim Senyurt(MVP)
Matematik Mühendisi bir .NET Severin Yazıları...

PLINQ - ForAll [Beta 1]

Perşembe, 28 Mayıs 2009 17:43 by bsenyurt

Merhaba Arkadaşlar,

Bildiğiniz gibi bir süredir LINQ sorgularının paralel çalıştırılması ile ilişkili çalışmalarıma ve araştırmalarıma devam etmekteyim. Bu yazımdaki konumuz ise System.Linq.ParallelEnumerable static sınıfı içerisinde tanımlanmış olan ForAll genişletme metodudur(extension methods).

public static void ForAll<TSource>(this ParallelQuery<TSource> source, Action<TSource> action);

ForAll metodu yukarıdaki prototipinden de görüldüğü gibi ParallelQuery referanslarına uygulanabilmektedir. Bununla birlikte metod ikinci parametre olarak, Action<TSource> tipinden generic bir temsilci almaktadır.

public delegate void Action<in T>(T obj);

Yukarıdaki prototipe göreyse, Action<T> temsilcisi(delegate), generic tip olarak ForAll metoduna gelen tipi(TSource) kullanmaktadır. Bu generic tip tahmin edeceğiniz üzere ParalleQuery referansınında kaynak tipidir. Ayrıca temsilci geriye herhangibir değer döndürmeyen(void) metodları işaret edebilmektedir.

Sonuç olarak ForAll metodu aslında, AsParallel metodunun kullanılması sonucu üretilen referans üzerinden gelen her bir nesne örneği için yapılması istenen işlemleri ele almaktadır. Bu açıdan bakıldığında akla gelen soru şu olacaktır.

Paralel sorguların çalışması sonucu üretilen çıktılar üzerinde foreach döngüleri yardımıyla da dolaşabiliyorken, ForAll metodunu neden kullanırız?

Aşağıdaki kod parçasını göz önüne alalım.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;

namespace UsingForAll
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> productList = GetProductList();

            var result = from p in productList.AsParallel()//.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
                         where p.ListPrice>=400 && p.Color=="Black"
                         select p;

            result.ForAll(p=>Console.WriteLine("("+Thread.CurrentThread.ManagedThreadId.ToString()+")\t"+p.Name));
        }

        static List<Product> GetProductList()
        {
            List<Product> productList = new List<Product>();

            SqlConnection conn = new SqlConnection("data source=Manchester;database=AdventureWorks2008;integrated security=true");
            SqlCommand cmd = new SqlCommand("Select ProductId,Name,ListPrice,ProductNumber,Color,SafetyStockLevel From Production.Product", conn);
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            while (reader.Read())
            {
                productList.Add(new Product
                {
                    ProductId = Convert.ToInt32(reader[0]),
                    Name = reader[1].ToString(),
                    ListPrice = Convert.ToDecimal(reader[2]),
                    ProductNumber = reader[3].ToString(),
                    Color = reader[4].ToString(),
                    SafetyStockLevel = Convert.ToInt32(reader[5])
                }
                );
            }
            reader.Close();
            return productList;
        }
    }

    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public string ProductNumber { get; set; }
        public string Color { get; set; }
        public int SafetyStockLevel { get; set; }
    }
}

Bu kod parçasında odaklanmamız gereken nokta result referansı üzerinden ForAll metodunun çağırılışıdır. Bu çağrı sırasında labmda operatöründen(=>) yaralanılmaktadır ve bulunan ürünlerin adları ile o an çalışmakta olan Thread' in numarası(ManagedThreadId) Console ekranına yazdırılmaktadır. Sonuç aşağıdakine benzer olacaktır.

Her ne kadar Thread sayıları eşit olmasada 4 ve 1 nolu iki ayrı iş parçasının çalıştırıldığı görülmektedir. Şimdi aynı sorgu sonuçlarını foreach döngüsü yardımıyla elde etmeye çalıştığımızı düşünelim.

List<Product> productList = GetProductList();

var result = from p in productList.AsParallel()//.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
                 where p.ListPrice>=400 && p.Color=="Black"
                 select p;

foreach (Product p in result)
{
   Console.WriteLine("(" + Thread.CurrentThread.ManagedThreadId.ToString() + ")\t" + p.Name);
}

Bu kez uygulama çalıştırıldığında aşağıdaki sonuçları alırız.

Volaaa!!! Cool 1 numaralı sadece tek bir thread görünüyor.

Bu nasıl oldu? Acaba foreach döngüsü kullanıldığında sorgu AsParallel metodu olmasına rağmen paralel çalıştırılmadı mı? Yoksa çalışma zamanı(runtime) sorgunun paralel çalıştırılmaya değer olmadığına mı kanaat getirdi(ki böyle bir meselede var)?

Aslında farklı çalışmanın sebebi şu. LINQ sorguları bilindiği gibi kullanıldıkları yerde çalıştırılırlar(deferred execution ilkesi). Bu nedenle sorgunun çalıştırılması foreach döngüsünde ilk eleman elde edilmeye çalışıldığı sırada olur. Lakin sorgu AsParallel metodu nedeniyle paralel çalışmasına rağmen, foreach metodu okuma işlemine başlamadan önce tüm yönetimli thread' leri tekrardan tek bir thread içerisinde birleştirir. Yani foreach döngüsünün kendisi paralel çalışma özelliğne sahip değildir. Bu nedenle paralel çalıştırılan sorgu sonuçlarını, o an üzerinde çalıştığı thread' de birleştirmeden ilerleyemez. ForAll metodu ise tam aksine çalışmakta olup, okuma işlemlerininde paralel yürütülmesini sağlamaktadır. Aslında durumu basit iki resim ile canlandırmaya çalışalım. Aşağıdaki şekilde foreach çalışması sırasındaki işleyiş sembolize edilmektedir.

Buna göre sorgu paralel olarak çalışan görevlere ayrılmakta ve herbir görev içerisinde where gibi koşullar ele alınmaktadır. Ancak tüm PLINQ ifadesi tamamlandığında sonuçlar tek bir Task altında birleştirilmektedir. (Kahverengi çerçeveli kutucuklar bulunan nesne örnekleri üzerinden yapılan işlemleri sembolize etmektedir. Örneğin Console.WriteLine gibi) Sonrasında ise herbir öğe için foreach döngüsü içerisinde yazılan kodlar işletilmektedir.

Aşağıdaki şekilde ise ForAll kullanımı sırasındaki senaryo ifade edilmeye çalışılmaktadır.

Yine PLINQ ifadesinin çalışması sırasında n adet Task paralel olarak başlatılır. Ancak foreach' ten farklı olarak her task' in içerisinde hem Where gibi koşulların kontrolü ele alınmakta hemde örneğimizde ki her bir sonuç için ayrı ayrı işlemler(Console.WriteLine gibi) gerçekleştirilmektedir. Yani task' ler paralel olarak işledikten sonra tek bir Task altında birleştirilmezler. Sanıyorum şekil yardımıyla sizde benim gibi, gerçekleşen iki farklı işleyişi daha net canlandırabildiniz. (Tabi işlemcinin içerisine girip olan biteni canlı canlı görmemiz mümkün değil. Ama kim bilir, belki gelecek nesil sistemlerde çalışma zamanını, tıpkı bir doktorun sanal bir hastanın organları içerisinde ilerleyişi gibi, bilgisayar donanımı üzerindem gözlemleyebiliriz. Wink)

Herşey güzel ama, hangisini ne zaman kullanmak gerekir öyleyse?

Aslında foreach döngüsünü daha çok sorgu sonuçlarının sırasını(order) korumak istediğimiz durumlarda değerlendirebiliriz. Bununla birlikte, sonuç listesi üzerinde ardışıl olarak işlemler yapmak istiyorsakta tercih edebiliriz.

Böylece geldik kısa bir yazımızın daha sonuna. Bir sonraki yazımızda görüşünceye dek hepinize mutlu günler dilerim.

UsingForAll.rar (22,17 kb)

Tags:   ,
Categories:   LINQ | PLINQ
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Paralel Sorgularda İstisna Yönetimi(Exception Handling) [Beta 1]

Salı, 26 Mayıs 2009 17:30 by bsenyurt

Merhaba Arkadaşlar,

Yönetimli kod(Managed Code) tarafında istisna yönetimi oldukça önemli konulardan birisidir. Uygulamaların veya kod süreçlerinin istem dışı sonlanmasının önüne geçilmek istendiği durumlarda, basit try...catch...finally bloklarından yararlanabilir yada Enterprise Library gibi kütüphanelerin sunduğu bloklardan faydalanarak istisna yönetimini üst seviyede sağlayabiliriz.

Bu yazımda çok geniş kapsamda düşünmeyip, PLINQ(Parallel Language INtegrated Query) ifadelerinde oluşabilecek istisnai durumların nasıl ele alınması gerektiği üzerinde durmaya çalışacağız. Olaya hızlı bir giriş yapıp aşağıdaki örnek kod parçasına sahip olduğumuzu düşünelim.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace SequentialPLINQ
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> productList = GetProductList();           
            productList[497].SafetyStockLevel = 0;
            productList[503].SafetyStockLevel = 0;

            var result1 = from product in productList.AsParallel()                         
                          orderby product.ProductId
                          where product.ListPrice>=500
                          select new
                          {
                              product.ProductId,
                              product.Name,
                              product.ListPrice,
                              product.Color,
                              SellPrice=FindSellPrice(product.ListPrice,product.SafetyStockLevel)    // İlk durum                         
                          };


            try
            {
                foreach (var r in result1)
                {
                    Console.WriteLine(r.ProductId + " " + r.Name + " (" + r.SellPrice.ToString("C2") + ")");
                }
            }
            catch (AggregateException excp) // Hata oluştuğunda PLINQ içerisinde başlatılan tüm alt işlemler(Threads) iptal edilir
            {
                // PLINQ ifadeleri çalıştırıldığı sırada oluşan istisnalar AggreateException nesne örneği içerisindeki InnerExceptions özelliğinin refere ettiği koleksiyonda toplanırlar.
                foreach (Exception error in excp.InnerExceptions)
                {
                    Console.WriteLine(error.Message);
                }
            }
        }
           static decimal FindSellPrice(decimal listPrice,int stockLevel)
        {
            decimal result=-1;
                if (DateTime.Now.Day >= 25
                    && DateTime.Now.Day <= 28)
                    result=listPrice - (listPrice * (1 / stockLevel)); // SafetyStockLevel' ın 0 gelmesi halinde exception oluşacak olan yer.
                else
                    result= listPrice * 1.18M;
            return result;
        }

        static List<Product> GetProductList()
        {
            List<Product> productList = new List<Product>();

            SqlConnection conn = new SqlConnection("data source=Manchester;database=AdventureWorks2008;integrated security=true");
            SqlCommand cmd = new SqlCommand("Select ProductId,Name,ListPrice,ProductNumber,Color,SafetyStockLevel From Production.Product", conn);
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            while (reader.Read())
            {
                productList.Add(new Product
                {
                    ProductId = Convert.ToInt32(reader[0]),
                    Name = reader[1].ToString(),
                    ListPrice = Convert.ToDecimal(reader[2]),
                    ProductNumber = reader[3].ToString(),
                    Color = reader[4].ToString(),
                    SafetyStockLevel = Convert.ToInt32(reader[5])
                }
                );
            }
            reader.Close();
            return productList;
        }
    }

    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public string ProductNumber { get; set; }
        public string Color { get; set; }
        public int SafetyStockLevel { get; set; }
    }
}

Visual Studio 2010 Professional Beta 1 ile geliştirilen bu kod parçasında, Product isimli bir sınıftan yararlanılmaktadır. Product tipine ait veriler, SQL sunucusu üzerindeki Product tablosundan alındıktan sonra, generic List<Product> koleksiyonu içerisinde tutulmakta ve sonrasında ise paralel sorgulamaya tabi tutulmaktadır. Burada ayrıca dikkat edilmesi gereken bir noktada, sorgulama sırasında FindSellPrice isimli metodun çağırılması ve parametre olarak, sorgunun t anındaki Product nesnesine ait ListPrice ile StockLevel değerlerinin gönderilmesidir. Dikkat edileceği üzere FindSellPrice metodu içerisinde güne göre ürünlerde indirim uygulanmasını hedef alan bir formül yer almaktadır. Formülü tamamen kafadan uydurduğumu ifirat etmek isterim. Zaten sizde bunu anlamışsınızdır. Wink Asıl varmak istediğim nokta, StockLevel değerlerinin 497 ve 503 nolu ürünler için bilinçli olarak sıfıra set edilmiş olmasıdır. Bu nedenle bölme işlemi sırasında bir istisna oluşması kaçınılmazdır.

(Yani kendi kendimize kaşınıp kod içerisine bir bubi tuzağı koymuş durumdayız. 

Önemli olan nokta, paralel sorgu motorunun bu tip bir durum ile karşılaştığında ne yapacağıdır. Nitekim söz konusu sorgulama tekniğine göre, operasyon bir kaç parça Thread' e bölünmete ve bu nedenle oluşacak bir istisnada(veya istisnalarda) çalışan iş parçalarına ne olacağı sorusu akla gelmektedir. İşte kodun yukarıdaki halinin çalışması sonrası ekran görüntümüz.

Görüldüğü gibi hiç bir Product bilgisi ekrana çıktı olarak gelmemiştir. Bu son derece doğaldır. Nitekim paralel sorgulama motoru herhangibir istisna ile karşılaştığında, çalışmakta olan tüm Thread' lerin iptal edilmesini sağlamaktadır. Diğer yandan istisna nesnesinin tipine dikkat edilmelidir. PLINQ ifadeleri içerisinde meydana gelebilecek istisnalar, AggregateException istisna sınıfı tarafından sarmalanmaktadır. Birden fazla istisna olabileceğinden, AggregateException sınıfı InnerExceptions isimli birde özelliğe sahiptir. Dolayısıyla catch bloğu içerisinde, sıfıra bölem hatasının yakalanması için, InnerExceptions koleksiyonunda dolaşılması gerekemtekdir. (InnerExceptions özelliği ReadOnlyCollection<Exception> tipinden bir koleksiyon döndürmektedir.)

Peki ya, istisna olan parçaların atlanması(bu örneğe göre) istenirse. Bir başka deyişle istisna almayan parçaların yinede paralel sorgulama sonucu ele alınması istenirse ne yapabiliriz?

Bu sorunun cevabı son derece basittir aslında. Exception yönetimi, FindSellPrice isimli metod içerisinde gerçekleştirilir.

static decimal FindSellPrice(decimal listPrice,int stockLevel)
{
    decimal result=-1;
    try 
    {
    if (DateTime.Now.Day >= 25 && DateTime.Now.Day <= 28)
       result=listPrice - (listPrice * (1 / stockLevel)); // SafetyStockLevel' ın 0 gelmesi halinde exception oluşacak olan yer.
    else
       result= listPrice * 1.18M;
    }
    catch(DivideByZeroException excp)
    {
      Console.WriteLine("\tStok miktarı 0 olduğundan satış fiyatı hesaplanamadı");
    }
    return result;
}

Bu durumda uygulama başarılı bir şekilde çalışacak ve aşağıdaki ekran görüntüsüne benzer sonuçlar alınacaktır.

Görüldüğü gibi istisnaya neden olan ürünler kontrollü bir şekilde elenmiş ve sorgunun paralel olarak yürütülmesine devam edilebilmiştir. Böylece geldik bir yazımızın daha sonuna. Bu yazımızda PLINQ sorgularında, istisna yönetiminde nelere dikkat etmemiz gerektiğine değinmeye çalıştık. Tekrardan görüşünceye dek hepinize mutlu günler dilerim. 

ExceptionHandling.rar (27,33 kb)

Tags:   ,
Categories:   LINQ | PLINQ
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

PLINQ - Paralellik Altında Ardışık(Sequential) Çalışmak [Beta 1]

Pazartesi, 25 Mayıs 2009 23:34 by bsenyurt

Merhaba Arkadaşlar,

Bir önceki blog yazımızda PLINQ ifadelerinde sıralama konusuna değinmeye çalışmıştık. Bu yazımızda ise, paralel olarak çalıştırılan LINQ sorguları içerisinde, ardışık(Sequential) olarak nasıl işlem yapılabileceğini incelemeye çalışacağız.

PLINQ ifadeleri, sorgu içerisindeki işlemleri paralel çalışan görevlere ayırmakta son derece başarılıdır. Ancak öyle senaryolar olabilirki, sorgunun belirli bir noktasından(noktalarından) sonra ardışık olarak işlemlerin devam etmesi istenebilir.(Hatta sonra tekrardan paralel olarak devam edilmeside sağlanabilir) Tabi bu şekilde anlatmaya çalışınca inanın benim kafamda karışıyor. Undecided Gelin olayı basit bir örnek ile ele almaya çalışalım. İşte Visual Studio 2010 Professional Beta 1 üzerinde geliştirdiğim Console uygulamasına ait kodlar.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace SequentialPLINQ
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> productList = GetProductList();

            int tid= 0;
            var result1 = from product in productList.AsParallel()
                          where product.Color.StartsWith("B")
                          orderby product.ProductId
                          select new
                          {
                              Id = tid++,
                              product.ProductId,
                              product.Name,
                              product.ListPrice,
                              product.Color
                          };
                         

            foreach (var r in result1)
            {
                Console.WriteLine(r.Id+" \t"+r.ProductId+" "+r.Name+" "+r.Color);
            }
        }

        static List<Product> GetProductList()
        {
            List<Product> productList = new List<Product>();

            SqlConnection conn = new SqlConnection("data source=Manchester;database=AdventureWorks2008;integrated security=true");
            SqlCommand cmd = new SqlCommand("Select ProductId,Name,ListPrice,ProductNumber,Color,SafetyStockLevel From Production.Product", conn);
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            while (reader.Read())
            {
                productList.Add(new Product
                {
                    ProductId=Convert.ToInt32(reader[0]),
                    Name=reader[1].ToString(),
                    ListPrice=Convert.ToDecimal(reader[2]),
                    ProductNumber=reader[3].ToString(),
                    Color=reader[4].ToString(),
                    SafetyStockLevel=Convert.ToInt32(reader[5])
                }
                );
            }
            reader.Close();
            return productList;
        }
    }

    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public string ProductNumber{ get; set; }
        public string Color { get; set; }
        public int SafetyStockLevel { get; set; }
    }
}

Örnekte SQL Server 2008 üzerinde kurulu olan, AdventureWorks2008 veritabanındaki Production şemasında yer alan Product tablosuna ait veriler kullanılmaktadır. Product tablosunun kod içerisindeki temsili için, Product isimli bir sınıf tasarlanmıştır. Sınıfa ait nesne örneklerinden oluşan generic List<Product> koleksiyonunun doldurulması için GetProductList metodundan yararlanılmaktadır. Söz konusu generic liste PLINQ ifadesi yardımıylada sorgulanmaktadır. Buraya kadarki kısımda zaten ilginç bir şey yok. Dikkat edeceğimiz nokta, isimsiz tip(Anonymous Type) içerisinde tid isimli sayacın arttırılmasıdır. Wink Öyleki uygulamayı çalıştırdığımızda aşağıdaki sonuçları elde ederiz.

Burada dikkat edilmesi gereken nokta tid değerlerinin ardışık mantığa göre değil, paralel çalışmanın bir sonucu olarak farklı sıralarda üretilmesidir. Bu nedenle 1, 3, 5, 8, 74... gibi bir dizi oluşmuştur. Bu dizi kodun her çalıştırılmasında farklı şekillerde üretilebilir. Örneği ikinci kez çalıştırdığımda bu kez aşağıdaki sonuçları aldım.

Görüldüğü üzere tid değerlerinin arttırımının, çıktıya yansıyışı farklı olmaktadır. Hatta MSDN kaynaklarında, arttırımı yapılan değerlerin tekrar etmesininde mümkün olabileceği berlitilmektedir. İşte yazmış olduğum bu anlamsız örnekteki gibi (örneğin arttırım işlemlerinin ardışık bir şekilde gerçekleştirilmesinin gerektiği vb...), paralel olarak çalışan LINQ sorguları içerisinde, ardışık çalışması gereken bölümler var ise, AsSequential genişletme metodunun(Extension Method) kullanılması gerekmektedir. Buna göre yukarıdaki örnekte yer alan LINQ sorgusunu,

var result1 = productList
                .AsParallel()
                .Where(p => p.Color.StartsWith("B"))
                .OrderBy(p => p.Name)
                .AsSequential()
                .Select(
                p => new
            {
                Id = tid++,
                p.ProductId,
                p.Name,
                p.ListPrice,
                p.Color
            });

şeklinde değiştirir ve örneği tekrar çalıştırırsak aşağıdaki sonuçları elde ederiz.

Görüldüğü gibi tid arttırımı düzenli bir şekilde çıktıya yansıtılmıştır. (Yinede dikkatlice baktığınızda Name özelliğine göre yapılan sıralamalarda, her iki LINQ sorgusu arasında küçük farklar olabileceğini görebilirsiniz)

Sonuç olarak paralel olarak çalıştırılan LINQ sorguları içerisinde, ardışık yürütülmesi gereken operasyonlar var ise bu durumda AsSequential genişletme metodnun kullanılması gerekmektedir. Diğer taraftan elbetteki bu kullanım, söz konusu paralel çalışmanın hızını düşürecek bir etkiye neden olacaktır. Bu da gözden kaçırılmaması gereken diğer bir noktadır.

Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

SequentialPLINQ.rar (25,80 kb)

Tags:   ,
Categories:   LINQ | PLINQ
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

PLINQ (Parallel LINQ) - Hello World [Beta 1]

Cuma, 22 Mayıs 2009 07:11 by bsenyurt

Merhaba Arkadaşlar,

Bildiğiniz gibi son yazımı deniz kenarında bir kafede tatildeyken yazmıştım Wink Ama tatil bitti malesef ve tekrardan Morpheus' un sözleri kulaklarımda çınladı "Wellcome to the real world". Cry Yinede 1 haftalığınada olsa tatil yapabildiğime şükrediyorum. Gerçek dünyaya döndükten sonra tabiki bir süre adaptasyon sorunları ile karşılaşıyor insan doğal olaraktan. Bu adaptasyon sorunları içerisinde boğuşurken, neleri araştırabilirim diye düşünürken buluverdim kendimi. Herşeyden önce .Net Framework 4.0 ve Visual Studio 2010 Beta 1 sürümlerinin yayınlandığını hepimiz biliyoruz. Dolayısıyla odaklanılacak konu zaten ap açık ortadaydı. .Net Framework 4.0 içerisinde entegre olarak gelen bir yenilik hemen ilgi odağım oldu. PLINQ(Parallel Language INtegrated Query). Aslında PLINQ yeni çıkmış bir eklenti değil. Zaten uzun süredir .Net Framework 3.5 ve Visual Studio 2008 üzerinde CTP sürümü ile testlerimizi yapabiliyorduk. Ne varki, .Net Framework 4.0 göz önüne alındığında PLINQ ile ilişkili tiplerin System.Core.dll assembly' ının 4.0 versiyonu içerisine doğrudan ilave edildiğini görüyoruz. Aşağıdaki Visual Studio 2010 Object Browser' dan alınan görüntüde bu durum açık bir şekilde gözlemlenebiliyor.

Tabiki öncelikli olarak PLINQ kavramından biraz bahsetmemizde yarar var. PLINQ aslında, Microsfot Research ve CLR(Common Language Runtime) takımları tarafından ortaklaşa geliştirilen Parallel Extensions isimli genişletmelerin sadece bir paçasıdır. Diğer parça ise TBL(Task Parallel Library) dir.(Bunu ilerleyen yazılarımda ele almaya çalışacağım) Her iki yapının kullanım amacı, Yönetimli Kod(Managed Code) tarafındaki eş zamanlı işleyişlerin kolay bir şekilde sağlanmasıdır. Söz konusu yapı PLINQ olunca haliyle, LINQ sorgularının kendi içerisine parçalanarak farklı thread' lerde çalışması ve bu parçaların paralel yürüyerek sonuçların elde edilmesi akla gelmektedir. Gerçektende PLINQ yapısının temel amacı bu şekilde özetlenebilir. Hatta PLINQ için Eş Zamanlı Sorgu Yürütme Motorudur(Concurrency Query Execution Engine) diyebiliriz. PLINQ temel olarak LINQ to XML ve LINQ to Objects gibi uygulama alanları üzerinde etkin bir şekilde kullanılabilmektedir.

NOT : Unutulmaması gereken noktalardan biriside, PLINQ ifadelerinin aslında çift çekirdek ve üstü işlemcilerin yada birden fazla işlemcinin olduğu sistemlerde anlamlı olmasıdır. Nitekim, PLINQ motoru, çalışmakta olan sorgu sürecini, makinenin sahip olduğu çekirdek sayısına göre parçalara ayırır ve yürütür. Bu özellikle büyük çaplı projeler göz önüne alındığında, şirketin sahip olduğu kaç bilgisayar var ise hepsini en azından çift çekirdekli olacak şekilde yenilemek gibi bir maliyet anlamına da gelmemelidir. Nitekim bazı istemci-sunucu mimarilerinde, sunucu tarafında çalışmakta olan pek çok LINQ sorgusu, PLINQ motoru kullanılaraktan daha efektif hale getirilebilir. Bir başka deyişle, istemciler birden fazla çekirdekli işlemcilere sahip olmasalarda, mümkün mertebe LINQ ifadelerini içeren iş  mantıklarının, sunucu tarafında olduğu senaryolarda PLINQ büyük avantajlar sağlayabilir(Çok kısa bir süre önce çalışmakta olduğum bir projede yer alan test makinesinin, 8 işlemcili olduğunu hatırlıyorum Laughing )

Tabi burada var olan nesneler üzerindeki LINQ sorgularının paralel olarak çalıştırılması için, Select, Where gibi genişletme metodlarının(Extension Methods) çalışma sırasında işi farklı parçalara bölebilecek versiyonlarının olması gerektiği düşünülebilir. İşte bu noktada devreye, System.Core assembly' ının 4.0 versiyonu içerisinde yer alan ve System.Linq isim alanında bulunan ParallelEnumerable adlı static sınıf girmektedir.

Bu sınıftaki en önemli genişletme AsParallel isimli fonksiyondur. Bu metodun görevi, IEnumerable türevli bir koleksiyonun paralel olarak sorgulanabilir hale getirilmesi veya hazırlanmasıdır. Öyleki, metod geriye ParallelQuery isimli sınıfa ait bir nesne örneği döndürmektedir. ParallelQuery sınıfı IEnumerable arayüzünü uygulamaktadır ama herşeyden önemlisi paralel sorgulanabilme için gerekli ön hazırlıkları içeren operasyonlarada sahiptir.

Bu teknik detaylar eminimki bir Hello World yazısında sizede sıkıcı gelmiştir. Hiç vakit kaybetmeden basit bir örnek ile ilerlemekte yarar olduğunu düşünmekteyim. Aşağıdaki kod parçası Visual Studio 2010 Beta 1 sürümünde yazılmış basit bir Console uygulamasına aittir.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> products = FillProducts();
            Console.WriteLine("Liste dolduruldu. İşlemlere devam etmek için tıklayın");
            Console.ReadLine();

            Stopwatch watch = Stopwatch.StartNew();

            var result1 = from p in products
                          where p.ListPrice >= 10 && p.InStock == true
                          orderby p.Name descending
                          select p;
            Console.WriteLine("Toplam {0} adet ürün bulundu",result1.ToList().Count.ToString());

            Console.WriteLine("Toplam süre {0}",watch.ElapsedMilliseconds.ToString());
            Console.WriteLine("Parallel Olduğunda");

            Stopwatch watch2 = Stopwatch.StartNew();

            var result2 = from p in products.AsParallel()
                          where p.ListPrice >= 10 && p.InStock==true
                          orderby p.Name descending
                          select p;
            Console.WriteLine("Toplam {0} adet ürün bulundu", result2.ToList().Count.ToString());

            Console.WriteLine("Toplam süre {0}", watch2.ElapsedMilliseconds.ToString());
        }

        static List<Product> FillProducts()
        {
            List<Product> products = new List<Product>();

            for (long i = 1; i < 1750000; i++)
            {
                Product prd = new Product {
                    Id = i
                    , Name = "Product" + i.ToString()
                    , ListPrice = i * 0.1M
                    , InStock=i%2==0?true:false
                };
                products.Add(prd);
            }

            return products;
        }
    }

   
    class Product
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
        public bool InStock { get; set; }
    }
}

Uygulama içerisinde Products isimli bir sınıf ve bu tipe ait nesne örneklerinden oluşan bir koleksiyon veri kaynağı olarak kullanılmaktadır. Dikkat edileceği üzere, iki adet LINQ sorgusu bulunmaktadır. Product tipinden olan generic List koleksiyonu, FillProducts metodu yardımıyla tamamen hayali veriler ile doldurulmuştur. Her iki sorguda ListPrice değeri 10' un üzerinde olan ve stokta bulunan ürünleri, adlarına göre ters sırada döndürmektedir. Ancak önemli olan nokta ikinci LINQ ifadesinde AsParallel metodunun kullanılmasıdır. Bu örnek kod parçasını çalıştırdığımda, aşağıdaki ekran görüntüsünde yer alan sonuçları aldım. 

Hemen şunu belirteyim. Programı yazdığım makinede çift çekirdekli Intel işlemci ve 4 Gb Ram bulunmakta. İşletim sistemi olarakta Windows Vista Enterprise yer alıyor. Tabi bu örnek için Intel tabanlı işlemcinin daha büyük önem taşıdığını hemen söyleyebiliriz. Çalışma zamanındanda görüldüğü gibi, paralel olarak yürütülen LINQ ifadesi neredeyse %50 daha az zamanda tamamlanmıştır. (Aslında bu kod parçasını 4 çekirdekli bir işlemcide test etmeyi çok istiyorum. Bu konuda siz değerli okurlarımın yorumlarını ve test sonuçlarını bekliyor olacağım Wink )

Uygulama çalışırken Task Manager aracı ile CPU kullanım durumuna baktığımda ise aşağıdaki sonuçlar ile karşılaştım.

Bu ekran görüntüsünde yer alan sonuçlar tam anlamıyla durmun net analizi olmasada bir parça olsun fikir vermektedir. Yuvarlak içerisine aldığımda kısımlar, sorgunun PLINQ motoru tarafından ele alınmaya başladığı yerlerdeki ölçüm değerleridir. Dikkat edileceği üzere CPU çekirdeklerinin kullanım değerleri %100' e vurmuş durumdadır ki buda aslında, sorgunun çalıştırılması sırasında tüm işlemci gücünün kullanıldığı anlamınada gelmektedir. (Aslında Einstein' ın kuramına göre bu durum göreceli olarak iyi sayılabilir. Ama sayılmayadabilir Undecided )

Yazdığım örnek kod parçasında işlemleri gerçekten yavaşlatmak adına bir sıralama işlemide kullandım. Anacak bunun yapılması zorunlu değildir. Özellikle sıralama kullanılmadığında sorgu çalıştırma sürelerinin birbirlerine çok yakın olduğunu gördüm. Açıkçası, PLINQ' in avantajı gerçekten çok uzun sürebilecek sorgular söz konusu olduğunda ortaya çıkmata. Bu nedenle her LINQ sorgusunun PLINQ formatına dönüştürülmesininde anlamlı olmadığını(olmayacağını) söyleyebiliriz. Nitekim, bazı durumlarda herşey tersine dönebilir. Örneğin aşağıdaki kod parçasını göz önüne alalım.

int[] values = new int[100];Random rnd = new Random();
for (int i = 1; i < values.Length; i++)
{
 values[i] = rnd.Next(1, 1000);
}

Stopwatch watch3 = Stopwatch.StartNew();
var result3 = from value in values
              where value % 2 == 0
              select value;
Console.WriteLine("Toplam {0} çift sayı var",result3.ToList().Count.ToString());
Console.WriteLine(watch3.ElapsedMilliseconds.ToString());

Stopwatch watch4 = Stopwatch.StartNew();
var result4 = from value in values.AsParallel()
              where value % 2 == 0
              select value;
Console.WriteLine("Toplam {0} çift sayı var", result4.ToList().Count.ToString());
Console.WriteLine(watch4.ElapsedMilliseconds.ToString());

Bu kod parçasındaki LINQ sorgularında, 100 tane raslantısal ve 1 ile 1000 arasında olan tamsayı değerlerinden oluşan bir dizi içerisinde kaç çift sayı olduğu tespit edilmektedir. İkinci sorgu, PLINQ motoru tarafından ele alınmaktadır. PLINQ' in, sorguyu paralel olan iş parçalarına bölerek çalıştırdığı düşünüldüğünde, ikinci ifadenin birincisine göre çok daha hızlı çalışması gerektiği tahmin edilebilir. Ama gerçekten böylemi olacaktır. İşte sonuçlar...

Aslında bu sonuç son derece doğaldır. PLINQ motoru çalışma zamanında, çekirdeklere bölünecek işler için hazırlıklar yapmalıdır, thread' leri ayarlamalıdır vb... Bu ön hazırlıklar nedeni ile zaten sorgunun sürece girmesi başlı başına bir zaman kaybı anlamına gelmektedir. Bu örnek en basit anlamda, PLINQ' in her LINQ ifadesi için ele alınmaması gerektiğinide göstermektedir.

Böylece geldik bir yazımızın daha sonuna. Tatil dönüşü sonrası üzerimdeki adaptasyon bıkkınlığını hafifleten bu yazımda sizlere, .Net Framework 4.0 içerisinde artık standart olarak yer alan ve Parallel Extension mimarisinin bir parçası olan PLINQ konusunu anlatmaya çalıştım. Elbetteki PLINQ içerisindede çok daha fazlası var. Bunlarıda ilerleyen yazılarımda aktarmaya çalışıyor olacağım. Sizlerde bu adresten Parallel Extension ile ilişkili son bilgileri alabilirsiniz. Hatta şu saatlerde VS 2010 ile gelen yeniliklerde anlatılmakta. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

HelloWorld.rar (21,81 kb)

Tags:   ,
Categories:   LINQ | PLINQ
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

LINQ Maceralarım

Perşembe, 10 Nisan 2008 01:36 by bsenyurt

Language INtegrated Query(LINQ) mimarisi sayesinde CLR nesneleri(Common Language Runtime Objects) üzerinden SQL tarzı sorgu ifadeleri yazılabilmektedir. Hatta LINQ mimarisi, SQL veritabanı(LINQ to SQL) ve XML (LINQ to XML) kaynakları üzerindede kullanılabilmektedir. Özellikle IEnumerable<T> arayüzünü uyarlayan tiplere ait nesne örnekleri için, Select, Where, GroupBy, Sum, Avg, Distinct ve daha pek çok bilinen sorgulama metodu uygulanabilmektedir. LINQ içerisinde yer alan imkanlar göz önüne alındığında, .Net Framework 1.1, 2.0 ve 3.0 ile geliştirilmiş pek çok projenin .Net 3.5' e aktarılarak bu olanaklardan yararlanabilmeleri için gerekli geçiş hazırlıklarının ciddi anlamda düşünüldüğüde ortadadır. Üstelik Visual Studio 2008, getirdiği çoklu framework desteği sayesinde .Net Framework 2.0, 3.0 ve 3.5 arasındaki geçişlerin kolayca yapılabilmesini sağlamaktadır. Bu gibi konular göz önüne alındığında bir geliştirici olarak LINQ' in daha önceki kod parçalarında kullanılabileceği yeni yerlerde merak konusu haline gelmektedir. İşte bu makalemizde, LINQ sorgularını farklı kod parçalarında kullanmaya çalışıyor olacağız.

LINQ mimarisinin kullanılabileceği alanlar göz önüne alındığında, Reflection(Yansıma), IO(Dosya giriş/çıkış), Bağlantısız Katman(Disconnected Layer) sadece bir kaç basit alan olarak ön plana çıkmaktadır. Ancak bu alanlar pek çok uygulamada önemli görevler üstlenmektedir. Söz gelimi yansıma teknikleri ile IDE geliştirilmesi(Visual Studio benzeri), Plug-In tabanlı uygulamalar yazılması, nitelik(Attribute) bazlı olacak şekilde çalışma ortamının organize edilmesi(özellikle deklerafit programlama tekniklerinde) gibi işlevsellikler ön plana çıkmaktadır. Dosyalama işlemleri en basit anlamda resim işleme programlarından, XML ayrıştırma uygulamalarına kadar pek çok alanda kullanılmaktadır. Çok doğal olarak Ado.Net mimarisine göre geliştirilen pek çok uygulamada bağlantısız katman nesneleri görülebilmektedir. Sadece bu konular bile göz önüne alındığında bazı kod ihtiyaçları için diziler, döngüler ve koşullu ifadelerin çok sık kullanıldığıda göze çarpmaktadır. Ancak LINQ sorguları sayesinde bu işlemler çok daha basit bir şekilde gerçekleştirilebilir. Elbetteki genişletme metodlarının üstlendiği yük çerçevesinde söz konusu döngülerin, koşullu ifadelerin ortadan kalkması gibi bir durum mümkün değildir. Ancak kodun çok daha etkin bir şekilde ve bir sorgulama diline yatkın olaraktan geliştirilmesi önemli bir avantajdır.

Not : Bilindiği gibi LINQ sorgularında yer alan anahtar kelimeler(keywords) aslında arka planda birer genişletme metoduna(Extension Methods) karşılık gelmektedir. Bu metodlar söz gelimi basit bir arama işlemi için gereken döngüsel veya koşullu ifadeleri kapsülleyerek geliştiricinin üzerinden almaktadır. Bu sayede geliştirici SQL diline yatkın bir şekilde sorgular yazabilmekte ve kodun daha etkili, ölçeklenebilir, anlaşılır bir şekilde geliştirilmesine odaklanabilmektedir.

Dilerseniz hiç vakit kaybetmeden örneklerimize başlayalım. İlk olarak dosyalama işlemlerini göz önüne alarak ilerleyebiliriz. Söz gelimi, herhangibir klasör içerisinde yer alan Jpg uzantılı dosyalardan boyutu 1000 kb üzerinde olanların tespit edilmesini istediğimizi düşünelim. Bu işlemi VS 2008 tabanlı bir Console Uygulamasında aşağıdaki kod parçası ile gerçekleştirebiliriz.

string klasorAdresi= @"C:\Documents and Settings\BurakSenyurt\My Documents\My Pictures\Google Pictures\";
DirectoryInfo dInfo = new DirectoryInfo(klasorAdresi);
var resimDosyalari = from fInfo in dInfo.GetFiles()
                                    where fInfo.Extension == ".jpg" && fInfo.Length >= 1000 * 1024
                                        select new
                                                    {
                                                        fInfo.Name
                                                        ,fInfo.Length
                                                        ,fInfo.CreationTime
                                                    };
foreach (var dosya in resimDosyalari)
    Console.WriteLine(dosya);

DirectoryInfo sınıfının GetFiles metodu FileInfo tipinden bir dizi döndürmektedir. Bu dizi bir Array tipi olduğu için LINQ ile birlikte gelen genişletme metodlarını(Extension Methods) kullanabilmektedir. Dolayısıyla LINQ ifadesi içerisinde from, select, where gibi anahtar kelimeler kolay bir şekilde ele alınabilmektedir. FileInfo dizisi üzerinden dosya uzantısı(Extension) .jpg ve uzunluğu(Length) 1000 Kb üzerinde olanlar tespit edilirken aynı zamanda isimsiz bir tip(Anonymous Type) üretimide gerçekleştirilmekte ve ilgili dosya için ad(Name), uzunluk(Length) ve oluşturulma zamanı(CreationTime) bilgilerinin yer aldığı yeni bir nesne örneği oluşturulmaktadır. Program kodunun çıktısı örnek klasör için aşağıdaki gibidir.

 

Eğlenceli değil mi? Öyleyse devam edelim. Diyelimki dosyalama işlemleri ile ilgili olaraktan şöyle bir ihtiyacımız oldu;Bir klasör içerisindeki dosyaları tiplerine göre gruplayıp, her grup içerisinde kaçar adet dosya bulunduğunu öğrenmek istiyoruz. Bu kodun LINQ ifadesini yazmadan önce, LINQ olmadan nasıl geliştirilebileceğini düşünmenizi öneririm. LINQ ile bu sorgu aşağıdaki kod parçasında olduğu gibi gerçekleştirilebilir.

string adres = @"C:\Windows\";
DirectoryInfo dInfo = new DirectoryInfo(adres);
var dosyaGruplari = from fInfo in dInfo.GetFiles()
                                    group fInfo by fInfo.Extension into grp
                                        select new
                                                    {
                                                        Uzanti = grp.Key,
                                                        Toplam = grp.Count()
                                                    };

foreach (var dosyaGrubu in dosyaGruplari)
    Console.WriteLine(dosyaGrubu.ToString());

Bu seferki LINQ ifadesinde group by kullanımı söz konusudur. Group By sayesinde aynen SQL' de olduğu gibi gruplama işlemi nesneler üzerinde yapılabilmektedir. Örnekte Windows klasörü altındaki dosyalar FileInfo tipinin Extension özelliğine göre gruplanmaktadır. Sonrasında ise gruplanan koleksiyon üzerinden Count genişletme metodu kullanılmakta ve her bir tip grubu için kaçar dosya olduğu hesaplanmaktadır. İlk örnekte olduğu gibi yine isimsiz tip(Anonymous Type) kullanılarak dosya grubuna ait uzantı ve toplam dosya sayısı bilgileri elde edilmektedir. Sonuç olarak uygulamanın ekran çıktısı aşağıdaki gibi olacaktır.

Şimdide herhangibir klasördeki jpg uzantılı dosyalardan L harfi ile başlayanları boyutlarına göre tersten sıralayarak elde etmek istediğimizi düşünelim. LINQ kullanmadığımız takdirde bize en çok sorun çıkartacak noktalardan biriside tersten sıralama işlemi olacaktır. Bunu sağlamak için doğal olarak FileInfo dizisi üzerinden ters sıralama algoritması uygulanması gerekir. Oysaki LINQ ifadeleri ile bu işlem için gerekli kod parçası aşağıdaki gibi kolayca geliştirilebilir.

string klasorAdresi = @"C:\Documents and Settings\BurakSenyurt\My Documents\My Pictures\Google Pictures\";
DirectoryInfo dInfo = new DirectoryInfo(klasorAdresi);

var dosyalar=from fInfo in dInfo.GetFiles()
                        where fInfo.Extension==".jpg" && fInfo.Name[0]=='L'
                            orderby fInfo.Length descending
                                select new
                                                {
                                                    fInfo.Name,
                                                    fInfo.Length
                                                };

foreach (var dosya in dosyalar)
    Console.WriteLine("{0} \t{1}",dosya.Length,dosya.Name);

Bu kez orderby anahtar kelimesi(ki bu arka planda OrderBy genişletme metoduna dönüştürülmektedir) kullanılarak dosyaların boyutlarına göre tersten sıralanması sağlanmıştır. Sonuç olarak kodun ekran çıktısı aşağıdakine benzer olacaktır.

LINQ sorguları dosyalama işlemleri dışında özellikle reflection(yansıma) tarafındada etkili bir şekilde kullanılabilir. Yazımızın bundan sonraki kısmındada yansıma teknikleri içerisinde LINQ ifadelerini örnekler üzerinde ele almaya çalışacağız. Öncelikli olarak Process' lerden başlamak taraftarıyım. Bilindiği üzere .Net uygulamaları sistem üzerinde açılan Process' ler içerisinde ayrı uygulama alanları(Application Domains) altına dahil edilirler. Hatta bu uygulama alanları kendi içlerinde, birden fazla(en az bir tane olmak üzere) Thread' ede sahip olabilirler. Sistem üzerinde çalışan Process' lerin yada o anda çalışmakta olan güncel Process' in bilgilerini almak için Process sınıfının farklı metodları bulunmaktadır. Bizimde aklımıza gelen soru şudur; acaba sistem üzerinde çalışmakta olan Process' ler içerisinde sadece tek bir Thread' e sahip olanlar hangileridir. Nitekim bilindiği üzere bazı Process' ler kendi içlerinde birden fazla Thread içermektedir. Bu amaçla aşağıdaki gibi bir kod parçası geliştirilebilir.

var processes = from prc in Process.GetProcesses()
                            where prc.Threads.Count == 1
                                orderby prc.ProcessName descending
                                    select new
                                                {
                                                    prc.ProcessName
                                                    , prc.PagedMemorySize64
                                                };
foreach (var process in processes)
    Console.WriteLine(process.ToString());

Process sınıfının static GetProcesses metodu ile o anda sistemde çalışmakta olan Process' ler elde edilmektedir. Sonrasında where anahtar kelimesi ile Threads özelliği üzerinden Count değeri kontrol edilir. 1 olanlar adlarına(ProcessName) göre orderby anahtar kelimesinden yararlanılarak tersten sıralanacak şekilde yeni bir isimsiz tip içerisinde toplanırlar. Bu isimsiz tip(Anonymous Type) örnek olarak Process' in adı(ProcessName) ve sayfalanmış bellek boyutu(PagedMemorySize64) değerlerini içermektedir. Sonuç olarak kodun çıktısı, çalışılan sistem üzerinde aşağıdaki gibi olmuştur.

Reflection ile başlamışken hızımızı kesmeyelim ve yeni bir sorgu ile devam edelim. Bu kez şöyle bir ihtiyacımız var; bir assembly' ın referans ettiği assmebly' lar içerisinden versiyonu .Net Framework 2.0 olmayanları bulmak istiyoruz. Bu tip bir durumda var olan Assembly' ın yüklenmesi ve referans ettiği Assembly' ların GetReferencedAssemblies metodu ile çekilmesi gerekir. Ne tesadüftürki GetReferencedAssemblies metodu AssemblyName tipinden bir dizi döndürmektedir. Dolayısıyla bu dizi üzerinden LINQ ifadeleri kullanılabilmesi olasıdır. Söz konusu ihtiyaç için aşağıdaki gibi bir kod parçası düşünülebilir.

AssemblyName[] result1 = Assembly.LoadFrom(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.EnterpriseServices.dll")
.GetReferencedAssemblies();
var framework2Olmayanlar = from asmb in result1
                                                where asmb.Version != new Version(2, 0, 0, 0)
                                                    select new
                                                                {
                                                                    AssemblyAdi = asmb.FullName
                                                                    ,IslemciMimarisi = asmb.ProcessorArchitecture
                                                                    ,HashAlgoritması = asmb.HashAlgorithm
                                                                };

foreach (var a in framework2Olmayanlar)
    Console.WriteLine(a.ToString());

Örnek olarak System.EnterpriseServices.dll assembly' ı kullanılmaktadır. Sorgu içerisinde dikkat edilecek olursa GetReferencedAssemblies metodu ile elde edilen sonuç kümesi üzerinden çekilen her bir AssemblyName nesnesinin Version özelliğine bakılmaktadır. Sonrasında ise yine bir isimsiz tip kullanılarak sadece Assembly' ın adı(FullName), işlemci mimarisi(ProcessorArchitecture) ve hash algoritması(HashAlgorithm) değerleri toplanmaktadır. Örneğin ekran çıktısı aşağıdaki gibi olacaktır.

LINQ sorguları içerisinde bazı yerlerde harici metodlarında çağırılması mümkündür. Söz gelimi bir koşul kontrolü için iterasyonun o andaki nesnesinin denetlenmesi gerektiği durumlarda harici metod çağrıları gerekebilir. Örneğin dll uzantılı dosyalar ile dolu bir klasör içerisinde .Net Assembly' ı olarak yüklenebilenlerin tespit edilmesini istediğimiz düşünelim. Böyle bir senaryoda Assembly sınıfının static LoadFrom metodu oldukça işe yarayacaktır. Nitekim söz konusu dll herhangibir nedenle yüklenebilen bir Assembly değilse çalışma zamanı istisnası(Runtime Exception) oluşacaktır. Aşağıdaki kod parçası bu durumu analiz etmek için geliştirilmiştir.

class Program
{
    static void Main(string[] args)
    {
        string path = @"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727";
        DirectoryInfo klasor = new DirectoryInfo(path);
        var assemblyOlanlar = from dosya in klasor.GetFiles("*.dll")
                                            where Yuklenebildinmi(dosya.FullName)
                                                select dosya;

        foreach (var asmb in assemblyOlanlar)
            Console.WriteLine(asmb.FullName);
    }

    static bool Yuklenebildinmi(string assemblyAdresi)
    {
        try
        {
            Assembly asmbly = Assembly.LoadFrom(assemblyAdresi);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

GetFiles metodu ile dll uzantılı FileInfo dizisi elde edildikten sonra her bir eleman için Yuklenebildimi isimli bir metod ile denetleme işlemi gerçekleştirilmektedir. Yuklenebildimi isimli fonksiyon, Assembly.LoadFrom metodu işe yarıyorsa true değerini, yaramıyorsa false değerini döndürmektedir. Buna göre true değeri dönen dosyaların yüklenebilen assembly' lar olduğu sonucuna varılmaktadır. Uygulamanın çalışma zamanındaki görüntüsü aşağıdakine benzer olacaktır.

Yine assembly' lar üzerinden LINQ sorguları yazmaya devam edelim. Örneğin bir assembly içerisinden dışarıya sunulan harici tipler göz önüne alınsın. Burada işin içerisine gruplama fonksiyonelliğinide katarak, hangi isim alanı(namespace) içerisinden kaç adet tipin dışarıya sunulduğu bilgiside elde edilebilir. Bu işi gerçekleştirmek için örnek olarak aşağıdaki gibi bir kod parçası göz önüne alınabilir.

Assembly systemAsmb = Assembly.LoadFrom(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Web.dll");
var hariciTipler = from t in systemAsmb.GetExportedTypes()
                            group t by t.Namespace into ng
                                orderby ng.Key descending
                                    select new
                                                {
                                                    IsimAlaniAdi = ng.Key,
                                                    TipSayisi = ng.Count()
                                                };
foreach (var hariciTip in hariciTipler)
    Console.WriteLine("{0} isim alanından {1} tip vardır", hariciTip.IsimAlaniAdi, hariciTip.TipSayisi.ToString());

Bu LINQ sorgusunda GetExportedTypes metodu yardımıyla örnek olarak System.Web.dll assembly' ı içerisinden dışarıya sunulmakta olan harici tiplerin listesi Type türünden bir dizi olarak elde edilmektedir. Sonrasında ise her tip, Namespace özelliğinin değerine göre group anahtar kelimesi yardımıyla ng isimli değişken altında gruplanmaktadır. Gruplanan veriler sonucu elde edilen liste Namespace adlarına göre tersten(descending) sıralanmaktadır. Bu noktada devreye orderby anahtar kelimesi girmektedir. Elde edilen listeden isim alanı adları Key özelliği ile ve tip sayılarıda Count genişletme metodu ile çekilerek yeni bir isimsiz tip altında toplanmaktadır. Kod parçasının çalışmasının sonucu oluşan örnek ekran çıktısı ise aşağıdaki gibidir.

Peki herhangibir assembly içerisinde kaç farklı isim alanı olduğunu bulmak istersek. Normal şartlarda bu işlem için isim alanı adlarını çektikten sonra bir fonksiyonellik geliştirilmesi gerekmektedir. Oysaki LINQ ile birlikte genen Distinct genişletme metodu sayesinde söz konusu işlem aşağıdaki kod parçasında olduğu gibi kolayca gerçekleştirilebilir.

Assembly systemAsmb = Assembly.LoadFrom(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll");
var isimAlanlari = (from t in systemAsmb.GetTypes()
                                select t.Namespace).Distinct();
Console.WriteLine("\n{0} assembly' ı içerisinde {1} farklı isim alanı adı vardır", systemAsmb.FullName,isimAlanlari.Count()-1);
foreach (var isimAlani in isimAlanlari)
    Console.WriteLine(isimAlani);

Burada dikkat edilmesi gereken noktalardan biriside Distinct işlevselliğinin bir metod olarak select sorgusunun arkasından kullanılmasıdır. Buna ek olarak Count genişletme metodu ilede farklı isim alanlarının sayısı çekilmektedir. Örnekte yer alan System.Xml.dll assembly' ı için ilgili sonuçlar aşağıdaki gibi olacaktır.

Reflection ile ilişkili olarak LINQ sorgularını kullanacağımız son bir örnek ile devam edelim. Bu sefer bir assembly içerisinde yer alan tiplerin toplam sayılarını türedikleri base type' lara göre gruplayarak elde etmeye çalışıyor olacağız. Bu amaçla, Type sınıfının BaseType özelliği gruplama işleminde kullanılabilir. Söz gelimi System.dll assembly' ı içerisindeki tipleri BaseType özelliklerinin değerlerine göre gruplamak istersek aşağıdaki kod parçası yeterli olacaktır.

Assembly systemAsmb = Assembly.LoadFrom(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll");
var tipler = from m in systemAsmb.GetTypes()
                    group m by m.BaseType into grp
                        select new
                                    {
                                        grp.Key
                                        ,Toplam = grp.Count()
                                    };
foreach (var tip in tipler)
    Console.WriteLine("{0} \t{1}", tip.Key, tip.Toplam.ToString());

Bir önceki örnektekine benzer olacak şekilde yine group by fonksiyonu kullanılmaktadır. Sonuç olarak üretilen isimsiz tip içerisinde BaseType adı ve toplam tip sayısı değerleri yer almaktadır. Örnek kodun çalışma zamanındaki ekran çıktısı aşağıdakine benzer olacaktır.

LINQ(Language INtegrated Query) ifadeleri pek çok dizi tipi ve koleksiyon üzerinde etkin bir şekilde kullanılabildiğinden, akla gelen konulardan biriside görsel uygulamalarda yer alan Controls koleksiyonlarıdır. Bir windows uygulamasında yada web uygulamasında Container görevi üstlenen ve bu sebepten Controls koleksiyonuna sahip olan nesnel topluluklar üzerinde de LINQ sorguları çalıştırılabilir. Bunu basit bir örnek üzerinden inceleyebiliriz. Söz gelimi aşağıdaki ekran görüntüsüde yer alan bir Windows Formumuz olduğunu düşünelim.

Amacımız şimdilik bu form üzerinde hangi tipte kontroller bulunduğunu göstermek. Bu amaçla basit olarak aşağıdaki gibi bir kod parçası yeterli olacaktır.

private void button2_Click(object sender, EventArgs e)
{
    lstSonuclar.Items.Clear();

    IEnumerable<Control> kontroller=Controls.Cast<Control>();

    var farkliTipler = (from kontrol in kontroller
                                select kontrol.GetType()).Distinct().OrderBy(k => k.Name);

    foreach (Type farkliTip in farkliTipler)
        lstSonuclar.Items.Add(farkliTip.Name);
}

Bu kod parçasıda belkide en önemli noktalardan biriside Controls özelliği üzerinden kullanılan Cast<T> genişletme metodudur. Cast<T> metodu kullanılmadığı takdirde Controls özelliği üzerinden Select, Where, GroupBy gibi LINQ sorgularında önem arz eden fonksiyonelliklere erişilemediği görülür. Cast<T> metodunun buradaki görevi Controls koleksiyonu içerisindeki bileşenleri, parametre olarak verilen generic tipe dönüştürerek IEnumerable arayüzünün taşıyabileceği bir nesne topluluğu referansı halinde üretmektir. Böylece LINQ sorguları için gerekli fonksiyonellikler elde edilebilmektedir. Windows formu üzerindeki görsel bileşenler Control sınıfından türemektedir. Bu sebepten Cast<T> metodunun generic parametresi Control tipindendir. Bu dönüştürme işleminin ardından Distinct ve OrderBy genişletme metodlarınında yer aldığı bir LINQ sorgusu çalıştırılması mümkün olmaktadır. Select sorgusunda, GetType metodunun kullanılmasının sebebi tiplerin benzersiz şekilde ele alınmak istemesidir. Sonuç olarak uygulamanın çalışma zamanındaki ekran çıktısı aşağıdakine benzer olacaktır.

Görüldüğü gibi form üzerinde hangi tipten kontrollerin var olduğu listelenmektedir. Yeni bir sorgu ile devam edelim. Bu sefer form üzerindeki kontrolleri tiplerine göre gruplayıp her bir tipten kaç adet olduğunu bulmak istediğimizi düşünelim. Bu basit gruplama işleminin kodu aşağıdaki gibi geliştirilebilir.

private void button2_Click(object sender, EventArgs e)
{
    lstSonuclar.Items.Clear();

    IEnumerable<Control> kontroller=Controls.Cast<Control>();

    var farkliTipler = from kontrol in kontroller
                                group kontrol by kontrol.GetType() into grp
                                    select new
                                                {
                                                    KontrolAdi=grp.Key
                                                    ,Toplam=grp.Count()
                                                };

    foreach (var farkliTip in farkliTipler)
        lstSonuclar.Items.Add(String.Format("{0} : {1}",farkliTip.KontrolAdi,farkliTip.Toplam.ToString()));
}

Bu sefer gruplama işlemi Control tipinin GetType metoduna göre yapılmaktadır. Uygulama kodunun ekran çıktısı aşağıdaki gibi olacaktır.

Cast<T> metodu doğrudan LINQ genişletme metodlarının kullanılamadığı pek çok senaryoda ele alınabilir. Söz gelimi aşağıdaki kod parçası çok basit olarak Application Log altındaki girişlerden programın çalıştırıldığı gün içerisinde üretilenlerin çekilmesini sağlamaktadır.

EventLog logs = new EventLog("Application", ".", "");
IEnumerable<EventLogEntry> entries = logs.Entries.Cast<EventLogEntry>();

var girisler = from entry in entries
                    where entry.TimeGenerated.Day == DateTime.Now.Day
                        select new
                                    {
                                        entry.Category,
                                        entry.CategoryNumber,
                                        entry.EntryType,
                                        entry.TimeGenerated
                                    };

foreach (var giris in girisler)
    Console.WriteLine(giris.ToString());

Bu kod parçasında kullanılan Cast<T> genişletme metodu geriye, IEnumerable<EventLogEntry> arayüzü(interface) tarafından taşınacak bir nesne topluluğu referansı döndürmektedir. IEnumerable<T> arayüzüne ulaşıldığı içinde LINQ sorgusu kolay bir şekilde ele alınmış ve aşağıdaki ekran çıktısının üretilmesi sağlanmıştır.

Cast<T> metodu ile benzer özelliğe sahip bir diğer önemli metodda OfType<T> genişletme metodudur. Bu metod bir nesne topluluğu üzerinde, generic parametre tipine göre filtreleme yapılabilmesini ve geriye LINQ sorgularının uygulanabileceği bir IEnumerable<T> referansı döndürülmesini sağlamaktadır.

Not : Cast<T> metodu ile OfType<T> metodu benzer işlevselliğe sahip görünmekle birlikte arada önemli farklar vardır. OfType<T> metodu temel olarak generic parametre tipine göre bir filtreleme yapmakta iken, Cast<T> metodu generic parametre tipine dönüştürme yapmaktadır. Bu sebepten dönüştürme yapılamayacağı durumlarda Cast<T> metodu, çalışma zamanında InvalidCastException istisnası üretilmesine neden olur. Oysaki OfType<T> metodu bu durumu tamamen görmezden gelir ve diğer nesneden devam eder. OfType<T> kendi içerisinde is anahtar kelimesini kullanarak tip kontrolü yapmaktayken, Cast<T> doğrudan dönüştürme adımını uygular.

Şimdi OfType<T> metodunu örnek bir senaryo üzerinden ele almaya çalışalım. Örneğin uygulamamızda kullandığımız .Net Framework 2.0 ile geliştirilmiş bir kütüphane olsun. Bu kütüphane içerisinde yer alan metodlardan bazılarınında ArrayList gibi tür güvenli olmayan koleksiyonlar döndürdüğünü düşünelim. Referansta bulunan uygulamanın .Net 3.5 tabanlı olduğu düşünülecek olursa, gelen koleksiyon nesneleri üzerinden LINQ sorguları çalıştırılması istenebilir. Bu noktada OfType<T> metodu oldukça işe yarayacaktır. Söz konusu senaryoyo ele almak için aşağıdaki tipi içeren bir sınıf kütüphanesi(Class Library) olduğunu düşünelim.

public class Yardimci
{
    public ArrayList ListeyiAl()
    {
        ArrayList liste = new ArrayList();
        liste.Add("Burak");
        liste.Add("Bili");
        liste.Add("Behçet");
        liste.Add("Necdet");
        liste.Add("Kerim");
        liste.Add("Mayk");
        liste.Add(19.90);
        liste.Add(10);
        liste.Add(true);
        liste.Add(false);
        liste.Add('C');
        return liste;
    }
}

Kod parçasında kasıtlı olarak ArrayList içerisine farklı tipte veriler atılmıştır. Eğerki LINQ sorgusunda bu metoddan dönen değerler içerisinden sadece string tabanlı olanları ele almak istiyorsak, OfType<T> metodunu aşağıdaki kod parçasında olduğu gibi kullanabiliriz.

GenelIslemler.Yardimci yrdm = new GenelIslemler.Yardimci();

var besHarfliler = from nesne in yrdm.ListeyiAl().OfType<string>()
                            where nesne.Length == 5
                                select nesne;

foreach (string nesne in besHarfliler)
    Console.WriteLine(nesne);

OfType<string> metodu buradaki kullanıma göre ListeyiAl fonksiyonundan gelen ArrayList içerisindeki tüm nesnelerde, is kontrolünü yaparak sadece String olanları geriye döndürmektedir. Sonrasında nesnelerin karakter uzunluğu kıyaslanarak 5 ise çekilmektedir. Program kodunun çıktısı aşağıdaki gibi olacaktır.

Yazımızda son olarak .Net Framework 2.0 ile yazılmış ve DataTable nesnelerini kullanan bir uygulamayı .Net 3.5' e taşıyarak basit LINQ sorgularını nasıl ele alabileceğimizi incelemeye çalışacağız. (LINQ sorgularının işlevselliğinin ön plana çıktığı vakalarda, var olan .Net uygulamaları .Net 3.5 versiyonuna terfi edilmek durumdan kalabilir.) Bu amaçla ilk olarak .Net Framework 2.0 ile geliştirilmiş ve test amacıyla aşağıdaki kodlara sahip bir Console uygulamamız olduğunu düşünelim.

using System;
using System.Data;
using System.Data.SqlClient;

namespace Net20DataTable
{
    class Program
    {
        static void Main(string[] args)
        {
            DataTable tbl = null;
   
            using (SqlConnection conn = new SqlConnection("data source=.;database=AdventureWorks;integrated security=SSPI"))
            {
                SqlDataAdapter adapter = new SqlDataAdapter("Select ProductId,Name,ListPrice,Class,SellStartDate,ProductSubCategoryId From Production.Product", conn);
                tbl = new DataTable("Products");
                adapter.Fill(tbl);
            }
        }
    }
}

Kod, AdventureWorks isimli SQL Server 2005 veritabanına bağlanmakta ve Production şemasındaki Product tablosundan bir kaç alanı çekmektedir. Çekilen veri kümesi işlenilmek üzere bir DataTable nesnesi içerisinde toplanmaktadır. Çok doğal olarak uygulama .Net Framework 2.0 tabanlı olduğundan, LINQ ifadelerinin DataTable üzerinden uygulanması(veya başka bağlantısız katman nesneleri üzerinden) mümkün değildir. Eğer elimizde Visual Studio 2008 var ise yapılması gerekenler çok basittir. Öncelikli olarak proje özelliklerinden(Properties) Application sekmesine geçilmeli ve Target Framework seçeneği .Net Framework 3.5 olarak değiştirilmelidir.

Bu işlemin ardından uygulamanın bir kere daha derlenmesinde yarar vardır. (Söz konusu adımların ardından System.Core.dll assembly' ının projeye hemen referans edildiğide görülebilir.) Artık LINQ sorgularının yazılmasına başlanabilir. DataTable için bu sorguların uygulanabilmesi için AsEnumerable metodunun erişilebilir olması gerekmektedir. Ancak bu genişletme metoduna şu anda erişilemediği görülmektedir. Bunun sebebi System.Data.DataSetExtensions.dll assembly' ının projeye referans edilmemiş olmasıdır. Dolayısıyla öncelikle bu assembly' ın referans edilmesi gerekmektedir.

Artık uygulamada yer alan DataTable üzerinde LINQ sorguları çalıştırılabilir. İşte bir örnek;

var altKategorisi4OlanUrunler = from row in tbl.AsEnumerable()
                                                    where row["ProductSubCategoryId"].ToString() == "4"
                                                        select new
                                                                    {
                                                                        Id = Convert.ToInt16(row["ProductId"]),
                                                                        Ad = row["Name"].ToString(),
                                                                        Fiyat = Convert.ToDouble(row["ListPrice"])
                                                                    };

foreach (var urun in altKategorisi4OlanUrunler)
    Console.WriteLine(urun.ToString());

Bu kod parçasında görülen LINQ sorgusuna göre DataTable içerisinden ProductSubCategoryId alanının değeri 4 olanların ProductId,Name,ListPrice kolonlarının verilerinden oluşan yeni bir isimsiz tip(Anonymous Type) topluluğu elde edilmektedir. Kodun ekran çıktısı aşağıdakine benzer olacaktır.

Görüldüğü gibi LINQ sorguları .Net Framework içerisinde pek çok farklı alanda uygulanabilmektedir. Reflection, IO, Windows Forms Controls, Application Log, DataTable, eski bir uygulamadan gelen ArrayList bu yazıda ele alınan basit bir kaç alandır. LINQ sorguları Office ürünlerinde dahi kullanılabilmektedir. Söz gelimi Outlook içerisindeki kontaklar LINQ sorguları ile filtrelenebilir. Örnekleri arttırmak ve yaymak mümkündür. Ancak unutulmaması gereken noktalardan biriside bu işlemlerin yapılması için LINQ sorgularının olmasının zorunlu olmadığıdır. Öyleki LINQ sorgularıda özünde, .Net Framework 3.5 ile gelen genişletme metodlarını(Extension Methods) yoğun bir şekilde ele almaktadır. Bir başka deyişle LINQ olmadanda metodlar yardımıyla bu istekler karşılanabilir. Diğer taraftan LINQ sorgularının getirdiği dil esnekliği, kullanım kolaylığı, anlaşılabilirlik göz ardı edilmemelidir. Her geliştirici kullandığı programlama dili yardımıyla nesneler üzerinden SQL benzeri sorgu ifadeleri yazabilmek ister. LINQ bu imkanı sağlayarak önemli bir açığı kapatmaktadır. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

LINQveReflectionArastirma.rar (97,49 kb)

Tags:   ,
Categories:   C# 3.0 | LINQ
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

LINQ to SQL : Arka Planda Neler Oluyor?

Çarşamba, 19 Aralık 2007 16:43 by bsenyurt

Veritabanı(Database) nesnelerinin programatik ortamda sınıf gibi tipler(Type) ve metod benzeri üyeler(Members) ile ifade ediliyor olması, bu tiplere ait nesne örnekleri üzerinden sorgulalamalar yapılabilmesi ihtiyacınıda ortaya çıkartmıştır. Bir veritabanı nesnesinin programatik taraftaki karşılığının nesne yönelimli(Object Oriented) bir dilde geliştirilmesi son derece kolaydır. Örneğin bir tablo(Table) göz önüne alındığında, bu tablonun kendisi bir sınıf(Class) olarak tasarlanabilir. Benzer şekilde, tablo içerisindeki alanlar(Fields) sınıf içinde yer alan birer özellik(Property) olarak düşünülebilir. Basit CRUD(CreateRetrieveUpdateDelete) işlemleri, varlık sınıfı(Entity Class) diyebileceğimiz tipin birer üye metodu olarak düşünülebilir. Tabloda yer alan kolonların bazı niceleyicileri(örneğin Null değer içerip içermedikleri, primary key olup olmadıkları vb...) sınıfın kendisi ve üyeleri için birer nitelik(Attribute) olarak tasarlanabilir. Ne varki bu eşleştirme kolaylığı dışında, programatik tarafta yer alan nesnel yapılar üzerinde, SQL cümlelerine benzer ifadeler ile sorgulamalar yapmak kolay değildir. Nitekim, programatik tarafın SQL benzeri cümelere karşılık gelen fonksiyonellikleri ele alıyor olması gerekmektedir. LINQ(Language Integrated Query) mimariside, temel anlamda programatik tarafta yazılan ifadeleri arka planda metodlar, temsilciler(Delegates) yardımıyla kurduğu bir modele dönüştürmektedir. LINQ' in kullanıldığı alanlar göz önüne alındığında en popüler seçeneklerden biriside LINQ to SQL mimarisidir.

Language INtegrated Query to SQL mimarisi ile, varlık tipleri(Entity Types) üzerinden sorgular çalıştırılabilir. Basit anlamda, nesneler(Objects) üzerinde uygulanabilen LINQ sorguları, SQL tarafına ulaştıklarında ise bildiğimiz sorgu ifadelerine(Query Expressions) dönüşmektedir. Bilinen LINQ operatörlerinin veya metodlarının tamamının SQL tarafına uygulanamadığı veya henüz uygulanamadığı bir gerçektir. Nitekim programatik ortamın esnekliği nedeni ile, örneğin bir dizinin ters çevrilerek elemanları üzerinde döngüsel anlamda ilerlenebilmesinin, SQL tarafında karşılığının bulunması zordur(ki buda LINQ metodlarından olan Reverse fonksiyonelliğinin neden LINQ to SQL üzerinde kullanılamadığınıda açıklamaktadır).

Bu ana fikirlerden yola çıkarak makalemizdeki ana temamızın, SQL ifadelerine çevrilebilen LINQ operatörlerinin veya fonksiyonelliklerinin, arka planda ne şekilde tasarlandıklarını inceleyebilmektir. Bir başka deyişle basit ve karmaşık LINQ cümlelerinin, SQL tarafında ele alınabilen karşılıklarının ne olduklarını tespit edebilmektir. Bu araştırmadaki en büyük yardımcılarımız ise SQL Server Profiler ve Estimated Execution Plan araçları(Tools) olacaktır. SQL Server Profiler aracı kullanılarak, varlık(Entity) nesneleri üzerinde çalıştırılan LINQ ifadelerinin karşılığı olan SQL sorgu cümlelerini görmek mümkün olabilmektedir. Diğer taraftan Estimated Execution Plan aracı sayesinde, LINQ için arka tarafta çalıştırılan bir sorgu cümlesinin icra planının görülmesi ve alternatif ifadeler ile aralarındaki farklar tespit edilerek daha optimal yolların göz önüne alınması sağlanabilir.

Dilerseniz hiç vakit kaybetmeden örneklerimize geçerek devam edelim. Her zamanki gibi Visual Studio 2008 RTM üzerinden örnek kod parçalarımızı çalıştırıyor olacağız. Basit bir Console uygulaması üzerinden ilerlerken AdventureWorks ve Northwind veritabanlarındaki(Database) bazı tabloları kullanıyor olacağız. Bu anlamda AdventureWorks.dmbl ve Northwind.dmbl isimli DataBase Markup Language içeriklerimiz aşağıdaki ekran görüntülerinde yer aldığı gibi olacaktır.

AdventureWorks.dbml;

AdventureWorks veritabanından örnek sorgulamalar için ProductCategory, ProductSubCategory, Product, SalesPerson ve SalesOrderHeader tabloları ele alınmaktadır.

Northwind.dmbl;

Northwind veritabanından ise Customer ve Supplier tabloları ele alınmaktadır.

İlk olarak aşağıdaki gibi basit bir LINQ ifadesi ile başlayalım.

AdventureWorksDataContext adw = new AdventureWorksDataContext();

var mClassProducts = from prd in adw.Products
                                    where prd.Class == "M"
                                        select prd;

int mCount = mClassProducts.Count();
double mSumListPrice = mClassProducts.Sum<Product>(prd => (double)prd.ListPrice);

Console.WriteLine(mCount.ToString());
Console.WriteLine(mSumListPrice.ToString("C2"));

İlk olarak AdventureWorksDataContext nesnesi örneklenmektedir. Sonrasında mClassProducts isimli alana atanan ifadede AdventureWorksDataContext içerisinde yer alan Products özelliğinin karşılığı olan generic Table<Product> tipi ele alınmaktadır. Buna göre Product nesne örneklerinden, Class özelliklerinin(Properties) değerleri M olanlar seçilmektedir. Daha önceki makalalerdede belirttiğimiz gibi var anahtar kelimesi ile tanımlanmış olan değişkene atanan bu ifade çalışma zamanında hemen yürütülmemektedir. İcra işlemi için bir for iterasyonu olması yada ifade üzerinden örnekteki gibi Aggregate benzeri fonksiyonelliklerin çalıştırılması gerekmektedir. mCount alanının değeri, hazırlanan LINQ ifadesinde Count metodu uygulanarak elde edilmektedir. Bir başka ifadeyle, sınıfı M olan ürünlerin toplam sayısı bulunmaktadır. mSumListPrice alanına atanan değer ilede, sınıfı M olan ürünlerin liste fiyatlarının(ListPrice) toplamı elde edilir. Sonrasında ise bu sonuçlar ekrana yazdırılır. Çalışma zamanında(run time) uygulamanın çıktısı aşağıdaki gibi olacaktır.

Gelelim arka tarafta çalıştırılan SQL sorgu cümlelerine. SQL Server Profiler aracı kullanılarak yapılan çalışmada Count ve Sum aggregate metodlarının aşağıdaki gibi icra edildiği görülmektedir. Count fonksiyonunun çağırılması sonucu çalışan sorgu şu şekildedir;

exec sp_executesql N'SELECT COUNT(*) AS [value]
                                    FROM [Production].[Product] AS [t0]
                                        WHERE [t0].[Class] = @p0',N'@p0 nvarchar(1)',@p0=N'M'

Burada dikkat edilmesi gereken noktalardan birisi Count(*) ifadesidir. Normal şartlar altında tavsiye edilen yöntemlerden birisi Count(ProductID) tarzında bir kullanım yapılması yönündedir. Bu tarz bir kullanımın performans yönünde avantaj sağladığı bilinmektedir. Nitekim LINQ tarafından gelen ifadeye göre Count(*) şeklinde bir SQL fonksiyonu kullanılmıştır. Diğer tarafan LINQ sorgusunun aşağıdaki gibi değiştirilmesi düşünülebilir.

var mClassProducts = from prd in adw.Products
                                    where prd.Class == "M"
                                        select prd.ProductID;

Dikkat edileceği üzere burada sadece ProductID alanları seçilmektedir. Bu ifade üzerinden Count metodu kullanılırsa SQL tarafında icra edilen operasyonun değişmediği, bir başka deyişle Count(*) çağrısı yapıldığı görülür. Sum fonksiyonunun çağırılması sonucu çalışan sorgu ise aşağıdaki gibidir.

exec sp_executesql N'SELECT SUM([t1].[value]) AS [value]
FROM (
    SELECT CONVERT(Float,[t0].[ListPrice]) AS [value], [t0].[Class]
        FROM [Production].[Product] AS [t0]
    ) AS [t1]
WHERE [t1].[Class] = @p0',N'@p0 nvarchar(1)',@p0=N'M'

Dikkat edilecek olursa burada Sum işlemi için bir iç Select sorgusu daha yürütülmektedir. Aynı amaca yönelik olaraktan aşağıdaki gibi bir sorguda göz önüne alınabilir.

SELECT SUM(ListPrice) AS [Value]
FROM Production.Product
GROUP BY Class
HAVING Class='M'

Burada Group By kullanımı gerçekleştirilmektedir. LINQ' in Sum metodunu neden farklı bir şekilde yorumladığı tartışılabilir. Sonuç itibariyle her iki sorgu cümlesininde beklenen icra planlarına(Esitamted Execution Plan) bakıldığında Products gibi sadece 504 satıra sahip olan küçük bir tabloda çok fazla bir fark olmadığı görülmektedir. Ancak tablo boyutunun artması halinde bu durumun belirgin performans farklılıklarına yol açıp açmayacağına bakılmalıdır. Aşağıdaki ekran görüntülerinde her iki sorgunun icra planlarına ait icra maliyetleri daha net bir şekilde görülmektedir.

Diğer LINQ to SQL operatörlerini inceleyerek devam edelim. Sıradaki LINQ ifadesinde, Contains metodu ele alınmaktadır. Contains metodu String sınıfına ait bir fonksiyondur. Aşağıdaki kod parçasında yer alan ifadeye göre Contains metodunun görevi, ProductNumber alanında PA hecesi olan Product nesne örneklerinin tespit edilmesinin sağlanmasıdır.

var allProducts = from prd in adw.Products
                            where prd.Class == null && prd.ProductNumber.Contains("PA")
                                select prd;

foreach (Product p in allProducts)
{
    Console.WriteLine(p.ProductNumber + " " + p.Name);
}

Sorgu ifadesi null değere sahip olan sınıflara ait ürünlerden, ürün numarasında PA hecesi geçenleri elde etmektedir. Yukarıdaki kod parçasını içeren Console uygulaması çalıştırıldığında aşağıdaki sonuçlar alınacaktır.

Bizim için asıl önem arz eden konu LINQ to SQL ifadesinin SQL sunucusuna nasıl gönderildiğidir. Hemen SQL Server Profiler aracına bakılırsa aşağıdaki sorgu cümlesinin çalıştırıldığı görülebilir.

exec sp_executesql N'SELECT
    [t0].[ProductID], [t0].[Name]
    , [t0].[ProductNumber], [t0].[MakeFlag]
    , [t0].[FinishedGoodsFlag], [t0].[Color]
    , [t0].[SafetyStockLevel], [t0].[ReorderPoint]
    , [t0].[StandardCost], [t0].[ListPrice]
    , [t0].[Size], [t0].[SizeUnitMeasureCode]
    , [t0].[WeightUnitMeasureCode], [t0].[Weight]
    , [t0].[DaysToManufacture], [t0].[ProductLine]
    , [t0].[Class], [t0].[Style], [t0].[ProductSubcategoryID]
    , [t0].[ProductModelID], [t0].[SellStartDate]
    , [t0].[SellEndDate], [t0].[DiscontinuedDate]
    , [t0].[rowguid], [t0].[ModifiedDate]
FROM [Production].[Product] AS [t0]
WHERE
    ([t0].[Class] IS NULL) AND ([t0].[ProductNumber] LIKE @p0)'
,N'@p0 nvarchar(4)',@p0=N'%PA%'

Dikkat edileceği üzere programatik tarafta yapılan ==null kontrolü SQL tarafında Is Null olarak, Contains metodu ise Like olarak çevrilmiştir. Buradaki Like ifadesine gönderilen %PA% değeri, içerisinde PA hecesi geçenleri ifade etmektedir. Öyleyse Contains metodu yerine örneğin StartsWith fonksiyonu kullanılırsa ne olacağına bakılmalıdır. Bu amaçla LINQ to SQL ifadesini aşağıdaki gibi değiştirdiğimizi düşünelim.

var allProducts = from prd in adw.Products
                            where prd.Class == null && prd.ProductNumber.StartsWith("PA")
                                select prd;

Bu durumda SQL tarafına gönderilen sorgu cümlesinin içeriğine bakıldığında LIKE anahtar kelimesine atanan parametrenin PA% şeklinde olduğu görülmektedir. Dolayısıyla beklenildiği gibi PA hecesi ile başlayanların tedarik edilmesi için sorguda gerekli düzenleme yapılmıştır.

Bir önceki LINQ to SQL ifadesinde, SQL sunucusu üzerinde çalışan sorguya bakıldığında Product tablosundaki tüm alanların çekildiği görülmektedir. Oysaki çoğu durumda elde edilip veri kümesi Entity üzerine alındığında yanlızca bir kaç alan üzerinde işlem yapılmaktadır. Söz gelimi elde edilen listenin bir GridView üzerinde gösterilmesi istendiğinde tüm alanlar yerine gerekli olanların gösterilmesi tercih edilir. İşte bu noktada isimsiz tiplerin(Anonymous Types) faydası ortaya çıkmaktadır. Buna göre aşağıdaki LINQ to SQL ifadesini ele alalım.

var allProducts = from prd in adw.Products
                            where prd.Class == null && prd.ProductNumber.Contains("PA")
                                select new
                                    {
                                        prd.Name
                                        , prd.ProductNumber
                                    };

foreach (var p in allProducts)
{
    Console.WriteLine(p.ToString());
}

Bu örnek kod parçasında PA hecesini içeren ve Class alanının değeri null olan Product tiplerindeki Name ve ProductNumber özellikleri kullanılarak yeni bir tip elde edilmektedir. Burada ihtiyaçlar dahilinde isimsiz tipin(Anonymous Type) hangi özellikleri(Properties) içereceği belirlenebilir. Bu örnek kod parçasının çalışma zamanında üreteceği çıktı aşağıdaki ekran görüntüsündekine benzer olacaktır.

SQL sunucusu tarafına gönderilen sorgu cümlesinin içeriği ise aşağıdaki gibidir.

exec sp_executesql N'SELECT [t0].[Name], [t0].[ProductNumber]
FROM [Production].[Product] AS [t0]
WHERE
    ([t0].[Class] IS NULL) AND ([t0].[ProductNumber] LIKE @p0)'
,N'@p0 nvarchar(4)',@p0=N'%PA%'

Görüldüğü gibi sadece istenen alanların çekilmesi sağlanmaktadır. Buda isimsiz tiplerin LINQ to SQL tarafında oldukça önemli bir rol oynadığını göstermektedir.

LINQ to SQL tarafında kullanılan ilginç fonksiyonelliklerden ikiside Skip ve Take metodlarıdır. Skip metodu parametre olarak verilen değer kadar atlanılmasını, Take metodu ise atlanılan satırdan itibaren kaç satır alınacağını belirtmektedir. Buda basit olarak bir sayfalamanın(Paging) yapılabilmesine olanak sağlamaktadır. Çok doğal olarak metodlar yardımıyla programatik ortamda kolayca uygulanabilen bu tekniğin SQL tarafına aktarılmasında SQL 2005 ile birlikte gelen Row_Number fonksiyonunun önemli bir rolü vardır. Bu metodları daha iyi analiz etmek için aşağıdaki kod parçasını ele aldığımızı düşünelim.

var tenCtg = (from cat in adw.ProductSubcategories select cat)
                        .Skip<ProductSubcategory>(5)
                            .Take<ProductSubcategory>(10);

foreach (ProductSubcategory c in tenCtg)
{
    Console.WriteLine("{0} -> {1} ", c.ProductSubcategoryID.ToString(), c.Name);
}

Yukarıdaki kod parçasında yer alan LINQ to SQL ifadesine göre, ProductSubCategories koleksiyonu üzerinden ilk satır atlanarak 6ncı satırdan itibaren 10 satırlık bir ProductSubCategory nesne topluluğunun çekilmesi amaçlanmaktadır. Söz konusu kod parçasının çalıştırılmasının sonucu oluşan program çıktısına ait ekran görüntüsü aşağıdaki gibidir.

Örnek kod parçasında kullanılan LINQ to SQL ifadesi için SQL sunucusu üzerine gönderilen sorgu cümlesi ise aşağıdaki gibi olacaktır.

exec sp_executesql N'SELECT [t1].[ProductSubcategoryID], [t1].[ProductCategoryID], [t1].[Name], [t1].[rowguid], [t1].[ModifiedDate]
FROM (
    SELECT ROW_NUMBER() OVER
        (ORDER BY
            [t0].[ProductSubcategoryID], [t0].[ProductCategoryID],
             [t0].[Name], [t0].[rowguid]
            , [t0].[ModifiedDate])
        AS [ROW_NUMBER], [t0].[ProductSubcategoryID], [t0].[ProductCategoryID], [t0].[Name], [t0].[rowguid], [t0].[ModifiedDate]
    FROM [Production].[ProductSubcategory] AS [t0]
) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]',N'@p0 int,@p1 int',@p0=5,@p1=10

Dikkat edileceği üzere Row_Number fonksiyonu burada önemli bir rolü üstlenmektedir. Şimdi son kod parçasında aşağıdaki gibi bir ekleme daha yaptığımızı düşünelim.

var tenCtg = (from cat in adw.ProductSubcategories select cat)
                        .Skip<ProductSubcategory>(5)
                            .Take<ProductSubcategory>(10);

foreach (ProductSubcategory c in tenCtg)
{
    Console.WriteLine("{0} -> {1}({2}) ",c.ProductSubcategoryID.ToString(),c.Name,c.Products.Count().ToString());
}

Yapılan değişikliğe göre, 5nci satırdan itibaren alınan 10 ProductSubCategory nesnesinin her biri için Products özelliğinden yola çıkılarak Count değerleride hesaplanmaktadır. Bir başka deyişle her bir alt kategorideki ürünlerin toplam sayılarıda elde edilmektedir. Bu kod parçasının çalışma zamanı(run time) görüntüsü ise aşağıdaki gibidir.

ProductSubCategory ve Product sınıfları arasında bire çok(one to many) bir ilişki mevcuttur. Bu ilişki programatik taraftada ilgili varlık sınıflarına(Entity Class) yansıtılmaktadır. Bu nedenle bir ProductSubCategory nesne örneği üzerinden Products özelliği ile bağlı olunan Product nesne topluluğuna geçiş yapmak son derece kolaydır. Bu da gönül rahatlığı ile Count gibi metodları kullanıp istediğimiz tarzda sonuçları alabilmemizi olanaklı kılmaktadır. Ne varki Count çağrısı for döngüsü içerisinde, bir önceki sorgudan elde edilen her bir ProductSubCategory nesne örneği için ayrı ayrı yapılmaktadır. Bunun SQL sunucusu üzerinde oluşturacağı sonuç ise şudur; elde edilen her bir ProductSubCategory için, buna bağlı toplam ürün sayısını döndüren bir sorgu cümlesi çalışmaktadır. Aşağıdaki ekran görüntüsünde bu durumun bir kısmı ifade edilmektedir.

Sorgu ifadelerine dikkat edilecek olursa Count metodu çağırıldığında aslında SQL tarafında bir Count hesabı yapılmamaktadır. Products özelliğine geçildiğinden, o anki ProductSubCategoryID değerine bağlı Product satırları, programatik taraftaki nesne topluluğuna yüklenmektedir. Bu nesneler yüklendikten sonra bildiğimiz koleksiyonlara ait olan Count metodu çalışmakta ve toplam ürün sayıları bu şekilde elde edilmektedir. Hiç bir durumda bu tarz bir yol ile toplam sayıların elde edilmesi tercih edilmemelidir. Görüldüğü gibi gayet masumane olan ama çok işe yaradığı düşünülen basit bir kod parçası arka tarafta son derece fazla sayıda ve yoğun sorgu cümlelerinin çalışmasına neden olmuştur.

SQL tarafında birden fazla tablo üzerinde bir arada işlem yapılması gerektiği durumlarda çoğunlukla join yapılarından yararlanılmaktadır. Aynı yapı bildiğiniz gibi LINQ ile nesneler üzerindede gerçekleştirilebilmektedir. Sıradaki örnekte LINQ to SQL için join kullanımına bakılmaktadır. Bu amaçla aşağıdaki gibi bir kod parçası geliştirdiğimizi düşünelim.

var allList = from pc in adw.ProductCategories
                    join psc in adw.ProductSubcategories
                        on pc.ProductCategoryID equals psc.ProductCategoryID
                            join p in adw.Products
                                on psc.ProductSubcategoryID equals p.ProductSubcategoryID
                                    where p.Class == null && p.ListPrice > 100
                                        select new
                                        {
                                            CategoryName = pc.Name
                                            , SubCategoryName = psc.Name
                                            , ProductNumber = p.ProductNumber
                                            , ProductName = p.Name
                                        };

foreach (var prd in allList)
{
    Console.WriteLine(prd.ToString());
}

Buradaki kod parçasına göre ProductCategories, ProductSubCategories ve Products özelliklerine bağlı generic Table<T> koleksiyonları join anahtar kelimesi yardımıyla anahtar özellikler üzerinden birleştirilmekte ve yeni bir isimsiz tipe(Anonymous Type) ait nesne topluluğu elde edilmektedir. Bu nesne topluluğu elde edilirken Class değeri null olan ve ürünlerin ListPrice değeri 100' den büyük olanların elde edilmesi sağlanmaktadır. Buna göre uygulamanın ekran çıktısı aşağıdaki gibi olacaktır.

Söz konusu LINQ to SQL ifadesinin SQL tarafındaki karşılığı ise aşağıdaki gibidir.

exec sp_executesql N'SELECT [t0].[Name] AS [CategoryName], [t1].[Name] AS [SubCategoryName], [t2].[ProductNumber], [t2].[Name] AS [ProductName]
FROM [Production].[ProductCategory] AS [t0]
    INNER JOIN [Production].[ProductSubcategory] AS [t1] ON [t0].[ProductCategoryID] = [t1].[ProductCategoryID]
        INNER JOIN [Production].[Product] AS [t2] ON ([t1].[ProductSubcategoryID]) = [t2].[ProductSubcategoryID]
WHERE ([t2].[Class] IS NULL) AND ([t2].[ListPrice] > @p0)',N'@p0 decimal(33,4)',@p0=100.0000

Görüldüğü gibi join kelimeleri SQL tarafında standart inner join muamelesi görmektedir.

Sıradaki LINQ to SQL ifadesinde DateTime yapısının(struct) parçalarından yararlanılmakta olup, bu ayrıştırmanın SQL tarafına nasıl yansıtıldığı incelenmeye çalışılmaktadır. Bu amaçla uygulamaya aşağıdaki kod parçasını eklediğimizi düşünelim.

 var result = from p in adw.Products
                    where p.SellStartDate.Month >= 6 && p.SellStartDate.Month <= 12
                        select new
                                    {
                                        p.ProductNumber
                                        , p.Name
                                        , p.ListPrice
                                        , p.SellStartDate
                                        , p.SellEndDate
                                    };

foreach (var p in result)
{
    Console.WriteLine(p.ProductNumber + " " + p.SellStartDate.Month.ToString() + " ");
}

Yukarıdaki kod parçasında yer alan LINQ ifadesinde, her bir Product nesne örneğinin SellStartDate özellikleri üzerinden hareket edilerek Month değerlerine bakılmakta ve 6ncı ila 12nci ay arasında olanlar değerlendirilerek yeni bir isimsiz tip(Anonymous Type) içerisinde toplanmaktadır. Örneğe ait çalışma zamanı ekran çıktısı aşağıdaki gibidir.

Burada merak edilen konu, Month değerlerinin SQL tarafında nasıl ele alınacağıdır. Bu amaçla örnek çalıştırıldıktan sonra SQL Server Profiler aracına bakılırsa, DatePart SQL fonksiyonunun kullanıldığı açık bir şekilde görülebilir.

exec sp_executesql N'SELECT [t0].[ProductNumber], [t0].[Name], [t0].[ListPrice], [t0].[SellStartDate], [t0].[SellEndDate]
FROM [Production].[Product] AS [t0]
WHERE
    (DATEPART(Month, [t0].[SellStartDate]) >= @p0)
        AND (DATEPART(Month, [t0].[SellStartDate]) <= @p1)'
,N'@p0 int,@p1 int',@p0=6,@p1=12

Böylece SellStartDate alanlarının içeriği DatePart fonksiyonu kullanılaraktan ayrıştırılmakta ve elde edilen Month kısmına görede değer aralığı kontrolü yapılmaktadır.

Bazı durumlarda sorgulanan verinin string bazlı olması halinde karakter tabanlı kontroller yapılmak istenebilir. Söz gelimi B harfi ile başlayan ürünlerin elde edilmesi gibi. Bu gibi durumlarda string bazlı verilerin karakter katarı olduğu programatik tarafta ele alınması gereken bir durumdur. Aşağıdaki kod parçasında hem bu durum ele alınmakta hemde First metodunun kullanımı incelenmektedir.

Product result = (from p in adw.Products select p)
                            .First<Product>(prd => prd.Name[0] == 'C');

Console.WriteLine(result.Name + " " + result.ListPrice);

Buradaki ifadeye göre, her bir Product nesne örneğinin Name özelliklerinin ilk harflerine bakılmaktadır. İlk harfi C olan ürünlerden ise sadece ilki First metodu yardımıyla elde edilmektedir. Bu işlevselliği gerçekleştirmek için First metodu içerisinde lambda(=>) operatörü kullanılmaktadır. Lambda operatörü sayesinde eşitliğin sol tarafından sağ tarafına o anki Product nesne örneği geçirilmektedir. Eşitliğin sağ tarafında ise o anki Product nesne örneğinin Name özelliğinin ilk harfine bakılmaktadır. Eğer ilk harf C ise o anda üzerinde durulan Product nesne örneği eşitliğin sağından soluna doğru geri döndürülmektedir. Örneğin çalışma zamanındaki ekran çıktısı aşağıdaki gibi olacaktır.

Söz konusu LINQ to SQL ifadesinin SQL tarafına aktarılan sorgu cümlesi ise aşağıdaki gibidir.

exec sp_executesql N'SELECT TOP (1)
        [t0].[ProductID], [t0].[Name], [t0].[ProductNumber]
        , [t0].[MakeFlag], [t0].[FinishedGoodsFlag], [t0].[Color]
        , [t0].[SafetyStockLevel], [t0].[ReorderPoint], [t0].[StandardCost]
        , [t0].[ListPrice], [t0].[Size], [t0].[SizeUnitMeasureCode]
        , [t0].[WeightUnitMeasureCode], [t0].[Weight], [t0].[DaysToManufacture]
        , [t0].[ProductLine], [t0].[Class], [t0].[Style], [t0].[ProductSubcategoryID]
        , [t0].[ProductModelID], [t0].[SellStartDate], [t0].[SellEndDate]
    , [t0].[DiscontinuedDate], [t0].[rowguid], [t0].[ModifiedDate]
FROM [Production].[Product] AS [t0]
WHERE
    UNICODE(CONVERT(NChar(1),SUBSTRING([t0].[Name], @p0 + 1, 1))) = @p1',N'@p0 int,@p1 int',@p0=0,@p1=67

Herşeyden önce First metodunun tam karşılığı olarak TOP (1) söz dizimi kullanılmaktadır. Diğer taraftan programatik ortamda [] indeksleyici operatörünü kullanarak string veri tipinin ilk karakterine geçmemiz ve C harfini kontrol etmemizin karşılığı Unicode, Convert, SubString SQL fonksiyonları olmuştur. Burada 67 değerinin C harfine karşılık geldiğini hatırlayalım. Elbette çekilen veri bir Product tipi olduğundan, Product tablsoundaki tüm alanların Select ifadesine alındığı görülmektedir. Daha öncedende belirtildiği gibi, sadece gerekli alanların çekilmesi adına kod tarafında isimsiz metod(Anonymous Method) kullanımına gidilebilir.

LINQ tarafında çoklu seçimlerde(Select Many) yapılabilmektedir. Bu tarz bir kullanıma örnek olarak aşağıdaki kod parçası ele alınabilir.

var result = from p in adw.SalesPersons
                    where p.Bonus >= 1000
                        from h in p.SalesOrderHeaders
                            where h.TerritoryID == 1
                                select new
                                            {
                                                 p.SalesPersonID
                                                , p.Bonus
                                                , h.SubTotal
                                                , h.AccountNumber
                                            };

foreach (var r in result)
{
    Console.WriteLine(r.ToString());
}

Buradaki sorgu ifadesine göre, SalesPersons koleksiyonunda tutulan SalesPerson nesne örneklerinden Bonus özelliklerinin değeri 1000' in üzerinde olanlar alınmaktadır. Sonrasında ise elde edilen kümedeki her bir SalesOrder üzerinden SalesOrderHeaders koleksiyonuna gidilmekte ve bölge değeri 1 olanlar çekilmektedir. Bir başka deyişle Bonus' u 1000' in üzerinde ve sipariş kalemleri 1 numaralı bölgeye doğru yapılmış olan satış personelinin elde edilmesi söz konusudur. Elde edilen veri kümesi değerlendirilerek yeni bir isimsiz tip(Anonymous Type) içerisinde birleştirilmeleri sağlanmaktadır. Örnek kodun çalışma zamanındaki çıktısı aşağıdaki gibi olacaktır.

Şu aşamada bizim ilgilendiğimiz kısım SQL tarafına gönderilen sorgu cümlesidir. Bu cümlede aşağıdaki şekildedir.

exec sp_executesql N'SELECT [t0].[SalesPersonID], [t0].[Bonus], [t1].[SubTotal], [t1].[AccountNumber]
    FROM [Sales].[SalesPerson] AS [t0], [Sales].[SalesOrderHeader] AS [t1]
        WHERE ([t1].[TerritoryID] = @p0) AND ([t0].[Bonus] >= @p1)
                        AND ([t1].[SalesPersonID] = [t0].[SalesPersonID])'
,N'@p0 int,@p1 decimal(33,4)',@p0=1,@p1=1000.0000

Burada From kelimesinden sonraki kısma bakıldığında SalesPerson ve SalesOrderHeader tablolarının birlikte ele alındıkları görülmektedir.

Gelelim gruplama fonksiyonelliklerinin SQL tarafına nasıl yansıtıldığında. Bu amaçla aşağıdaki örnek kod parçasını göz önüne alıyor olacağız.

var result = from p in adw.Products
                    where p.Class != null
                        group p by p.Class into g
                            select new
                                            {
                                                ClassName = g.Key
                                                ,TotalListPrice = g.Sum<Product>(p => p.ListPrice)
                                            };

foreach (var r in result)
{
    Console.WriteLine("{0} : {1}", r.ClassName, r.TotalListPrice);
}

Örnekteki ifadede, Products koleksiyonundaki her bir Product nesnesinin Class özelliklerine göre gruplara ayrılması ve her bir gruba ait ListPrice özelliklerinin toplam değerlerinin bulunması sağlanmaktadır. Bir başka deyişle sınıfları olan ürünlerin sınıflara göre gruplandıklarında, toplam liste fiyatı değerlerinin ne olduğu elde edilmektedir. Bu kod parçasının icra edilmesi halinde, çalışma zamanında aşağıdakine benzer sonuç ortaya çıkmaktadır.

Görüldüğü gibi ürünler sınıflara göre gruplanmış ve toplam ürün fiyatlarının değerleri elde edilmiştir. Burada çalışan LINQ to SQL ifadesinin SQL tarafına gönderilen karşılığı ise aşağıdaki gibi olacaktır.

SELECT SUM([t0].[ListPrice]) AS [TotalListPrice], [t0].[Class] AS [ClassName]
FROM [Production].[Product] AS [t0]
WHERE [t0].[Class] IS NOT NULL
GROUP BY [t0].[Class]

Aslında üretilen SQL cümlesi tam olarak düşündüğümüz şekildedir. Bununla birlikte dikkat edilmesi gereken bir husus vardır. Buda LINQ sorgusundaki where kelimesinin kullanıldığı yerdir. Örnekte, where ifadesi ile seçilen küme üzerinde gruplama yapılmaktadır. Bu nedenle group by kelimesinden önce where kullanılmaktadır. Ancak aynı ifade aşağıdaki haliylede geliştirilebilir.

var result = from p in adw.Products
                    group p by p.Class into g
                        where g.Key!=null
                            select new
                                        {
                                            ClassName = g.Key
                                            ,TotalListPrice = g.Sum<Product>(p => p.ListPrice)
                                        };

foreach (var r in result)
{
    Console.WriteLine("{0} : {1}", r.ClassName, r.TotalListPrice);
}

Bu sefer gruplanan nesneye ait Key özelliğinin null olup olmadığına bakılmaktadır. Kod bu haliyle çalıştırıldığında da bir önceki ile aynı sonuçların elde edildiği görülebilir. Ne varki SQL tarafına gönderilen ifadeye bakıldığında aşağıdaki sonuçlar ortaya çıkmaktadır.

SELECT [t1].[Class] AS [ClassName], [t1].[value] AS [TotalListPrice]
FROM (
            SELECT SUM([t0].[ListPrice]) AS [value], [t0].[Class]
            FROM [Production].[Product] AS [t0]
            GROUP BY [t0].[Class]
) AS [t1]
WHERE [t1].[Class] IS NOT NULL

Sonuç bir öncekinden oldukça farklıdır. Bu kez devreye ek bir alt sorgu cümlesi daha girmektedir. Önce sınıflara göre gruplanmış ürünlerin ListPrice değelerinin toplamları ve sınıf adlarının olduğu küme elde edilmektedir. Sonrasında ise bu küme üzerinden Class değerleri null olmayanlar çekilmektedir. Bu noktada where kelimesinin kod tarafından yerinin değiştirilmesinin önemli olup olmadığına karar vermek gerekebilir. Ancak geliştirilen örneğe ait oluşturulan sorguların icra planlarına(Execution Plan) bakıldığında bir fark olmadığı açıkça görülmektedir.

LINQ tarafında yer alan enteresan metodlardan biriside Except metodudur. Bu metoddan yararlanılarak belirli bir şartın dışında kalan nesnel kümelerin elde edilmesi sağlanabilir. Örnek olarak aşağıdaki gibi bir kod parçası geliştirdiğimizi düşünelim.

var result = (from c in north.Customers select c.City)
                    .Except(from s in north.Suppliers select s.City);

foreach (var r in result)
{
    Console.WriteLine(r);
}

Bu örnekte NorthwindDataContext kullanılmaktadır. Buna göre LINQ ifadesinde ilk parantez içerisinde kalan kısımda Customers koleksiyonunda duran Customer nesne örneklerinden City özellikleri çekilmektedir. Except metodunda yazılan ifadede Suppliers tablosunda yer alan Supplier nesne örneklerinden City özelliklerini çekmektedir. Her iki küme bir arada düşünüldüğünde ortaya çıkan sonuç şudur; Customer nesne örneklerinde olup, Supplier nesne örneklerinde bulunmayan City özellikleri elde edilmektedir. Daha düzgün bir ifadeyle, bir başka deyişle SQL' ce düşünüşdüğünde, müşterilerin yaşayıpta tedarikçilerinin bulunmadığı şehir adlarının elde edildiğini söyleyebiliriz. Programın çalışma zamanındaki çıktısı aşağıdaki gibidir.

Bu tarz bir ihtiyacı SQL tarafında karşılamak için Not In kullanımı tercih edilebilir. LINQ tarafında metod bazlı yazılan bu örnek ise, SQL tarafına aşağıdaki şekilde aktarılmaktadır.

SELECT DISTINCT [t0].[City]
FROM [dbo].[Customers] AS [t0]
WHERE
    NOT (EXISTS
            (
        SELECT NULL AS [EMPTY]
            FROM [dbo].[Suppliers] AS [t1]
                WHERE (([t0].[City] IS NULL)
                    AND ([t1].[City] IS NULL))
                        OR (([t0].[City] IS NOT NULL)
                            AND ([t1].[City] IS NOT NULL)
                                AND ([t0].[City] = [t1].[City]))
            )
        )

Burada önemli olan Exists SQL fonksiyonu ile gereken işlevselliğin sağlanmış olmasıdır. Not konulmasının sebebi, Exists ile belirtilen alt sorgudaki koşula uyanların dışarıda bırakılmasını sağlamaktır. Nitekim t0 ve t1 tablolarındaki City değerlerine bakılarak eşit olanların elde edilmesi sağlanırken Except metodu kullanılması nedeniyle bunların dışarıda tutulmasını ancak Not anahtar kelimesi sağlayabilmektedir. Ayrıca hem Suppliers hemde Customers tablosundaki City alanları için detaylı bir Null kontrolü yapılmaktadır.

Makalemize yine enteresan LINQ fonksiyonları ile devam edelim. Bu kez Any ve All isimli metodları incelemeye çalışıyor olacağız. Bu amaçla ilk olarak Any metodunun kullanımına kısaca bakılım.

 var result = from p in adw.SalesPersons
                        where p.SalesOrderHeaders.Any(soh => soh.SubTotal >= 224356)
                            select p;

foreach
(SalesPerson person in result)
{
    Console.WriteLine("Person Id: " + person.SalesPersonID + " Bonus: " + person.Bonus + " Sales Last Year : " + person.SalesLastYear);
    foreach (SalesOrderHeader header in person.SalesOrderHeaders)
        Console.WriteLine("\t" + header.AccountNumber + " Sub Total: " + header.SubTotal);
}

LINQ to SQL ifadesinde SalesPersons koleksiyonundaki her bir SalesPerson çekilmektedir. Bunlara bağlı olan SalesOrderHeaders koleksiyonundaki SalesOrderHeader nesne örneklerinin ise SubTotal değerlerine bakılarak seçim işlemi koşullandırılmaktadır. Any metodunun buradaki görevi ise şudur; SubTotal değerlerinden herhangibiri 224356' nın üzerinde olan satırlar elde edilebilmektedir. Yani, satış personelinin siparişlerine ait SubTotal değerlerinden herhangibiri 224356' nın üzerinde olanların elde edilmesi sağlanmaktadır. Örnek kod parçasının çalışma zamanındaki çıktısı aşağıdaki gibi olacaktır.

Bu tarz bir işleyiş için SQL tarafı göz önüne alındığında ortaya karmaşık bir sorgu çıkacağı düşünülebilir. Nitekim LINQ to SQL ifadesinin SQL tarafındaki karşılığı aşağıdaki gibidir.

exec sp_executesql N'
SELECT
    [t0].[SalesPersonID], [t0].[TerritoryID], [t0].[SalesQuota]
    , [t0].[Bonus], [t0].[CommissionPct], [t0].[SalesYTD]
    , [t0].[SalesLastYear], [t0].[rowguid], [t0].[ModifiedDate]
FROM [Sales].[SalesPerson] AS [t0]
    WHERE EXISTS(
                                SELECT NULL AS [EMPTY]
                                FROM [Sales].[SalesOrderHeader] AS [t1]
                                WHERE ([t1].[SubTotal] >= @p0) AND ([t1].[SalesPersonID] = [t0].[SalesPersonID])
    )'
,N'@p0 decimal(33,4)',@p0=224356.0000

Dikkat edileceği üzere Exists anahtar kelimesi kullanılarak SubTotal değeri ele alınmakta ve buna uyanların SalesPerson tablosundan çekilmesi sağlanmaktadır. Gelelim All metoduna. Bu sefer Any' den farklı olarak bağlı olunan kümedeki her bir eleman için belirtilen koşulun sağlanmış olma şartı aranmaktadır. Bunu daha net kavrayabilmek için örneğimizi aşağıdaki gibi değiştirelim.

var result = from p in adw.SalesPersons
                    where p.SalesOrderHeaders.All(soh => soh.SubTotal >= 80)
                        select p;

foreach (SalesPerson person in result)
{
    Console.WriteLine("Person Id: " + person.SalesPersonID + " Bonus: " + person.Bonus + " Sales Last Year : " + person.SalesLastYear);
    foreach (SalesOrderHeader header in person.SalesOrderHeaders)
        Console.WriteLine("\t" + header.AccountNumber + " Sub Total: " + header.SubTotal);
}

Bu kodun çalışma zamanı çıktısı ise aşağıdaki gibi olacaktır.

Bu sefer bir SalesOrder' ın bağlı olduğu SalesOrderHeaders koleksiyonundaki her bir SalesOrderHeader nesne örneğinin SubTotal değerlerinin her biri 80' in üzerinde olanların elde edilmesi sağlanmaktadır. Bir başka deyişle bir SalesOrder üzerinden ulaşılan nesne topluluğunda n tane SalesOrderHeader olduğu düşünülecek olursa, bunların her birine ait SubTotal özelliklerinin değerlerinin 80 ve üzerinde olma şartı konulmaktadır. Söz konusu All metodu için SQL tarafında üretilen çıktı ise aşağıdaki gibidir.

exec sp_executesql N'
SELECT
    [t0].[SalesPersonID], [t0].[TerritoryID], [t0].[SalesQuota]
    , [t0].[Bonus], [t0].[CommissionPct], [t0].[SalesYTD]
    , [t0].[SalesLastYear], [t0].[rowguid], [t0].[ModifiedDate]
FROM [Sales].[SalesPerson] AS [t0]
WHERE NOT (
                        EXISTS(
                                        SELECT NULL AS [EMPTY]
                                        FROM [Sales].[SalesOrderHeader] AS [t1]
                                        WHERE ((
                                                            (CASE
                                                                WHEN [t1].[SubTotal] >= @p0 THEN 1 ELSE 0
                                                            END)) = 0)
                                                            AND ([t1].[SalesPersonID] = [t0].[SalesPersonID])
))'
,N'@p0 decimal(33,4)',@p0=80.0000

Bu kez koşulun kontrolü için Case ifadesinden yararlanılmakta ve SubTotal 80' üzerinde ise 1, değilse 0 değeri Where ifadesine katılarak 0 olanların çekilmesi sağlanmaktadır. Yanlız burada yine Not Exist kullanıldığında dikkat etmekte yarar vardır. Buna görede bir Where ifadesinin SalesPerson tablosu için üretilmesi sağlanmaktadır.

Örneklerimize Concat metodu ile devam edelim. Concat metodunu daha çok iki farklı sonuç kümesindeki belirli özelliklerin bir arada ele alınmasını istediğimiz durumlarda göz önüne alabiliriz. Bu bir anlamda iki string' in birleştirilemesine benzer bir durumdur. Tabi şu anda söz konusu olan string değil IEnumerable gibi referanslardır. Concat metodunun SQL tarafında ürettiği çıktıya bakmak için aşağıdaki örnek kod parçasını geliştirdiğimizi düşünelim.

var result = (from cust in north.Customers select new { cust.Country, cust.City })
                    .Concat(from supl in north.Suppliers select new { supl.Country, supl.City });
foreach (var r in result)
{
    Console.WriteLine(r.Country + ":" + r.City);
}

Bu örnekte NorthwindDataContext tipi kullanılmakta olup Customers ve Suppliers koleksiyonlarındaki nesnelerde Country ve City değerleri birleştirilip çekilmektedir. Sonuçta kodun çalışma zamanı çıktısı aşağıdaki gibi olacaktır.

Elbetteki SQL tarafına bakıldığında Concat metodunun aşağıdakine benzer bir dönüşüme uğradığı görülmektedir.

SELECT [t2].[Country], [t2].[City]
FROM (
            SELECT [t0].[Country], [t0].[City]
            FROM [dbo].[Customers] AS [t0]
                UNION ALL
            SELECT [t1].[Country], [t1].[City]
            FROM [dbo].[Suppliers] AS [t1]
) AS [t2]

Açıkça Union All kullanılaraktan iki Select ifadesinin birleştirildiği ve elde edilen küme üzerinden Country ile City alanlarına ait değerlerin çekildiği söylenebilir. Örnekte dikkati çeken noktalardan biriside tekrarlı alanların olmasıdır. Örneğin ekran çıktısının üst taraflarına bakıldığında iki adet Mexico kentinin olduğu London' un iki kere geçtiği rahat bir şekilde görülebilir. Çok doğal olarak tekrarsız bir listenin elde edilmesi istendiğinde kod tarafında Distinct metodunun kullanılıyor olması yeterli olacaktır. Yani kod parçasında aşağıdaki değişikliğin yapılması yeterlidir.

var result = (from cust in north.Customers select new { cust.Country, cust.City }).Concat(from supl in north.Suppliers select new { supl.Country, supl.City }).Distinct();

Bu durumda programın çıktısı aşağıdaki gibi olacaktır.

Diğer taraftan Distinct metodunun kullanılması sonrasında SQL tarafına gönderilen sorgu cümlesinde ise Distinct anahtar kelimesinin kullanıldığıda aşikardır.

SELECT DISTINCT [t3].[Country], [t3].[City]
FROM (
            SELECT [t2].[Country], [t2].[City]
            FROM (
                        SELECT [t0].[Country], [t0].[City]
                        FROM [dbo].[Customers] AS [t0]
                            UNION ALL
                        SELECT [t1].[Country], [t1].[City]
                        FROM [dbo].[Suppliers] AS [t1]
            ) AS [t2]
) AS [t3]

Ancak burada bir öncekinden farklı bir sorgunun oluştuğuda gözlerden kaçmamalıdır. Bu kez iç içe alınmış Select sorgusu söz konusudur. Oysaki en dışta yer alan Select kullanımına gerek yoktur. Çünkü Distinct anahtar kelimesi içerideki sorgu cümlesine eklenerekte aynı sonuçların alınması sağlanabilir. Ne varki sorgu cümlesini bu şekilde oluşturup SQL tarafına gönderen LINQ to SQL mimarisidir.

Yine ilginç bir LINQ metodu ve SQL karşılığı ile devam edelim. Bu kez iki farklı veri kümesinin kesişimlerinin elde edilmesinde kullanılabilen Intersect metodu üzerinde duracağız. Bu metodun analizi için aşağıdaki gibi bir kod parçası geliştirdiğimizi düşünelim.

var result = (from c in north.Customers select c.City)
                    .Intersect(from s in north.Suppliers select s.City);

foreach (var r in result)
{
    Console.WriteLine(r);
}

Öncelikli olarak ilk parantezler arasında Customers koleksiyonundaki her bir Customer nesnesinin City değerleri çekilmektedir. İkinci parantez içerisinde yapılanda benzerdir. Tek farkı Suppliers koleksiyonu için çalışmakta olmasıdır. Intersect metodunun burada getridiği kolaylık ise şudur. Customers ve Suppliers koleksiyonlarında bulundan ortak City özelliklerinin elde edilmesini sağlamaktadır. Bir başka deyişle yine SQL' ciler gibi konuşacak olursak, müşterilerin ve tedarikçilerin bir arada bulunduğu şehirlerin elde edilmesi sağlanmaktadır . Buna göre örneğin çalışma zamanındaki ekran çıktısı aşağıdaki gibi olacaktır.

Bu sonucun elde edilmesi için arka planda çalıştırılan SQL cümlesi ise aşağıdaki gibidir.

SELECT DISTINCT [t0].[City]
FROM [dbo].[Customers] AS [t0]
WHERE EXISTS(
                            SELECT NULL AS [EMPTY]
                            FROM [dbo].[Suppliers] AS [t1]
                            WHERE
                                (([t0].[City] IS NULL)
                                    AND ([t1].[City] IS NULL))
                                        OR (([t0].[City] IS NOT NULL)
                                            AND ([t1].[City] IS NOT NULL)
                                                AND ([t0].[City] = [t1].[City]))
)

Çalıştırılan SQL sorgusu, LINQ tarafında Except metodu kullanıldığı zamankine benzerdir. Tek fark burada kesişim kümesinin bulunması gerektiğinden Not Exists kullanılmamış olmasıdır.

Makalemizde son olarak ?: operatörünün kullanıldığı bir durumu ele almaya çalışıyor olacağız. Bu operatör şu aşamada LINQ' e bağımlı olmayan C# programlama dilinin ilk versiyonundan beri var olan bir araçtır. Bu tip bir operatörün LINQ ifadesi içerisinde kullanılması haline SQL tarafında oluşacak olan cümlelere bakmaya çalışıyor olacağız. Bu amaçla aşağıdaki kod parçasını geliştirdiğimizi düşünelim.

 var result = from prd in adw.Products
                    select new
                                    {
                                        prd.Name
                                        ,prd.SafetyStockLevel
                                        ,LevelOk = prd.SafetyStockLevel >= 50 ? "Seviye İyi" : "Seviye Düşük"
                                    };

foreach (var p in result)
{
    Console.WriteLine(p.Name + " | " + p.SafetyStockLevel + " | " + p.LevelOk);
}

Bu kod parçasında kullanılan LINQ ifadesine bakıldığında, LevelOk isimli isimsiz tip özelliğinin değerinin SafetyStockLevel özelliğinin değerine göre belirlendiği görülmektedir. SafetyStockLevel özelliğinin değerinin 50 ve üzerinde olması halinde LevelOk özelliğine Seviye İyi değeri atanmaktadır. Aksi durumda ise Seviye Düşük değeri atanmaktadır. Kodun çalışma zamanındaki çıktısı aşağıdaki gibi olacaktır.

SQL tarafına baktığımızda ise aşağıdaki sorgu cümlesinin çalıştırıldığı görülmektedir.

exec sp_executesql N'SELECT [t0].[Name], [t0].[SafetyStockLevel],
    (CASE
        WHEN [t0].[SafetyStockLevel] >= @p0 THEN CONVERT(NVarChar(12),@p1) ELSE @p2 END
    )
    AS [LevelOk]
FROM [Production].[Product] AS [t0]'
    ,N'@p0 int,@p1 nvarchar(10),@p2 nvarchar(12)',@p0=50,@p1=N'Seviye İyi',@p2=N'Seviye Düşük'

Dikkat edileceği üzere, LevelOk alanının elde edilmesi sırasında Case When SQL ifadesi kullanılmaktadır.

Buraya kadar anlatılan örneklerde LINQ operatörlerinden veya metodlarından bir kısmının SQL tarafına nasıl aktarıldıkları incelenmeye çalışılmıştır. Diğer taraftan makalemizin başındada belirtildiği üzere programatik tarafta kullanılan her tür LINQ operatörü veya fonksiyonunun SQL tarafına aktarılmasıda mümkün değildir. Söz gelimi aşağıdaki kod parçasını ele aldığımızı düşünelim.

 var result = (from ctg in adw.ProductSubcategories select ctg)
                   .TakeWhile<ProductSubcategory>(sCtg => sCtg.Name[0] == 'A');

foreach (ProductSubcategory sc in result)
{
    Console.WriteLine(sc.Name + " " + sc.ProductSubcategoryID.ToString());
}

Bu kod parçası yürütülmek istendiğinde çalışma zamanında(run time), aşağıdaki ekran görüntüsündende izlenebileceği gibi NotSupportedException tipinden bir istisna(Exception) alınmaktadır.

Nitekim TakeWhile metodunun SQL tarafında bir karşılığı yoktur. TakeWhile gibi SkipWhile, Last, ElementAt, Reverse gibi pek çok metodunda SQL tarafında karşılığı bulunmadığından desteklenmemektedirler.

Sonuç olarak programatik tarafta varlık katmanı(Entity Layer) üzerinde işlemlerimizi oldukça kolaylaştıran ve nesneler üzerinde sorgular çalıştırabilmemizi sağlayan LINQ to SQL' in gücü ortadadır. Ne varki performansın öne geçmesi gereken durumlarda, yazılan LINQ ifadelerinin arka planda oluşturduğu SQL çıktıları değerlendirilmeli ve en doğru şekilde kullanılmalarına gayret edilmelidir. Zaten zaman içerisinde benzer vakalar için en uygun LINQ söz dizimlerinin ne olacağı daha net bir şekilde ortaya çıkacaktır. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

DahaFazlaLINQSorgusu2.rar (104,12 kb)

Tags:   ,
Categories:   LINQ
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Bağlantısız Katmanda LINQ

Pazartesi, 2 Nisan 2007 20:44 by bsenyurt
Language Integrated Query (Dil ile tümleştirilmiş sorgu) yardımıyla yapabileceklerimiz saymakla bitmiyor. Aslında LINQ projesinin en önemli çıkış nedeni, Anders Hejslberg' ın anlatımıyla veri ve nesne eşitsizliğidir. (data!=objects) Bu ifadeyi, TechEd 2006 sunumlarında kullanan Anders Hejslberg, özellikle veri yapılarının programlama ortamına alınması sonrasında, var olan basit sorgu tekniklerinin uygulanamayışından yakınmaktadır. LINQ projesinin aslında en temel amacı, uygulamaların çalışma alanlarında (.Net perspektifinden baktığımızda Application Domain' ler içerisinde), bellek üzerinde konuşlanan nesneler üzerinden bildiğimiz veri sorgulama kurallarını uygulayabilmektir. Bir başka deyişle, nesne(object) üzerinde, var olan veritabanı nesnelerini taşıyabilen Entity bileşenleri üzerinde, belleğe alınan Xml veri setleri üzerinde, sorgulamaları bilinen alışılagelmiş söz dizimleri ile tek bir standart altında yapabilmektir. Tüm bu farklı nesnel yapıların ortak bir sorgulama dilini kullanabiliyor olması da LINQ projesinin ana fikirlerinden birisidir aslında.

Yukarıdaki grafikte, LINQ projesinin odaklandığı temel modeller ifade edilmeye çalışılmıştır. LINQ sorguları bildiğiniz gibi bellek üzerinde herhangibir şekilde IEnumerable arayüzünü uyarlamış olan her tür nesne topluluğuna uygulanabilmektedir. Bu nedenle bellek içi nesnelerden (in memory objects), veritabanı(database) bağlantılı Entity nesnelerine kadar pek çok yerde kullanılabilmektedir.

C# 3.0 ve geleceği ile ilgili olarak önceki makalelerimizde, DLINQ, XLINQ modellerini incelemeye çalışmıştık. Bunların yanında LINQ ile yapabileceklerimizi daha derinlemesine kavrayabilmek maksadıyla bol bol sorgu geliştirdik. Bugünkü makalemizde ise, özellikle bağlantısız katman (disconnected layer) nesneleri üzerinde, yani bildiğimiz DataSet ve DataTable nesne örnekleri üzerinde LINQ sorgularını nasıl yazabileceğimizi basit bir şekilde incelemeye çalışacağız. DataSet ve DataTable gibi bileşenler bildiğiniz gibi herhangibir veri kaynağından yüklenen sonuç kümelerini uygulama belleğinde tutmak amacıyla kullanılmaktadır. Ne varki çalışma zamanında, bağlantısız katman nesneleri üzerindeki verilerde sorgulama yapabilmek için çeşitli yollara başvurmamız gerekir. Örneğin bunlardan birisi Select metodudur. Bir başka teknikte veri kümelerini DataView bileşenlerine alıp filtreleme amacıyla yardımcı fonksiyonellerden faydalanmaktır. LINQ, felsefe olarak yukarıda bahsettiğimiz tüm veri kümeleri için ortak bir sorgulama ortamı sunmaktadır. Öyleyse bağlantısız katman nesneleri içinde bu tekilleştirilmiş sorgulama modelini nasıl ele alabiliriz?Dilerseniz hiç vakit kaybetmeden örneğimize başlayalım. Bu seferki örneğimizi LINQ Windows Application projesi olarak geliştireceğiz. Nitekim, DataTable içeriğini ekranda görsel olarak ele alabileceğimiz bir ortam olayları daha net algılayabilmemizi sağlayacaktır. Elbetteki bu makalede bahsedilen işlemleri gerçekleştirebilmek için sistemimizde LINQ Preview sürümünün yüklü olması gerektiğini unutmayalım.

Herhangibir DataTable üzerinden LINQ sorguları çalıştırabilmemiz için System.Data.Extensions isimli kütüphanenin program içerisinde referans edilmiş olması yeterlidir. Çalışmakta olduğumuz LINQ Windows uygulaması bu referansı varsayılan olarak içermektedir.

Herşeyden önce uygulamamızın bellek üzerinde DataSet ve DataTable nesnelerine sahip olması gerekiyor. Bu amaçla makalemizde AdventureWorks ve Northwind veritabanlarından yararlanacağız. DataTable nesnelerimizi doldurmak için başvurabileceğimiz iki yol var. Bunlardan birisi, standart Ado.Net tiplerinden ve fonksiyonelliklerinden yararlanmak. Bir başka deyişle, DataAdapter tipi ve Fill metodundan bahsediyoruz. Ancak bu kez biraz daha farklı olarak entity nesnelerinden faydalanacağız. Hatırlarsanız DLINQ konusunu incelediğimiz makalemizde,  bir database ve içerisindeki tablolar için otomatik olarak entity hazırlayabilmemizi sağlayan SqlMetal isimli bir aracın LINQ Preview projesi ile birlikte geldiğinden bahsetmiştik. Bizim için gereken Entity sınıflarını oluşturması için SqlMetal aracını aşağıdaki gibi kullanıp üretilen .cs dosyalarını projemize eklememiz yeterli olacaktır. (SqlMetal aracına, LINQ Preview' u kurduktan sonra, varsayılan olarak D:\Program Files\LINQ Preview\Bin adresinden ulaşabilirsiniz.)

Dolayısıyla artık entity nesneleri üzerinden DataTable nesne örnekleri içerisine veri doldurma işlemini gerçekleştirebiliriz. Yazacağımız ilk kod parçası, AdventureWorks veritabanı içerisinde yer alan Production şemasındaki Product tablosundan bazı satırların bir DataTable içerisine LINQ sorguları yardımıyla alınması işlemini gerçekleştirecektir. Bu amaçla aşağıdaki kod parçasında olduğu gibi AdventureWorks isimli sınıfımıza ait bir nesne örneği oluşturmamız gerekmektedir.

AdventureWorks adWorks = new AdventureWorks("data source=localhost;database=AdventureWorks;integrated security=SSPI");

Artık entity sınıflarımıza ait nesne örneklerini, adWorks üzerinden kullanabiliriz. Aşağıdaki metod ile, global olarak tanımladığımız adWorks nesnesini kullanarak, yine global olarak tanımladığımız dtUrunler isimli DataTable isimli nesne örneğine veri doldurma işlemi yapılmaktadır. Biz LINQ sorgumuz içerisinden belirli alanları alıp yeni bir isimsiz tip (anonymous type) olarak çekmekteyiz. Elbetteki bu sorgu içerisinde bildiğimiz tüm LINQ imkanlarını kullanabiliriz. Where, order by gibi ifadeler bunlara örnek olarak verilebilir.

private DataTable LoadProductsTable()
{
    var urunler =from prd in adWorks.Production.Product
                        select new {
                                            prd.ProductID
                                            ,prd.Name
                                            ,prd.ListPrice
                                            ,prd.Class
                                            ,prd.SellStartDate
                                            ,prd.SafetyStockLevel
                                            ,prd.StandardCost
                                        };

    DataTable dtUrunler=new DataTable("Urunler");
    dtUrunler=urunler.ToDataTable();
    return dtUrunler;
}

Burada ilk olarak adWorks.Production.Product entity nesnesi üzerinden bir LINQ sorgusu çalıştırılmaktadır. Bunun sonucunda elde edilen veri kümesini bir DataTable içerisine aktarmak için ise tek yapılması gereken ToDataTable isimli metodun çağırılmasıdır. (System.Data.Extensions isim alanı, DataTable ve DataRow' lar için LINQ sorguları hazırlanmasını sağlayan pek çok genişletme metodu içermektedir.)

NOT : Kendi örneklerimizi denerken dikkat etmemiz gereken bir nokta vardır. Özellikle null değer alabilen sayısal ve tarihsel formatlı alanlar için LINQ sorguları aşağıdaki ekran görüntüsünde yer alan çalışma zamanı istisnasına neden olabilmektedir. Örneğin Product tablosunda sayısal ve null değer alabilen bir alan olarak tanımlanmış olan ProductSubCategoryID için bu istisna mesajı elde edilmektedir.

Aynı durum null değerler alabilen varchar, nvarchar tipli alanlar için geçerli değildir. Bunların program ortamı içerisinde yer alan entity sınıfları içerisinde string olarak kullanıldığına ve string' in özellikle referans tipi olduğu için null değer taşıyabildiğine dikkat edelim.

Artık elde ettiğimiz DataTable nesne örneğini herhangibir görsel taşıyıcıya (container) bağlayabiliriz. Bu amaçla .Net 2.0 ile gelen DataGirdView kontrolü biçilmiş kaftandır. Uygulamada bu durumu test etmek için ana formumuzun Load olay metodu içerisinde aşağıdaki örnek kod parçaları yazılmıştır.

adWorks = new AdventureWorks("data source=localhost;database=AdventureWorks;integrated security=SSPI");

dtUrunler = LoadProductsTable();
dgUrunler.DataSource=dtUrunler;

label1.Text = "Ürün Sayısı " + (dgUrunler.Rows.Count-1).ToString();

Programın çalışması sonucu aşağıdaki ekran görüntüsünü elde ederiz. Dikkat ederseniz Product tablosundan 504 adet ürün bilgisi yüklenmiştir.

Asıl amacımız elbetteki DataTable nesne örneğini doldurmak değildir. Özellikle şunu tekrar belirtmekte fayda vardır. Örneğimizde Entity tipleri üzerinden veri çekme işlemi yapılmıştır. Pekala bunu DataAdapter yardımıyla da gerçekleştirebiliriz. Ancak asıl yapmak istediğimiz veriyi bağlantısız katmana nasıl aldığımız değil, bellekte veri taşıyan DataTable üzerinden LINQ sorgularını nasıl çalıştırabileceğimizdir. Nitekim LINQ, DataTable veya DataSet içerisine verinin nasıl çekildiği ile ilgilenmez. Bu amaçla örneğin yukarıdaki sonuçları döndüren DataTable bileşenimizin içerisinde üretim tarihi (SellStartDate alanının değeri) bellirli bir zamandan sonra olanları bulmak istediğimizi düşünelim. Söz konusu sorgu için aşağıdaki gibi bir kod parçasını kullanabiliriz.

var sorgulanabilirUrunler = dtUrunler.ToQueryable();

var sonuclar=from prd in sorgulanabilirUrunler
                        where prd.Field<DateTime>("SellStartDate")>=dateTimePicker1.Value
                            select new {
                                                ProductID=prd.Field<int>("ProductID")
                                                ,Name=prd.Field<string>("Name")
                                                ,ListPrice=prd.Field<decimal>("ListPrice")
                                                ,Class=prd.Field<string>("Class")
                                                ,SellStartDate=prd.Field<DateTime>("SellStartDate")
                                                ,SafetyStockLevel=prd.Field<short>("SafetyStockLevel")
                                                ,StandartCost=prd.Field<decimal>("StandardCost")
                                            };

dgUrunler.DataSource=sonuclar.ToDataTable();

label1.Text="Ürün Sayısı "+(dgUrunler.Rows.Count-1).ToString();

Dikkat edeceğimiz ilk nokta ToQueryable metodunun kullanılmasıdır. Bu metodun tek amacı DataTable üzerinde LINQ sorgularının çalıştırılabilmesini sağlamaktır. Aslında ToQueryable, ToDataTable, Field<T> gibi metodlar, System.Data.Extensions.dll içerisinde gelen genişletme metodlarıdır. Bunları görmek için her hangibir decompiler aracını kullanabiliriz. Örneğiz XenoCode Fox 2007 Community Edition aracı yardımıyla System.Data.Extensions.dll içeriğine bakacak olursak aşağıdaki sonuçları alırız.

Gördüğünüz gibi DataTable için ToQueryable ve ToDataTable metodları, DataRow tipi için Field<T> metodu vb... yer almaktadır. Field<T> metodu, sorgulanabilir hale getirlmiş olan DataTable içerisindeki DataRow dizileri üzerinden istenen alanın elde edilebilmesi amacıyla kullanılmaktadır. Dikkat ederseniz generic bir metoddur ve tip olarakta, çekilen alanın veri tipini almaktadır. Bu tipin elbetteki doğru girilmesi şarttır. Aksi takdirde derleme zamanı hataları alırız.

Peki, sorgumuz tam olarak ne yapmaktadır? Tahmin edeceğiniz gibi where anahtar kelimesi sayesinde SellStartDate alanının değeri DateTimePicker kontrolünde seçilen tarihten sonra gelen satırlar çekilmektedir. Buradaki where cümlesinde yer alan prd.Field<DateTime>("SellStartDate")>=dateTimePicker1.Value ifadesinin söz konusu DataTable içerisindeki her bir DataRow için çalıştığını unutmayalım. Bunu daha kolay idrak edebilmek için bu tip bir gereksinimi LINQ olmadan eski usuller ile yazmak istediğinizi düşünün. Tüm satrıları gezeceğimiz bir döngü yazmamız gerektiğini tahmin edebiliriz. Sonuç itibariyle kodumuzu çalıştırdığımızda aşağıdaki veri kümesini elde ederiz.

Sorgularımızı çeşitlendirebiliriz. Öyleki artık elimizdeki nesne, DataTable üzerinden elde edilmiş sorgulanabilir bir DataRow kümesinden başka bir şey değildir ve LINQ ifadelerine doğrudan destek vermektedir. Şimdi işlemlerimizi biraz daha ilerletelim. Örneğin birbiriyle ilişkili olabilen iki DataTable üzerinde LINQ yardımıyla bir Join işlemi gerçekleştirmeye çalışalım. Bu amaçla Northwind veritabanında yer alan Order ve OrderDetails tablolarından faydalalanbiliriz. Öncelikle bu tabloları entity nesnelerimize alacağız ve sonrasında ise DataTable nesne örneklerine yükleyeceğiz. Son olarakta bu iki DataTable örneğine ait sorgulanabilir bir nesne üzerinden LINQ yardımıyla bir Join işlemi gerçekleştireceğiz. Bu amaçla programımıza aşağıdaki metodları ekleyelim.

private DataTable LoadOrdersTable()
{
    var siparisler=from s in north.Orders
                            select new {
                                                s.OrderID
                                                ,s.ShipAddress
                                                ,s.ShipCity
                                                ,s.ShipRegion
                                                ,s.ShipPostalCode
                                                ,s.ShipCountry
                                            };

    return siparisler.ToDataTable();
}

private DataTable LoadOrderDetailsTable()
{
    var siparisDetaylari=from d in north.OrderDetails
                                    select new {
                                                        d.OrderID
                                                        ,d.UnitPrice
                                                        ,d.Quantity
                                                    };
       
    return siparisDetaylari.ToDataTable();
}

Metodlarımız sırasıyla north isimli global olarak tanımlanmış entity nesnesi üzerinden hareket ederek Orders ve OrderDetails tablolarından belleğe bazı alanlar için veri çekmektedir. Son olarak elde edilen sonuç kümeleri ToDataTable metodu yardımıyla geri döndürülüyor. Şimdi bu iki veri kümesininde OrderID alanları üzerinden birbirlerine bağlı olduğunu biliyoruz. Dolayısıyla birleştirme işlemini gerçekleştireceğimiz sorgu cümesinde bu durumu göz önüne almamız gerekiyor. Bu amaçla aşağıdaki gibi bir kod parçasından faydalanabiliriz.

AdventureWorks adWorks;
Northwind north;
DataTable dtUrunler, dtSiparisler, dtSiparisDetaylari;

private void Form1_Load(object sender, EventArgs e)
{
    adWorks = new AdventureWorks("data source=localhost;database=AdventureWorks;integrated security=SSPI");
    north = new Northwind("data source=localhost;database=Northwind;integrated security=SSPI");

    dtUrunler = LoadProductsTable();
    dgUrunler.DataSource = dtUrunler;

    label1.Text = "Ürün Sayısı " + (dgUrunler.Rows.Count - 1).ToString();

    dtSiparisler = LoadOrdersTable();
    dtSiparisDetaylari = LoadOrderDetailsTable();
   
    dgSiparisler.DataSource = dtSiparisler;
    dgSiparisDetaylari.DataSource = dtSiparisDetaylari;
}

private void btnJoin_Click(object sender, EventArgs e)
{
    var sorgulanabilirOrders = dtSiparisler.ToQueryable();
    var sorgulanabilirOrderDetails = dtSiparisDetaylari.ToQueryable();

    var sonuclar=from o in sorgulanabilirOrders
                            join od in sorgulanabilirOrderDetails
                                on o.Field<int>("OrderID") equals od.Field<int>("OrderID")
                                    select new {
                                                        SiparisID=o.Field<int>("OrderID")
                                                        ,BirimFiyat=od.Field<decimal>("UnitPrice")
                                                        ,Miktar=od.Field<short>("Quantity")
                                                        ,Sehir=o.Field<string>("ShipCity")
                                                        ,Ulke=o.Field<string>("ShipCountry")
                                                    };

    dgJoin.DataSource=sonuclar.ToDataTable();
}

LINQ mimarisinde kullandığımız Join kalıbını burada da aynen kullanmaktayız. Tek dikkat etmemiz gereken, generic Field<T> metodunu nasıl ele aldığımızdır. o takma adı ile siparişleri tutan sorgulanabilir DataRow nesne dizisini (sorgulanabilirOrders), od takma adı ilede sipariş detaylarını tutan sorgulanabilir DataRow nesne dizisini (sorgulanabilirOrderDetails) ifade etmekteyiz. Buna göre join işlemini OrderID alanları üzerinden gerçekleştiren ifademiz aşağıdaki gibidir. Burada her iki DataRow dizisindeki ilgili alanların eşitliğine göre bir kıstas getirilmektedir.

on o.Field<int>("OrderID") equals od.Field<int>("OrderID")

Programımızı çalıştırdığımızda aşağıdakine benzer bir ekran görüntüsü ile karşılaşırız. (TabPage' in üst tarafında yer alan iki DataGridView bileşeni, sırasıyla Orders ve OrderDetails bilgilerini göstermektedir.)

Dilersek join ile yazmış olduğumuz sorgumuza where ile başka kısıtlamalarda katabiliriz. Örneğin, elde edilen sonuç kümesinde Quantity alanının değeri 10' un üzerinde olanları elde etmek için tek yapmamız gereken sorgumuzu aşağıdaki gibi genişletmek olacaktır.

var sonuclar=from o in sorgulanabilirOrders
                            join od in sorgulanabilirOrderDetails
                                on o.Field<int>("OrderID") equals od.Field<int>("OrderID")
                                              where od.Field<short>("Quantity")>10

                                    select new {
                                                        SiparisID=o.Field<int>("OrderID")
                                                        ,BirimFiyat=od.Field<decimal>("UnitPrice")
                                                        ,Miktar=od.Field<short>("Quantity")
                                                        ,Sehir=o.Field<string>("ShipCity")
                                                        ,Ulke=o.Field<string>("ShipCountry")
                                                    };

Where ifadesinde ilgili alanın değerinin karşılaştırma işlemine tabi tutmak için yine Field<T> generic metodundan faydalandığımızda dikkat edelim.

İstersek join ile yaptığımız birleştirme işlemini, içerisinde DataRelation nesnesi barındıran bir DataSet üzerinden de gerçekleştirebiliriz. Bu sefer devreye üst tablodaki herhangibir satıra bağlı alt satırların getirilmesini sağlayacak GetChildRows isimli bir fonksiyonellik gelecektir. Durumu daha iyi anlayabilmek için aşağıdaki kod parçasını göz önüne alabiliriz.

DataSet ds = new DataSet();
ds.Tables.Add(dtSiparisler);
ds.Tables.Add(dtSiparisDetaylari);

DataRelation drOrdToDtl = new DataRelation("OrdToDetails", dtSiparisler.Columns["OrderID"],dtSiparisDetaylari.Columns["OrderID"]);
ds.Relations.Add(drOrdToDtl);

var sorgulanabilirOrders=dtSiparisler.ToQueryable();

var sonuclar=from o in sorgulanabilirOrders
                        from od in o.GetChildRows("OrdToDetails")
                            select new {
                                                SiparisID=o.Field<int>("OrderID")
                                                ,BirimFiyat=od.Field<decimal>("UnitPrice")
                                                ,Miktar=od.Field<short>("Quantity")
                                                ,Sehir=o.Field<string>("ShipCity")
                                                ,Ulke=o.Field<string>("ShipCountry")
                                            };

dgJoin.DataSource=sonuclar.ToDataTable();

DataSet içerisinde yer alan, dtSiparisler ve dtSiparisDetaylari isimli DataTable nesnelerinin işaret ettiği veri kümeleri arasındaki ilişkimiz OrderID alanları üzerinden Orders' dan OrderDetails' e doğrudur. Bunu DataSet içerisinde tanımlayan ise Ado.Net' in ilk çıkışından beri bildiğimiz DataRelation nesnesidir. LINQ sorgumuz, bu nesneyi GetChildRows isimli metod içerisinde parametre olarak kullanmaktadır. Böylece o takma adı ile temsil edilen sorgulanabilirOrders içerisindeki her bir DataRow için bu ilişki kullanılabilmektedir. Bu da doğal olarak, ilişkinin diğer ucunda yer alan siparişe ait detay bilgisinin elde edilebilmesi anlamına gelmektedir. LINQ sorgumuz iki adet from anahtar kelimesi içerdiğinden sonuç doğal olarak bir Join sorgusunun çıktısı ile aynı olacaktır. Uygulamamızı bu haliyle çalıştırdığımızda ilk yazdığımız join sorgusundakine benzer sonuçları elde ederiz.

DataTable ve DataSet' ler üzerinde ToQueryable, ToDataTable, Field<T> metodları dışında, LoadSequence, DistinctRows, EqualAllRows, UnionRows, IntersectRows, ExceptRows, SetField<T> isimli metodlarda kullanılabilmektedir. Bu metodların temel amacı, DataTable ve DataRow gibi nesneler üzerinde LINQ tekniklerinin daha da genişletilmesini sağlamaktır. Örneğin LoadSequence metodu sayesinde herhangibir sorgu sonucu elde edilen kümeyi bir var olan bir DataTable içerisine ilave edebiliriz. Bu metod ve diğerleri hakkında daha fazla bilgi almak için LINQ dökümantasyonundan faydalanabilirsiniz.

Böylece geldik bir makalemizin daha sonuna. Bu makalemizde, LINQ' yu DataTable gibi bağlantısız katman nesneleri üzerinde nasıl kullanabileceğimizi incelemeye çalıştık. Bir sonraki makalemizde görüşmek dileğiyle hepinize mutlu günler dilerim.

Örnek Uygulama İçin Tıklayınız.

Tags:  
Categories:   C# 3.0 | LINQ
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share