LINQ to SQL ile İleri Arama – PredicateBuilder

Standard

İleri arama (daha bilindik adı ile advanced search) kullanıcıların büyük miktarlardaki veriyi istedikleri şekilde filtrelemesini sağlar. Tipik bir ileri arama ekranı pek çok metin kutusu ve/veya liste bulundurur ve kullanıcılar bunların arasından istediklerini doldurup istediklerini boş bırakarak arama yaparlar. Bu yazımda birlikte bir veritabanından LINQ to SQL ile bu şekilde ileri arama yapabilen bir Silverlight uygulaması oluşturacağız.

Arkaplanında bir veritabanı olan uygulamamızda böyle bir ekran oluşturmak istediğimizde, ilk bakışta LINQ to SQL kullanarak “Select From Where” ile istediğimiz sonuca ulaşmamız gayet kolaymış gibi gelebilir. Ancak şimdi üzerinden gideceğimiz örneğe baktığımızda, bazı alanların boş bazılarının dolu olmasının işleri nasıl karıştırdığını ve sorgumuzun “Where” ifadesini dinamik olarak oluşturmamız gerektiğini göreceksiniz.

Gerçek hayatta futbolu pek sevmem ancak burada güzel bir örnek olduğu için onu kullanıyorum. Diyelim ki futbol oyuncularını bir veritabanında tutuyoruz, ve kullanıcıların gelip bu veritabanından oyuncunun adına, soyadına, forma numarasına, takımına ve oynadığı konuma göre arama yapmasını istiyoruz. Gözümüzde canlanması için aşağıdaki arayüzü veriyorum.

Normalde sadece Ad alanı olsaydı aşağıdaki gibi bir LINQ sorgusu yazmamız gerekirdi:

                    if (!String.IsNullOrEmpty(adTextBox.Text))
                    {
                        var aramaSonucu = from oyuncu in veri.Oyuncus
                                          where oyuncu.Ad.Contains(adTextBox.Text)
                                          select oyuncu;
                    }

Görüldüğü üzere gayet basit bir şekilde aramamızı yapabiliyoruz. Ama, Ad ve Soyad’ı birlikte ele alırsak, bu sefer kodu aşağıdaki şekilde yazmamız gerekecek:

                    if (!String.IsNullOrEmpty(adTextBox.Text) && String.IsNullOrEmpty(soyadTextBox.Text))
                    {
                        var aramaSonucu = from oyuncu in veri.Oyuncus
                                          where oyuncu.Ad.Contains(adTextBox.Text)
                                          select oyuncu;
                    }
                    else if (String.IsNullOrEmpty(adTextBox.Text) && !String.IsNullOrEmpty(soyadTextBox.Text))
                    {
                        var aramaSonucu = from oyuncu in veri.Oyuncus
                                          where oyuncu.Soyad.Contains(soyadTextBox.Text)
                                          select oyuncu;
                    }
                    else if (!String.IsNullOrEmpty(adTextBox.Text) && !String.IsNullOrEmpty(soyadTextBox.Text))
                    {
                        var aramaSonucu = from oyuncu in veri.Oyuncus
                                          where oyuncu.Ad.Contains(adTextBox.Text) && oyuncu.Soyad.Contains(soyadTextBox.Text)
                                          select oyuncu;
                    }

Sorunun ne olduğu bu kodda açığa çıkıyor. Ad ve Soyad alanlarının doldurulup doldurulmadığı ile ilgili tüm ihtimalleri teker teker if – else içinde ele almamız ve sorgumuzu ona göre yazmamız gerekti. Bu tüm ihtimalleri değerlendirmemizin sebebi hangi alanların aramaya dahil edilip edilmeyeceğini belirlemek, ama bu da her durum için ayrı bir sorgu yazmamızı gerektiriyor. Aranabilecek alan sayısı arttıkça, ki bizim örneğimizdeki 5 alanın bile kombinasyonlarını yazmayı denediğimizde 100’ün üzerinde ayrı sorgu gerekiyor, işin içinden çıkılmaz hale geliyor.

