RSS Feeds

4 Ekim 2010 Pazartesi

Veri Modelinin Doğru Tasarlanması ve Sağladığı Avantajlar

Gerek LINQ to SQL, gerekse ADO.NET Entity Framework, uygulamaların geliştirilmesinde kullanılırken öncelikle bir veri modeli oluşturulur. Bu veri modeli, LINQ to SQL’de uzantısı .dbml, EF’de ise .edmx olan dosyalardan ve beraberindeki kod dosyalarından ibarettir. Peki veri modelinin tam olarak görevi nedir? Bu soruyla ilk karşılaşıldığında hemen akla şu basit cevap gelir : “Veri modeli sayesinde çalışma anında CRUD operasyonlarında kullanılan SQL komutları üretilebilir.” Hmm, güzel, fakat tek başına yeterli bir ifade gibi görünmüyor. Gerçek şu ki, eğer veri modeli doğru tasarlanırsa, çok önemli faydalar sağlayabilir. Ayrıca esnek bir yazılım geliştirme ortamı yaratılmasında da büyük rol oynar.

1

Resimde de görüldüğü üzere veri modeli, veritabanı ile çalışan uygulama arasındaki bir katmandan başka birşey değildir. Bu model içerisinde çoğunlukla Entity sınıfları ile birlikte Stored Procedure ya da User Defined Function gibi programatik öğelere karşılık üretilen metotlar bulunmaktadır. Tabi ki bu noktada Entity sınıflarının da kendi üzerinde Identity Column, Primary / Foreign Key vb. bilgileri taşıdığının altını çizmek gerekir. Veri modeli aracılığıyla SELECT, INSERT, UPDATE, DELETE gibi komut cümleleri çalışma anında otomatik üretilebilir. Ayrıca sunucudan gelen sonuç kümelerinin Entity nesneleri halinde belleğe yüklenmeleri de yine veri modeli sayesinde olur. Fakat yazının da başında belirtildiği üzere, doğru oluşturulan bir veri modeli bundan çok daha fazlasını gerçekleştirebilmelidir.

Konuyu bir örnek üzerinde açıklamanın daha faydalı olacağı düşüncesindeyim. Amacımız Kitap ve DVD kayıtlarını tutmak üzere bir veritabanı tasarlamak ve sonrasında da bu veritabanına Entity Framework aracılığıyla erişmek olsun.

Kitap

DVD

UrunID UrunID
Ad Ad
Fiyat Fiyat
KdvOran KdvOran
Yazar Yonetmen
SayfaSayi Sure
BasimYil Tur

Bu varlıkları içerecek bir veritabanı tasarımında da tabi ki farklı seçenekler söz konusudur. Bu seçenekler, özellikle sorgu performansı, veri tutarlılığı ve normalizasyon kuralları gibi çeşitli faktörlerin etkisiyle meydana gelmektedir. Geliştirilecek örnek, bu seçeneklerden iki tanesini içerecek olup, Entity Data Model’in yazılım projelerine kazandırdığı esnekliği daha iyi anlamamızı sağlayacaktır.

Seçenek 1 : Table Per Type Model

2 

Birinci veritabanı tasarımında, adından da belli olduğu üzere her varlığa karşılık bir tablo oluşturulmuş ve bu sayede Kitap ve DVD varlıklarına ait ortak kolonlar Urun tablosunda tanımlanarak, UrunID,Ad,Fiyat ve KdvOran kolonlarının tüm tablolarda tekrarının önüne geçilmiştir. Hmmm, güzel. Tabi ki bu tasarımın çeşitli avantaj ve dezavantajları olduğunu belirtmek gerekir.

Avantajları

  • Kolonlar her tabloda tekrar ettirilmez
  • Gerektiği taktirde kolonlara NULL kısıtlaması getirebilir

Dezavantajları

  • Bir varlığın bilgileri temel bilgileri Urun, kendine özel bilgileri ise ikinci bir tabloda bulunduğundan, dolayısıyla yapılan JOIN’lerden dolayı sorgu performansı düşer

Konumuz bu tasarımların doğruluğunu yanlışığını tartışmak olmadığından, avantaj ve dezavantajlar üzerinde daha fazla durmadan ikinci tasarımı inceleyelim ( Table Per Type modeliyle ilgili daha ayrıntılı bilgi için internette çokça döküman bulunabilmektedir ) .

Seçenek 2 : Table Per Hierarchy Model

3 

İkinci tasarımda ise yalnızca tek tablo bulunması, dikkatleri ilk çeken noktadır. Bu tabloda bulunan UrunTip kolonu ise, kayıtlı ürünün hangi türden (DVD ya da Kitap) bir ürün olduğunun bilgisini içermektedir. Hmmm, e bu da güzelSmile Kısaca, avantaj ve dezavantajlarından da bahsetmek gerekirse :

