Gelişmiş haber arama

Ben haber dedim ama siz istediğiniz gibi uyarlayabilirsiniz. :)

Sitelerimizde, arama işlemlerini sıklıkla kullanırız. LINQ + EF (LINQ + Entity Framework) ile çalışan arkadaşlar bu noktada sıkıntıya düşerler. Bu da son derece normaldir. 

LINQ+EF kullanan arkadaşlara tavsiyem şudur: Bilgi ekleme/silme/listeleme işlemleri için LINQ+EF kullanabilirsiniz. Ama arama kısmı için enerjinizi boşu boşuna LINQ+EF ile harcamayın. Eski klasik yönteme geri dönün.

Yani klasik sql cümlecikleri ile bu işi halletmeye çalışın. (http://dalt.in/Fi6q3 linkindeki HelperDB ile ilgili yazım ilginizi çekebilir.)

Kod örneğini sizlerle paylaşmadan önce şunları belirtmeliyim:

  • Katmanlı mimari ile hazırlandı.
  • Sayfalama fonksiyonu eklendi. (nasıl olduğu ile ilgili detaylı açıklamayı daha sonra ayrı bir yazıda yapmaya çalışacağım.)
  • Gerekli yerlerde class kullanımından kaçınılmadı.
  • Yazının başında haber dediğim için veritabanı ve kodlar "haberler" ile ilgili.
  • Veritabanı olarak MySQL kullanıldı. (Scriptler ve örnek datalar için aşağıdaki indirme linkini kullanabilirsiniz.)
  • Örnek haber metinlerine takılmayın. Rastgele veriler olarak kabul edin.
  • Önemli sayfaların kodlarını aşağıda bulabilirsiniz. Geriye kalan tüm dosyalar için sayfanın en altındaki indirme linkini kullanabilirsiniz.
  • GenelIslemler.cs dosyasını daha önceden sizlerle paylaşmıştım.
  • İlla LINQ+EF kullanmak istiyorsanız http://dalt.in/linqkit ve http://dalt.in/g4SGc linklerine göz atabilirsiniz. Ama emin olun ki direkt sql kodlarıyla çalışarak her zaman daha doğru ve hızlı sonuç alırsınız. Ayrıca çok karmaşık aramaları da ne yaparsanız yapın beceremezsiniz. :)
  • Kodlar gözünüze kalabalık / anlaşılmaz gelebilir ilk bakışta.
    Ancak biraz incelerseniz aslında basit bir mantığı olduğunu sadece yazım işleminin biraz meşakkatli olduğunu farkedeceksiniz.
  • Arama parametrelerinde mümkün olduğunca çok veri tipi kullanılmaya çalışıldı. (string, int, datetime ve bool)
  • Katmanlı mimari, sayfalama gibi yazı başlığı ile direkt ilgisi olmayan konulardan bahsedilmedi.
  • Arama parametreleri QueryString'te tutukuyor. HaberAramaInfo sınıfı coding ve encoding işlerini hallediyor.
  • Tasarım incelikleri göz ardı edildi.
  • Listeleme detaylandırılmadı.

Şimdi ilk olarak proje yapısını göstereyim:

Kısaca;

  • DAL - Data Access Layer => Veritabanına erişim için kullanılan class lar bu katmanda.
  • HelperDB => Direkt olarak DB ye erişen ve sql kodlarını çalıştıran katman. (DAL'a gömülü)
  • Info => Veritabanındaki tabloları ve programımızda gerekli olan veri yapılarının modellendiği katman.
  • Web => Sitemiz :)

Gelelim kodları paylaşmaya...

Web.Config - ConnectionString

 <connectionStrings>
    <add name="HaberConnection" connectionString="server=localhost;User Id=root;password=xxx;database=haberdb;charset='latin5';" providerName="MySql.Data.MySqlClient"/>
  </connectionStrings>

HelperDB sınıfı, ConnectionString bilgisini web.config ten okumaktadır.


Şimdi de modellemeler...

HaberInfo.cs

namespace DevrimAltinkurt.Info
{
    public class HaberInfo
    {
        public int Id { get; set; }
        public string Baslik { get; set; }
        public string Ozet { get; set; }
        public string Ayrinti { get; set; }
        public DateTime Tarih { get; set; }
        public int Okunma { get; set; }
        public int IlId { get; set; }
        public bool Flash { get; set; }
    }
}

HaberAramaInfo.cs

namespace DevrimAltinkurt.Info
{
    public class HaberAramaInfo
    {
        public string AramaMetni { get; set; }
        public DateTime Tarih1 { get; set; }
        public DateTime Tarih2 { get; set; }
        public int IlId { get; set; }
        public string _Il { get; set; }
        public int Flash { get; set; }

        public HaberAramaInfo()
        {
            Tarih1 = DateTime.MinValue;
            Tarih2 = DateTime.MaxValue;
        }
        public string ToQueryString()
        {
            string s = "";

            if (!string.IsNullOrEmpty(this.AramaMetni))
                s += "|aramametni|" + this.AramaMetni;
            if (Tarih1 > DateTime.MinValue)
                s += "|tarih1|" + this.Tarih1.ToShortDateString().Replace("/", "-");
            if (Tarih2 < DateTime.MaxValue)
                s += "|tarih2|" + this.Tarih2.ToShortDateString().Replace("/", "-");
            if (this.IlId > 0)
                s += "|ilId|" + this.IlId;
            if (!string.IsNullOrEmpty(this._Il))
                s += "|il|" + this._Il;
            if (this.Flash > 0)
                s += "|flash|" + this.Flash;

            return s;
        }

        public void FromQueryString(string s)
        {
            List<String> s2 = new List<string>(s.Split('|'));

            int sira = s2.IndexOf("aramametni");
            if (sira > -1 && sira % 2 == 1) this.AramaMetni = s2[sira + 1];

            sira = s2.IndexOf("tarih1");
            if (sira > -1 && sira % 2 == 1) this.Tarih1 = Convert.ToDateTime(s2[sira + 1].Replace("-", "/"));

            sira = s2.IndexOf("tarih2");
            if (sira > -1 && sira % 2 == 1) this.Tarih2 = Convert.ToDateTime(s2[sira + 1].Replace("-", "/"));

            sira = s2.IndexOf("flash");
            if (sira > -1 && sira % 2 == 1) this.Flash = Convert.ToInt32(s2[sira + 1]);

            sira = s2.IndexOf("ilId");
            if (sira > -1 && sira % 2 == 1) this.IlId = Convert.ToInt32(s2[sira + 1]);
        }
    }
}

HaberDB

namespace DevrimAltinkurt.DAL
{
    public class HaberDB
    {
        HelperDB helperDB = new HelperDB();

        public List<HaberInfo> TumHaberleriGetir(int baslangic, int adet)
        {
            List<HaberInfo> haberler = new List<HaberInfo>();
            string sql = "select * from haberler order by Id desc limit ?Baslangic, ?Adet";
            var pars = new MySqlParameter[]{
                new MySqlParameter("?Baslangic", MySqlDbType.Int32),
                new MySqlParameter("?Adet" , MySqlDbType.Int32)
            };
            pars[0].Value = baslangic;
            pars[1].Value = adet;
            MySqlDataReader dr = helperDB.ExecuteReader(sql, pars);
            while (dr.Read())
            {
                haberler.Add(new HaberInfo
                {
                    Ayrinti = Genel.GetString(dr, "Ayrinti"),
                    Baslik = Genel.GetString(dr, "Baslik"),
                    Flash = Genel.GetBoolean(dr, "Flash"),
                    Id = Genel.GetInteger(dr, "Id"),
                    IlId = Genel.GetInteger(dr, "IlId"),
                    Okunma = Genel.GetInteger(dr, "Okunma"),
                    Ozet = Genel.GetString(dr, "Ozet"),
                    Tarih = Genel.GetDateTime(dr, "Tarih")
                });
            }
            dr.Close();
            return haberler;
        }
    }
}

Gelelim asp.net sayfalarımıza...

Arama Paneli

<asp:Panel ID="pnlArama" runat="server" DefaultButton="btnAra">
    <b>Haber Arama</b>
    <hr />
    Arama metni
    <br />
    <asp:TextBox ID="txtAramaMetni" runat="server" />
    <hr />
    Kayıt Tarihi
    <br />
    <asp:TextBox ID="txtTarih1" runat="server" Width="70" />
    »
    <asp:TextBox ID="txtTarih2" runat="server" Width="70" />
    <hr />
    İlgili il
    <br />
    <asp:DropDownList ID="ddlIller" runat="server" />
    <hr />
    Flash Haber
    <br />
    <asp:RadioButtonList ID="rblFlash" runat="server" RepeatDirection="Horizontal" RepeatLayout="Flow">
        <asp:ListItem Text="Tümü" Value="0" Selected="True" />
        <asp:ListItem Text="Olanlar" Value="1" />
        <asp:ListItem Text="Olmayanlar" Value="2" />
    </asp:RadioButtonList>
    <hr />
    <asp:Button ID="btnAra" runat="server" Text="Ara" OnClick="btnAra_Click" />
    |
    <asp:LinkButton ID="lbTumunuGoster" runat="server" Text="Tümünü göster" OnClick="lbTumunuGoster_Click" />
</asp:Panel>

Haber Listeleme

<asp:GridView ID="gvHaberler" runat="server" AutoGenerateColumns="false">
    <Columns>
        <asp:BoundField DataField="Id" HeaderText="Id" />
        <asp:BoundField DataField="Baslik" HeaderText="Başlık" />
        <asp:BoundField DataField="Tarih" HeaderText="Tarih" DataFormatString="{0:dd.MM.yyyy}" />
        <asp:BoundField DataField="Il" HeaderText="İl" />
        <asp:CheckBoxField DataField="Flash" HeaderText="Flash" />
    </Columns>
    <EmptyDataTemplate>
        Haber bulunamadı!
    </EmptyDataTemplate>
</asp:GridView>

Arama butonu

protected void btnAra_Click(object sender, EventArgs e)
{
    hai = new HaberAramaInfo();
    hai.AramaMetni = txtAramaMetni.Text.ToTemizMetin();
    hai.Flash = rblFlash.SelectedValue.ToInt32();
    hai.IlId = ddlIller.SelectedValue.ToInt32();
    hai.Tarih1 = txtTarih1.Text.ToTemizMetin().isDate() ? txtTarih1.Text.ToDateTime() : DateTime.MinValue;
    hai.Tarih2 = txtTarih2.Text.ToTemizMetin().isDate() ? txtTarih2.Text.ToDateTime() : DateTime.MaxValue;

    string url = "/Default.aspx?Ara=" + hai.ToQueryString();
    Response.Redirect(url, true);
}

Aramayı gerçekleştiren kod

HaberAramaInfo hai = null;

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        ddlIller.DataSource = new IlDB().IlleriGetir();
        ddlIller.DataTextField = "Il";
        ddlIller.DataValueField = "Id";
        ddlIller.DataBind();
        ddlIller.Items.Insert(0, new ListItem("Seçiniz", "0"));

        string ara = "";
        if (Request.QueryString["Ara"] != null)
        {
            ara = Server.UrlDecode(Request.QueryString["Ara"].ToString());
        }
        HaberleriGetir(ara);
    }
}