Peki, bu sorunun önüne nasıl geçebiliriz? Alternatiflerden biri, tüm oyuncular listesini çekip, ardından aşağıdaki şekilde filtrelemek:

                    var aramaSonucu = from oyuncu in veri.Oyuncus
                                      select oyuncu;

                    if (!String.IsNullOrEmpty(adTextBox.Text))
                    {
                        aramaSonucu.Where(e => e.Ad == adTextBox.Text);
                    }
                    if (!String.IsNullOrEmpty(soyadTextBox.Text))
                    {
                        aramaSonucu.Where(e => e.Soyad == soyadTextBox.Text);
                    }

Bu kod önceki sorunumuzu engelleyerek sonucumuzu çok miktarda if – else yazmamıza gerek kalmadan filtreliyor. Ancak bu da performans açısından çok verimsiz. Çünkü ilk önce tüm veriyi çekip, ardından aradığımız her alan için tekrar sorgu yapıyoruz. Örneğimiz üzerinden gidersek, detaylı aramayı yapmak için en kötü ihtimalde 6 kez sorgu yapmamız gerekiyor.

Aramayı tek sorguda yapmak için, Where koşulumuzu dinamik olarak oluşturmalıyız. İşte bunu gerçekleştirebilmek için de predicate ve PredicateBuilder sınıfını kullanmamız gerekli.

Predicate ve PredicateBuilder

Öncelikle predicate’ın ne olduğu ile başlayalım. Buradaki kelime anlamı “karşılaştırma belirtimi” olan predicate, aslında Where içine yazdığımız ifadenin kendisi. Aşağıdaki kod örneğinde aynı sorgunun hem predicate, hem de select – from – where ile yazılmış hali mevcut.

            var aramaSonucu = from oyuncu in veri.Oyuncus
                              where oyuncu.Ad == adTextBox.Text
                              select oyuncu.Ad;

            Func<Oyuncu, bool> predicate = c => c.Ad == adTextBox.Text;
            var aramaSonucu = veri.Oyuncus.Where(predicate);

Gördüğünüz üzere predicate’leri bir değişken gibi belirleyerek Where içine gönderebiliyoruz. Sıra bu predicate’leri amacımıza uygun bir şekilde kullanmaya geldi.

Burada devreye PredicateBuilder sınıfı giriyor. PredicateBuilder, aslında .NET Framework’ün kendisinde bulunmayan, bu adresten kaynak koduna ulaşabileceğiniz özel bir sınıf. Bu sınıfın bize sağladığı özellik ise, birden çok predicate’ı birbirine AND veya OR ifadeleri ile bağlayarak tek bir predicate haline getirebilmesi. Bu sayede de Where içindeki koşulumuzu çalışma zamanında dinamik olarak oluşturabiliyoruz.

O halde, uygulamamıza geçelim. Bu örnek için bize web service aracılığıyla veritabanına bağlanan bir Silverlight uygulaması gerekli, ancak bunu sıfırdan adım adım oluşturmak uzun sürecek ve yazıyı konusundan uzaklaştıracak. Bu nedenle, uygulamanın arayüzü ve arkaplanı tamamlanmış, içine kod yazmaya hazır halini size sağlayarak oradan ilerlemeyi daha uygun gördüm. Buradan uygulamayı indirip Visual Studio 2012 içinde açarak devam edebilirsiniz.

Ayrıca, eğer isterseniz buradan da İbrahim Kıvanç’ın web service aracılığıyla veritabanına bağlanan bir Silverlight uygulaması oluşturmayla ilgili 6 bölümlük serisine ulaşabilirsiniz.

PredicateApplication uygulamasını açtığımızda, Solution Explorer’da aşağıdaki dosyaları görüyoruz. Buradan PredicateApplication.Web projesi içerisindeki WebService.asmx.cs dosyasını açalım.