Avantajları

  • JOIN işlemi olmadan bilgiler elde edildiğinden sorgu performansı daha yüksektir.

Dezavantajları

  • Tüm kolonlar tek tabloda bulunduğundan, eğer ürün tipleri ve bunların özellikleri çok sayıda olursa kolon sayısı çok fazla artabilir. Hatta SQL Server’ın kolon limitine ulaşabilir.
  • Bir kayıt girilirken tüm kolonlar değer almayacağı için, ortak kolonlar dışındakilerin tümü boş geçilebilir ( nullable ) bırakılmalıdır. Bu da veri tutarlılığı açısından bir dezavantaj olarak düşünülmelidir.

İlk akla gelen avantaj ve dezavantajları sıraladıktan sonra, tekrardan konumuzun bu tasarımları tartışmaktan ziyade veri modeli olduğunu hatırlatarak örneğe kaldığımız noktadan hızla devam edelim ( Table Per Hierarchy modeliyle ilgili daha ayrıntılı bilgi için de internette çokça döküman bulunabilmektedirSmile ) .


Örnek Proje

Yazının başında, satır arasından yakalanması gereken, belki de en önemli nokta, Entity Data Model’in esnek bir uygulama ortamı yaratmasındaki rolüdür. Bu rolü daha iyi analiz edebilmek ve doğru bir modellemenin uygulamanın kodlanması sırasında ne gibi yararlar sağladığını anlamak için, hem TPT hem de TPH tasarımlayıla oluşturulan iki farklı veritabanını EF ile kullanmaya çalışacağız.

Geliştireceğimiz örnek proje, basit bir Console uygulaması olacak ve oluşturacağımız projeye iki Entity Data Model ekleyeceğiz. Bu modellerden biri TPT tasarımlı veritabanı, diğeri ise TPH tasarımlı veritabanına erişim sağlayacak. Bu durumda projenin, aşağıdakine benzer bir görüntüye sahip olacağını söyleyebiliriz.

4

TPT Tasarımlı Veritabanını EF ile Kullanmak

Projeye bir ADO.NET Entity Data Model eklendikten sonra oluşturulan Entity sınıflarının ilk görüntüsü aşağıdaki gibi olacaktır.

5

Mevcut EDMX’i analiz etmek gerekirse:

  • SQL’deki her tabloya karşılık bir sınıf oluşturulmuştur.
  • Urun tablosuyla, DVD ve Kitap tabloları arasında bulunan 1’e 1 ilişkisi, sınıflar üzerine de uygulanmıştır.
  • Bir satırın, diğer tabloda bulunan ilişkisel satırına ulaşmak için Navigation Property’ler oluşturulmuştur.

Ancak şunu söylemek gerekir ki, yukarıda, diagramı görülen sınıflar, doğru bir veri modelini kesinlikle ve kesinlikle ifade etmemektedirler! Diğer bir deyişle, sunucudaki veritabanıyla, uygulamanın iş katmanı arasında doğru bir köprü kurulamamaktadır. Bunun en büyük sebebi aslında şudur : Veritabanındaki Urun ismindeki tablo, kolon tekrarını önlemek ve ortak kolonları tek bir tabloda toplamak üzere oluşturulmuştur, aslında üretilen yazılım sisteminde Urun nesneleri olmayacaktır. Kısaca, iş katmanı içerisinde sadece DVD ve Kitap nesneleri oluşmalı, Urun ise soyut bir sınıf olarak tasarlanmalıdır. Hmm, anlaşılan veri modelinin şu anki hali, doğru bir OOP ( Nesne Yönelimli Programlama) tasarımı değildir.

Peki modelin bu haliyle bırakılmasının ne gibi dezavantajları olabilir? Bunu görmek için aşağıdaki kullanımları incelemek yeterlidir :

//20001 ID'li Urunun Bilgilerini Yazdırmak
Urun anaBilgiler = db.Urun.FirstOrDefault(u => u.UrunID == 20001);
if (anaBilgiler != null)
{
    Kitap detaylar = anaBilgiler.Kitap;

    Console.WriteLine("Kitap ID : {0}\nAd : {1}\nYazar : {2}", anaBilgiler.UrunID.ToString(),
        anaBilgiler.Ad,
        detaylar.Yazar);
}