private void HaberleriGetir(string aramaBilgisi)
{
    HaberAramaInfo hai = new HaberAramaInfo();
    hai.FromQueryString(aramaBilgisi);

    string sql_liste = 
@"select h.*, i.Il from haberler h
inner join iller i on h.IlId = i.Id
where 1=1";
    string sql_adet = "select Count(Id) from haberler where 1=1";
    string sql = " ";

    #region parametreler
    List<MySqlParameter> pars = new List<MySqlParameter>();
    if (!string.IsNullOrEmpty(hai.AramaMetni))
    {
        sql += " and (" +
                "Baslik like ?AramaMetni ";
        sql += " or Ayrinti like ?AramaMetni ";
        sql += " or Ozet like ?AramaMetni" +
                ") ";
        pars.Add((new MySqlParameter("?AramaMetni", MySqlDbType.String)));
        pars[pars.Count - 1].Value = "%" + hai.AramaMetni.ToTemizMetin() + "%";
        txtAramaMetni.Text = hai.AramaMetni.ToTemizMetin();
    }
    if (hai.IlId > 0)
    {
        sql += " and IlId = ?IlId";
        pars.Add((new MySqlParameter("?IlId", MySqlDbType.Int32)));
        pars[pars.Count - 1].Value =
            ddlIller.SelectedValue = hai.IlId.ToString();
    }
    if (hai.Flash > 0)
    {
        rblFlash.SelectedValue = hai.Flash.ToString();
        sql += " and Flash = ?Flash";
        pars.Add((new MySqlParameter("?Flash", MySqlDbType.Bit)));
        pars[pars.Count - 1].Value = hai.Flash == 1 ? true : false;
        rblFlash.SelectedValue = hai.Flash.ToString();
    }
    if (hai.Tarih1 > DateTime.MinValue)
    {
        sql += " and Tarih > ?Tarih1";
        pars.Add((new MySqlParameter("?Tarih1", MySqlDbType.Date)));
        pars[pars.Count - 1].Value = hai.Tarih1;
        txtTarih1.Text = hai.Tarih1.ToShortDateString();
    }
    if (hai.Tarih2 < DateTime.MaxValue)
    {
        sql += " and Tarih < ?Tarih2";
        pars.Add((new MySqlParameter("?Tarih2", MySqlDbType.Date)));
        pars[pars.Count - 1].Value = hai.Tarih2;
        txtTarih2.Text = hai.Tarih2.ToShortDateString();
    }
    #endregion
    sql += " order by Tarih desc ";

    HelperDB helperDB = new HelperDB();

    int toplam = helperDB.ExecuteScalar(sql_adet + sql, pars.ToArray()).ToInt32();
    UC_Sayfalama1.Toplam = toplam;
    UC_Sayfalama1.Adet = 5;

    //if (toplam == 0)
    //{
    //}
    //else
    //{
    UC_Sayfalama1.Sayfala();

    string sql_tum = sql_liste + sql +
                    string.Format(" limit {0}, {1}",
                        (UC_Sayfalama1.Sayfa - 1) * UC_Sayfalama1.Adet,
                        UC_Sayfalama1.Adet);

    gvHaberler.DataSource = helperDB.ExecuteDataTable(sql_tum, pars.ToArray());
    gvHaberler.DataBind();
    //}
}