Burada Silverlight uygulamamızın kullanacağı WebMethod’ları tanımlıyoruz. Bizim uygulamamızın sadece tek bir web metoduna ihtiyacı var. Aşağıdaki şekilde hem veritabanının DataContext nesnesini, hem de detaylı arama web metodunu tanımlıyoruz.

    public class WebService : System.Web.Services.WebService
    {
        PredicateDataClassesDataContext veri = new PredicateDataClassesDataContext();

        [WebMethod]
        public List<Oyuncu> detailedSearch(Oyuncu arananOyuncu)
        {

        }
    }

“Oyuncu” sınıfı, adı üzerinde, bir futbol oyuncusunu temsil eden, veritabanımızdaki Oyuncu tablosuna karşılık gelen sınıf. Az önce yarattığımız detailedSearch metodu arananOyuncu adında bir Oyuncu kabul ediyor. Bu “arananOyuncu”, bizim arama değerlerimizi içerecek.

Şimdi de PredicateBuilder sınıfını ekleyelim. WebService.asmx.cs dosyası içinde, öncelikle System.Linq.Expressions ad alanını ekledikten sonra, aşağıdaki kodu PredicateApplication.Web namespace’i içine kopyalayıp yapıştırıyoruz.

using System.Linq.Expressions;
public static class PredicateBuilder
{
  public static Expression<Func<T, bool>> True<T> ()  { return f => true;  }
  public static Expression<Func<T, bool>> False<T> () { return f => false; }
 
  public static Expression<Func<T, bool>> Or<T> (this Expression<Func<T, bool>> expr1,
                                                      Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters);
  }
 
  public static Expression<Func<T, bool>> And<T> (this Expression<Func<T, bool>> expr1,
                                                       Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters);
  }
}

Kodumuzun en can alıcı kısmına geldik. PredicateBuilder kullanarak metodumuzun içini aşağıdaki gibi dolduruyoruz.

        [WebMethod]
        public List<Oyuncu> detailedSearch(Oyuncu arananOyuncu)
        {
            var oyuncuPredicate = PredicateBuilder.True<Oyuncu>();

            if (!String.IsNullOrEmpty(arananOyuncu.Ad))
            {
                oyuncuPredicate = oyuncuPredicate.And(e => e.Ad.Contains(arananOyuncu.Ad));
            }
            if (!String.IsNullOrEmpty(arananOyuncu.Soyad))
            {
                oyuncuPredicate = oyuncuPredicate.And(e => e.Soyad.Contains(arananOyuncu.Soyad));
            }
            if (!String.IsNullOrEmpty(arananOyuncu.Konum))
            {
                oyuncuPredicate = oyuncuPredicate.And(e => e.Konum == arananOyuncu.Konum);
            }
            if (!String.IsNullOrEmpty(arananOyuncu.Takım))
            {
                oyuncuPredicate = oyuncuPredicate.And(e => e.Takım.Contains(arananOyuncu.Takım));
            }
            if (arananOyuncu.No != null)
            {
                oyuncuPredicate = oyuncuPredicate.And(e => e.No == arananOyuncu.No);
            }

            var aramaSonucu = veri.Oyuncus.Where(oyuncuPredicate);

            return aramaSonucu.ToList<Oyuncu>();
        }
    }

Burada ilk önce PredicateBuilder kullanarak bir oyuncuPredicate değişkeni oluşturduk. Bu değişkeni oluştururken seçtiğimiz “True”, başlangıçta tüm değerlerin seçileceğini, yani bir koşul uygulanmadığını gösteriyor. Daha sonraki if ifadeleri içinde ise arama değerlerini tek tek kontrol ederek eğer aramaya katılacaklarsa predicate sonuna And operatörü ile ekledik.

Sıra uygulamanın Silverlight tarafını düzenlemeye geldi. Yukarıdaki değişiklikleri yaptıktan sonra ilk önce projemizi derleyelim ve ardından da Solution Explorer içinden PredicateApplication projesi altında WebServiceRefence’a sağ tıklayarak “Update Service Reference” seçelim. Bu, Web tarafında yaptığımız değişikliklerin Silverlight tarafında görülmesini sağlayacak.