//Yeni bir DVD eklemek
Urun anaBilgiler = new Urun
{
    UrunID=10003,
    Ad="Yüzüklerin Efendisi",
    Fiyat=30,
    KdvOran=0.18
};
anaBilgiler.DVD = new DVD
{
    Yonetmen="Peter Jackson",
    Sure=TimeSpan.FromMinutes(167),
    Tur="Fantastik"
};

db.AddToUrun(anaBilgiler);
db.SaveChanges();

Görüldüğü üzere, modelin bu şekli ile Entity’lerin yüklenmeleri ve CRUD operasyonlarındaki kullanımları oldukça zahmetlidir. Bu sebeple aşağıdaki işlemlerin yapılması ve veri modelinin doğru bir şekilde kurulması oldukça önemlidir.

  1. Sınıflar arasındaki 1’e N ilişkileri silelim.

    6
  2. Kitap ve Urun sınıflarından UrunID özelliklerini silelim. Ayrıca bu sınıfları Urun sınıfından kalıtalım. Bunun için ToolBox’tan Inheritance aracını kullanabiliriz.
  3. Urun sınıfının özelliklerine giderek bu sınıfı Abstract olarak belirtelim.
  4. Son olarak Mapping Details penceresinden hem Kitap hem de DVD sınıfları için, UrunID kolonlarını, kalıtım yoluyla Urun sınıfından aldıkları UrunID özelliğiyle eşleyelim.

7

Yapılan bu düzenlemeler neticesinde, doğru bir Entity Data Model oluşturulduğunu söyleyebiliriz. Bu noktada OOP çerçevesinde ilerlendiğinin de altını çizmekte fayda vardır. Özellikle Urun sınıfının Abstract olmasıyla, sadece Urun tablosuna kayıtları girip detayları atlamak gibi olası hataların önüne geçilmiştir. Ayrıca hem Kitap hem DVD sınıflarında olması istenen ortak üyeler,  Inheritance ilişkisi sayesinde bu iki sınıfın ortak taban sınıfı olan Urun’de tanımlanabilirler. Gelelim yapılan düzenlemeler sonucu kodlamanın nasıl olması gerektiğine.

//20001 ID'li Urunun Bilgilerini Yazdırmak
Kitap aranan = db.Urun.OfType<Kitap>().FirstOrDefault(k => k.UrunID == 20001);
if (aranan != null)
{
    Console.WriteLine("Kitap ID : {0}\nAd : {1}\nYazar : {2}", aranan.UrunID.ToString(),
        aranan.Ad,
        aranan.Yazar);
}

//Yeni bir DVD eklemek
DVD yeni = new DVD
{
     UrunID = 10004,
     Ad="Jurassic Park",
     Fiyat=20,
     KdvOran=0.18,
     Sure=TimeSpan.FromMinutes(126),
     Yonetmen="Steven Spielberg",
     Tur="Bilim Kurgu"
};

db.AddToUrun(yeni);
db.SaveChanges();

//Tüm kitapları listelemek
foreach (Kitap ktp in db.Urun.OfType<Kitap>())

     Console.WriteLine("Kitap ID : {0}\nAd : {1}\nYazar : {2}", ktp.UrunID.ToString(),
ktp.Ad,
ktp.Yazar);
}

TPH Tasarımlı Veritabanını EF ile Kullanmak

İkinci veri kaynağı için bir model oluşturulduğunda, öncelikle aşağıdaki gibi tek bir sınıfın üretildiği görülür. Tabi ki, bu veri kaynağında tüm bilgiler tek tabloda bulunduğundan ötürü, varsayılan olarak modelde tek bir sınıf üretilir.

8

Peki bu durumda modelin kullanarak verilere ulaşımı nasıl gerçekleşecektir? Aşağıda, mevcut EDMX ile verilere nasıl erişileceği görülebilir.

//20001 ID'li Urunun Bilgilerini Yazdırmak
Urun aranan = db.Urun.FirstOrDefault(k => k.UrunID == 20001);
if (aranan != null)
{
    Console.WriteLine("Kitap ID : {0}\nAd : {1}\nYazar : {2}", aranan.UrunID.ToString(),
        aranan.Ad,
        aranan.Yazar);
}
//Yeni bir DVD eklemek
Urun yeni = new Urun
{
    UrunID=10003,
    Ad="Yüzüklerin Efendisi",
    Fiyat=30,
    KdvOran=0.18,
    Sure=TimeSpan.FromMinutes(167),
    Yonetmen="Peter Jackson",
    Tur="Fantastik",
    UrunTip="D"
};

db.AddToUrun(yeni);
db.SaveChanges();

//Tüm kitapları listelemek
foreach (Urun urn in db.Urun)
{
     if (urn.UrunTip == "K")
     {
         Console.WriteLine("Kitap ID : {0}\nAd : {1}\nYazar : {2}", urn.UrunID.ToString(),
urn.Ad,
urn.Yazar);
     }
}