Tabii ki using DevrimAltinkurt.DAL ve using DevrimAltinkurt.Info ifadelerini unutmuyorsunuz. Siz kendi istediğiniz namespaceleri kullanabilirsiniz.

Kodlar uzun ama mantık güzel, değil mi?

Şimdi gelelim bu sistemin avantajlarına:

  • Bu sistemi bir kere kullandıktan sonra diğer arama sayfalarını çok hızlı gerçekleştirirsiniz.
  • Arama parametreleri URL'de (QueryString) tutulduğu için istediğiniz forum sayfasına yazabilir, arkadaşlarınıza gönderebilir, facebook/twitter hesaplarınıza ekleyebilirsiniz..
  • URL kısaltma servislerinden faydalanabilirsiniz.
  • Arama butonunda oluşan URL bilgisini bir tabloda tutar, başka bir sayfada da bunları listelerseniz arama motorları açısından güzel bir iş yapmış olursunuz.
  • DateTime, bool, integer, string bilgilerle çalışırken veritipine göre özel önlemler aldığımıızın farkındasınız değil mi? Örneğin tarih bilgisini kullanırken, tarih1 e Minimum, tarih2 ye Maksimum tarih değerlerini atadık. Farklı algoritmalar geliştirmede yardımcı olacağı kesin!!! :)

Dezavantajları:

  • Kodları algılama ilk başta biraz zor olabilir. Ama gelişmiş arama yapmak isteyenler temel seviyenin üstündeki programcılardır diye mantık yürütebilirim.
  • Kodlar sadece MySQL üzerinde çalışır.
  • Parametre tip ve sayıları arttıkça kodlar da uzayacaktır.

Şimdi biraz da ekran görüntüsü paylaşayım:

Lütfen URL i inceleyiniz:

http://localhost:52382/Default.aspx?Ara=|aramametni|m%c3%bcjde|tarih1|20.12.2012|tarih2|01.01.2013|ilId|20|flash|2


Bütün proje kodlarını buradan indirebilirsiniz.

Herkese kolay gelsin.

Not: İleride belki bu uygulamanın LINQ+EF versiyonunu da hazırlayabilirim.