Daha sonra, PredicateApplication projesi içinde MainPage.xaml.cs dosyasını açarak içine aşağıdaki şekilde kodları ekleyelim:

using PredicateApplication.WebServiceReference;
    public partial class MainPage : UserControl
    {
        WebServiceSoapClient webService = new WebServiceSoapClient();

        public MainPage()
        {
            InitializeComponent();

            konumComboBox.Items.Add("");
            konumComboBox.Items.Add("Kaleci");
            konumComboBox.Items.Add("Defans");
            konumComboBox.Items.Add("Orta Saha");
            konumComboBox.Items.Add("Forvet");

            webService.detailedSearchCompleted +=webService_detailedSearchCompleted;
        }

        void webService_detailedSearchCompleted(object sender, detailedSearchCompletedEventArgs e)
        {
            MessageBox.Show("Arama tamamlandı");
            oyuncularDataGrid.ItemsSource = e.Result;
        }
    }

Bu kodda web service için değişken ve eventi tanımlamanın yanı sıra, basit bir şekilde arayüzümüzdeki ComboBox’ı doldurduk. Completed event, arama metodunu çağırdıktan sonra metodun yürütülmesi bitince çalışacak kod kısmımız. Web metod çağrılarımız asynchronous çalıştığı için tamamlandıklarında bir uyarı almamız kodun çalışıp çalışmadığını anlamamıza yardımcı olacak. Şimdi “Ara” düğmesine tıklandığında çalışacak olan metodu aşağıdaki gibi dolduralım.

        private void araButton_Click(object sender, RoutedEventArgs e)
        {
            Oyuncu arananOyuncu = new Oyuncu();

            if(!String.IsNullOrEmpty(adTextBox.Text))
            {
                arananOyuncu.Ad = adTextBox.Text;
            }
            if(!String.IsNullOrEmpty(soyadTextBox.Text))
            {
                arananOyuncu.Soyad = soyadTextBox.Text;
            }
            if(!String.IsNullOrEmpty(numaraTextBox.Text))
            {
                arananOyuncu.No = Int32.Parse(numaraTextBox.Text);
            }
            if(konumComboBox.SelectedItem != null)
            {
                if(konumComboBox.SelectedValue.ToString() != "")
                {
                    arananOyuncu.Konum = konumComboBox.SelectedValue.ToString();
                }
            }
            if(!String.IsNullOrEmpty(takımTextBox.Text))
            {
                arananOyuncu.Takım = takımTextBox.Text;
            }
            
            webService.detailedSearchAsync(arananOyuncu);
        }

Bu kodları da yazdığımızda artık uygulamamızı çalıştırabiliriz. Ekrandaki alanlardan istediklerimizi doldurarak Ara düğmesine bastığımızda sonuç yandaki DataGrid içinde görüntülenecek.

Ufak bir not olarak, veritabanına yalnızca Galatasaray ve Beşiktaş’ın toplam 57 oyuncusunu koydum (Beşiktaş’ınki güncel bile değil :D). Ona göre doğru sonuç beklentiniz olmasın.

Uygulamanın son haline buradan ulaşabilirsiniz.

Gelecek yazılarımda görüşmek üzere. 🙂

Advertisements

5 thoughts on “LINQ to SQL ile İleri Arama – PredicateBuilder

  1. mustafa burak kayabal

    var aramaSonucu = veri.Oyuncus.Where(oyuncuPredicate);
    diye bir metod yok !
    var aramaSonucu = veri.Oyuncus.Where;
    şeklinde bir kullanım var bunu anlayamadım ben

    • Merhaba,

      Buradaki Where metodu System.Linq tarafından gelen bir extension metodudur. Eğer kodun başına “using System.Linq;” diye eklerseniz o zaman ortaya çıkacaktır. 🙂

      İşlev olarak da, yukarıda bahsettiğim gibi bir predicate kullanarak verilerimizi filtrelememizi sağlar.

  2. ramazan

    Çok saolun proje odevimdi futbolcu ile ilgili bilgi veren program yazacaktım bazı yerlerde zorlandım sizin sayenizde devam ediyorum programa

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s