Hmm, pek iç açıcı görünmediğini itiraf etmeliyimSmile Özellikle UrunTip özelliğinin bu kullanımı hata riskini de arttırmaktadır. Halbuki, birinci veri kaynağı için oluşturulan veri modelerinin son şekli, gerçekten de etkili bir veri erişimi sağlamaktaydı.

Aşağıdaki adımlar izlenerek, ilk modeldekine benzer bir kalıtım hiyerarşisi oluşturulabilir.

  1. Modelde sağ tuşla boş bir yere tıklayarak açılan menüden Add Entity diyelim. Sonrasında Entity Name alanına Kitap yazarak, Base Type olarak Urun tipini belirtelim.
  2. Yazar,SayfaSayi ve BasimYil özelliklerini Urun sınıfından keserek Kitap sınıfına yapıştıralım.
  3. Yonetmen,Sure ve Tur özelliklerini ise Urun sınıfından keserek DVD sınıfına yapıştıralım.

    9
  4. DVD sınıfını Mapping Details penceresinden Urun tablosuyla eşleştirelim. Ayrıca Add Condition kısmından UrunTip değeri “D” olan satırların DVD nesneleriyle eşleştirileceğini belirtelim.

    10 
  5. Kitap sınıfını Mapping Details penceresinden Urun tablosuyla eşleştirelim. Ayrıca Add Condition kısmından UrunTip değeri “K” olan satırların Kitap nesneleriyle eşleştirileceğini belirtelim.
  6. Urun sınıfını, özellikler penceresinden Abstract olarak değiştirelim ve UrunTip özelliğini bu sınıftan kaldıralım.

Modelin son şekli aşağıdaki gibi olmalıdır :

11

Bingo!!! Kullanılan her iki veritabanı da birbirinden çok farklı olmasına rağmen aynı sonuca ulaşıldı. Tabi ki veri kaynaklarının farklı yapıları gereği farklı adımlar izlendi; fakat neticesinde varılan nokta her ikisinde de birebir aynıdır. Peki, eğer veri modellerinde varılan noktalar aynıysa, veri erişimi sırasında kullanılan kodlar da aynı olacak mıdır? Kocaman bir EVETWinkAşağıdaki örnek kullanımları bir yerden hatırlıyor olmalısınız. Doğru, TPT örneğindeki kodların aynılarıSmile

//20001 ID'li Urunun Bilgilerini Yazdırmak
Kitap aranan = db.Urun.OfType<Kitap>().FirstOrDefault(k => k.UrunID == 20001);
if (aranan != null)
{
    Console.WriteLine("Kitap ID : {0}\nAd : {1}\nYazar : {2}", aranan.UrunID.ToString(),
        aranan.Ad,
        aranan.Yazar);
}

//Yeni bir DVD eklemek
DVD yeni = new DVD
{
     UrunID = 10004,
     Ad="Jurassic Park",
     Fiyat=20,
     KdvOran=0.18,
     Sure=TimeSpan.FromMinutes(126),
     Yonetmen="Steven Spielberg",
     Tur="Bilim Kurgu"
};

db.AddToUrun(yeni);
db.SaveChanges();

//Tüm kitapları listelemek
foreach (Kitap ktp in db.Urun.OfType<Kitap>())

     Console.WriteLine("Kitap ID : {0}\nAd : {1}\nYazar : {2}", ktp.UrunID.ToString(),
ktp.Ad,
ktp.Yazar);
}

Örneklerde de görüldüğü gibi, oluşturulan veri modelleri sayesinde her iki veri kaynağı da aynı kodlarla kullanabilmektedir. Diğer bir deyişle, veritabanı tasarımları farklı olsalar da, kullanılan veri modellerinin programcıyı veritabanı tasarımından soyutlaması sonucu, aynı kodlama biçimiyle veri erişimi sağlanabilmektedir. Bu da günümüz programlama ihtiyaçları gözönüne alındığında gerçekten önemli bir avantajdır. Çünkü uygulamanın kodları yazıldıktan sonra tablolar üzerinde yapılan bu tarz tasarımsal değişikliklerde, yalnızca model üzerinde düzenlemeler yapmak yeterli olacak, uygulamanın kod kısmı üzerinde ekstra bir çalışmaya gerek kalmayacaktır.

Faydalı olması dileğiyle…

1 yorum:

Unknown dedi ki...

Değerli paylaşımınız için Tşk Hocam. Merakla bir sonraki yazınızı bekliyorum.

İyi çalışmalar.

Yorum Gönder