Başlık kısa ancak konu oldukça uzun. 2000'lerin başından beri
var olmasına rağmen Türkiye'deki .Net camiasında yurtdışındaki kadar popüler
olmayan ancak en güçlü O/RM seçeneklerinden biri olan NHibernate ile ilgili
hızlı bir tanıtım yazısı okumak üzeresiniz.
O/RM (Object Relational Mapping) mi?
Kaçınız şimdiye veri depolama ihtiyacı olmayan bir uygulama
geliştirdi? Eğer yazılım geçmişiniz benim gibi Commodore ve Amstrad dönemlerine
kadar uzanmıyorsa bu soruya "ben" şeklinde cevap vermenizin düşük bir
ihtimal olduğunu düşünüyorum.
Geliştirdiğimiz her yazılımda bir şekilde bir yerde veri
depolama ihtiyacı duyuyoruz. XML dosyalarından, ilişkisel veritabanlarına kadar
veri depolamak için birçok alternatif mevcut. Bu birçok alternatife erişip veri
üzerinde işlem yapmak için yine birçok farklı teknoloji mevcut. .Net dünyasında
olan birçok yazılım ekibi genelde bu alternatifler içinden Ado.Net'i,
dolayısıyla DataSet, DataReader vb. nesneleri kullanmayı tercih ediyor. Tabi ki
bunda yanlış olan hiçbir şey yok. Ancak durum biraz daha farklı. Şöyle ki;
Herşeyi nesnelerle düşünür, nesne olarak
değerlendiririz.
Yani bir topluluk portali uygulaması geliştiriyorsak,
"Üye"lerden, "Paylaşım"lardan, "Yorum"lardan vb.
birçok nesneden / varlıktan bahsediyoruz demektir. Ya da bir bankacılık
uygulaması ise geliştirdiğimiz, Hesap, Müşteri, Para Transferi, Kredi Kartı
gibi nesnelerdir uygulamamızın kapsamını oluşturan. Bu kapsama ilgi alanı
(domain), bu kapsam içinde yukarıda bahsettiğimiz her nesneye de ilgi alanı modeli
(domain model) ismi verilmektedir. Çözüm gerektiren her problemin adreslendiği
yer ilgi alanı modeli olmalıdır. Kredi Kartı numaralarının doğruluğunun
kontrolü, hesap bakiyesi belli bir miktarın üstünde olan müşterinin
"gold" müşteri olarak değerlendirilmesi gibi "iş" ile
ilgili her durum bu ilgi alanı içerisinde modellenir.
Bu modelleme süreci, modellerin veri depolarında saklanma
aşamasına gelindiğinde başka bir ihtiyacın ortaya çıkmasına neden olur. .Net
üzerinden ilerleyecek olursak, programlama dilinde "sınıf" olan bu
modeller, veri saklama ortamlarına aktarılırken tablolara, daha doğrusu
tabloları oluşturan kayıtlara dönüştürülmelidirler. Yani kendi içinde her tür
durumun (ya da daha gerçekçi olması açısından birçok durumun diye düzeltelim)
değerlendirildiği bir sınıfın depoladığı verinin alınması, ilişkisel
veritabanının anlayabileceği, saklayabileceği formata dönüştürülmesi ve
depolanması gereklidir. Eğer şimdiye kadar anlattığım gibi bir ilgi alanı
modeli geliştiriyorsanız ve ilgi alanı içindeki modellerinizin verisini örneğin
SQL Server'da saklamak için gerekli olan dönüşüm kodlarını yazıyorsanız tebrik
ederim, siz bir O/RM geliştiricisi olmuşsunuz bile.
Wikipedia'dan alınmış tam tanımı ile, O/RM, nesne yönelimli
programlama dillerinde uygulanan, birbirleriyle uyumsuz tip sistemleri
arasında, verinin uyumlu hale getirilmesi tekniğidir. Bu tanımı bizim
anlatımımıza uyarlayacak olursak; .Net ile SQL Server tabloları birbirleriyle
uyumsuz tip sistemleridir ve bizim, nesnelerden aldığımız her veriyi SQL Server
uyumlu hale getirmek için yazdığımız her satır kod O/RM tekniğine işaret eden
bir koddur diyebiliriz.
NHibernate'in yukarıdaki anlatımda tam olarak nereye oturduğu
sanırım anlaşılmıştır. NHibernate bir O/RM çatısı. .Net ile yazdığımız
sınıfların, ilişkisel veritabanı sistemleri ile eşleştirmesini
(dönüştürülmesini) gerçekleştiriyor. Daha güzel bir ifadeyle, biz ilgi
alanımıza ve iş kurallarımıza yoğunlaşırken, ilgi alanı modelimizin veritabanı
ile ilgili olan kısmındaki tüm ağır işi NHibernate yükleniyor.
O/RM'nin M'si
NHibernate, sınıfların ilişkisel veritabanı ile eşleştirilmesini
sağlıyor demiştik. O halde bu eşleştirmenin nasıl yapılması gerektiğini de
NHibernate'e göstermek gerekiyor. Modellerin hangi özellikleri, veritabanında
hangi kolon ile eşleşecek, modelin hangi özelliği eşlenen tablodaki anahtar
kolon olacak, modeller arasındaki ilişkiler veritabanına ne şekilde
yansıtılacak vb. gibi tüm eşleme kurallarının NHibernate tarafından bilinmesi
gerekiyor.
Eşleme kurallarının tanımlanması için kullanılabilecek iki
yöntem mevcut. Birinci yöntem -ki bu en eski ve yaygın olan yöntemdir- hbm.xml
uzantılı bir xml konfigürasyon dosyası ile eşleme kurallarını belirlemek.
İkinci yöntem NHibernate ile kardeş proje olan Fluent NHibernate kullanarak
gerçekleştirilen eşleme gerçekleştirmek. Xml konfigürasyon dosyalarını her
zaman sıkıcı ve itici bulmuşumdur, dolayısıyla bu yazıda Fluent kullanarak
eşlemeleri gerçekleştireceğim.
İlgi Alanı Modeli
Fluent NHibernate içinde hazır gelen örnek proje üzerinden ilerleyeceğiz.
Yazının sonunda projeyi indirebileceğiniz web adresini paylaştım. Yazı
içerisinde yapacağım küçük değişiklikleri kolayca projeye uyarlayabilirsiniz.
Projedeki modellerin yapısı şu şekilde:
İlgi alanı içinde dört model mevcut. Özellikleri şemada
gördüğünüz gibi. Modelde üzerinde durulması gereken şey sınıflar arasındaki
ilişkiler. Store ve Product sınıfları arasında çoka-çok (many-to-many) ilişki
mevcut. Yani bir mağazada birden çok ürün bulunabilir ve bir ürün birçok mağaza
stoğunda mevcut olabilir. Ayrıca Store ve Employee sınıfları arasında bire-çok
(one-to-many) ilişki mevcut. Yani bir mağazada birden çok çalışan bulunabilir.
Product ve Location sınıfları arasında ise biraz daha farklı bir ilişki mevcut.
Bu iki sınıf arasında bir bileşim (composition) ilişkisi tanımlı. Yani ürünün
depodaki koridor ve raf numarasını göstermekte olan Location sınıfı her ürünün
mutlaka sahip olması gereken bir bileşen. Modeli kullanırken koridor ve raftan
oluşan bu lokasyon nesnesine tek başına bir varlıkmış gibi erişebileceğiz ancak
ilişkisel veritabanı açısından bakıldığında Location nesneleri için ayrı bir
veritabanı tablosu oluşturulmayacak. Bunun yerine, Location sınıfına ait olan
Aisle ve Shelf özellikleri, sanki Product sınıfının doğrudan özellikleriymiş gibi
Product tablosunda iki ayrı kolon olarak tanımlanacak ve bu kolonlarda bilgi
depolanacak.
Fluent NH ile Eşleme
Eşlemenin nasıl kodlandığına geçmeden önce model içinden bir
sınıf örneği göstererek NHibernate'in bir özelliğinden bahsetmek istiyorum.
Store sınıfının tanımına bakalım:
public class Store
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual IList Products { get; set; }
public virtual IList Staff { get; set; }
public Store()
{
Products = new List();
Staff = new List();
}
public virtual void AddProduct(Product product)
{
product.StoresStockedIn.Add(this);
Products.Add(product);
}
public virtual void AddEmployee(Employee employee)
{
employee.Store = this;
Staff.Add(employee);
}
}
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual IList
public virtual IList
public Store()
{
Products = new List
Staff = new List
}
public virtual void AddProduct(Product product)
{
product.StoresStockedIn.Add(this);
Products.Add(product);
}
public virtual void AddEmployee(Employee employee)
{
employee.Store = this;
Staff.Add(employee);
}
}
Özelliklerin ve metodların virtual tanımlandığını
farketmişsinizdir. Bunun nedeni lazy-loading adı verilen ve birbiriyle
bağlantılı olan nesnelerin tek seferde değil ihtiyaç oldukça veritabanından
çekilmesini sağlayan NHibernate özelliğinin aktif olmasıdır. Bir mağazamız
(Store) ve bu mağaza içinde kayıt edilmiş 10 bin adet ürün (Product) olduğunu
düşünün. Lazy-loading özelliğinin aktif olmadığı durumda, veritabanından
mağazayı sorgulayıp çektiğimizde NHibernate 10 bin adet ürünü de ihtiyacımız
olsa da olmasa da çekip getirecektir.
Peki neden virtual?
NHibernate lazy-loading özelliğini devreye sokmak için,
NHibernate altyapısının inşası sırasında model sınıflarından türeyen, model sınıflarının
sahip olduğu ve olmadığı bir takım ek özellikleri içinde bulunduran
"proxy" sınıflar oluşturur ve oturum açık olduğu sürece model
sınıfları yerine bu proxy sınıflar üzerinden işlem gerçekleştirir. Model
sınıflarındaki özelliklerin virtual olması da, model sınıfın sahip olduğu
özelliklerin proxy tarafından override edilebilmesini ve lazy-loading için
gerekli olan algoritmanın proxy tarafından uygulanabilmesini sağlar.
Sınıf tanımında üzerinde durulması gereken bir diğer nokta,
bire-çok ve çoka-çok ilişkiler için yapılmış olan tanımlamalar. Bir mağaza
stoğunda birden çok ürün bulunabilir demiştik. Bunun karşılığı;
public virtual IList Products { get; set; }
olarak tanımlanıyor. Store sınıfının yapılandırıcısı içinde de
Products listesinin oluşturulduğunu görebilirsiniz. Store ve Employee sınıfları
arasında bire-çok ilişki olacak demiştik, bunun Employee sınıfındaki karşılığı
ise;
public virtual Store Store { get; set; }
olarak tanımlanıyor. NHibernate, doğru eşlemeler yapıldıktan
sonra tüm bu ilişkiler dahilinde kayıtların veritabanına gönderilmesini /
çekilmesini gerçekleştirecek.
Şimdi eşlemelerin nasıl yapıldığına StoreMap üzerinden bakalım.
public class StoreMap : ClassMap
{
public StoreMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasManyToMany(x => x.Products)
.Cascade.All()
.Table("StoreProduct");
HasMany(x => x.Staff)
.Cascade.All()
.Inverse();
}
{
public StoreMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasManyToMany(x => x.Products)
.Cascade.All()
.Table("StoreProduct");
HasMany(x => x.Staff)
.Cascade.All()
.Inverse();
}
}
Her model için bir eşleme sınıfı tanımlıyoruz (tamamen otomatik
olarak eşlemeleri gerçekleştirmek de mümkün, belki başka bir yazıda bu özelliği
inceleyebiliriz). Eşleme sınıfı ClassMap sınıfından türetiliyor ve eşleme
kuralları hangi sınıf için belirleniyorsa tip parametresi olarak bu sınıf
kullanılıyor. Eşleme sınınıfının yapılandırıcısı içinde ise lambda ifadeleri
ile sınıfın özelliklerinin ilişkisel veritabanı karşılığının ne olacağı
gösteriliyor.
Id(x => x.Id);
ifadesi ile, Store sınıfının Id özelliğinin veritabanında kimlik
(identity) kolon olarak oluşturulacağı belirleniyor.
Map(x => x.Name);
ifadesi ile, Store sınıfının Name özelliğinin veritabanında da
varsayılan özellikleri ile karşılığı olması gerektiği gösteriliyor.
HasManyToMany(x =>
x.Products).Cascade.All().Table("StoreProduct");
ile, Store ve Product arasındaki çoka-çok ilişki gösterilmiş
oluyor. Mağazaya bağlı ürünlerin, Store nesnesi içindeki Products listesinde
saklanacağı anlaşılıyor. Cascade.All() ile de, Store üzerinde yapılan
değişikliklerden (Id güncelleme ya da Store nesnesini silme), Store nesnesine
bağlı olan Product nesnelerinin de etkileneceği (güncelleme ya da bağlı
ürünlerin tamamının otomatik olarak silinmesi) kurala bağlanıyor. Çoka-çok
ilişkilerde birbiriyle ilişkili olan kayıtların depolanması için bir üçüncü
tabloya ihtiyaç var. İfadenin en sonundaki Table() metodu ile bu tablonun
adının ne olacağı belirtiliyor. HasManyToMany metodunun ilişkinin diğer
tarafındaki Product nesnesinde de karşılığı olması gerekli. ProductMap içindeki
ifade de şu şekilde;
HasManyToMany(x =>
x.StoresStockedIn).Cascade.All().Inverse().Table("StoreProduct");
Bir ürünün ilişkili olduğu mağazalar, Product içindeki
StoresStockedIn listesi ile tutuluyor. Tanımın bu tarafındaki tek fark
Inverse() metodu. Bu metod ilişkinin hangi tarafının ilişkinin sahibi olacağını
gösteren bir metod. Bu örnekte, ilişkinin depolanması sırasında ilişkinin
sahibi Store tarafı. Bu durumda NHibernate Store nesnesini depoluyor,
depoladığı Store nesnesinin veritabanı tarafından gelen Id değerini kullanarak
Store nesnesinin Products listesindeki nesneleri depoluyor. Inverse
kullanılmadığı durumda ise NHibernate, tüm Product nesnelerini Store ile
ilişkili değilmiş gibi depolar, ardından Store nesnesini depolar ve Store
nesnesinin Id değeri ile daha önce depoladığı Product nesnelerini teker teker
günceller.
HasMany(x => x.Staff).Cascade.All().Inverse();
Yukarıdaki eşleme direktifi ile, Store ve Employee arasındaki
bire-çok ilişki tanımı yapılıyor. Aynı şekilde Employee sınıfında da bunun
karşılığını bulmak mümkün:
References(x => x.Store);
Model içinde bulunmayan ancak sıkça kullanılan bir özellik olan
enumerasyonların nasıl eşlenebileceğini göstermek amacıyla Product sınıfına
Status adında bir özellik ekledim. Enumerasyonun ve Product sınıfının kısmi
tanımı şu şekilde:
public class Product
{
//...
public virtual InventoryStatus Status { get; set; }
//...
}
public enum InventoryStatus
{
OnOrder = 1,
AvailableForSale = 2,
InTransit = 3
}
{
//...
public virtual InventoryStatus Status { get; set; }
//...
}
public enum InventoryStatus
{
OnOrder = 1,
AvailableForSale = 2,
InTransit = 3
}
Aksini belirtmediğiniz sürece NHibernate, enumerasyonları string
karşılıkları ile saklayacaktır. Yani eğer Status kolonu için eşleme kuralımız
şu şekilde olsaydı:
Map(x => x.Status);
Ancak Status için eşlemeyi;
Map(x => x.Status).CustomType();
şeklinde yaparak enumerasyon değerlerini, her bir değerin
karşılığı olan int ile depolamış oluyoruz:
Product tablosunda gördüğünüz Aisle ve Shelf kolonlarının
Product nesnesinden değil, Product ile ilişkili olan Location nesnesinden
geldiğini söylemiştik. Fluent ile bu ilişkilendirmeyi sağlayan ProductMap
içindeki lambda ifadesi ise şu şekilde:
Component(x => x.Location);
Bu ifadeyle, Product sınıfı içindeki Location özelliğine bağlı
olan özelliklerin Product nesnesine aitmiş gibi Product ile aynı tabloda
saklanması sağlanmış oluyor. Ancak Product tablosundaki Aisle ve Shelf özelliklerine
C# ile erişmek istendiğinde Product.Location.Aisle ve Product.Location.Shelf
şeklinde erişilebiliyor (Location özelliğinin null değerine sahip olmadığını
varsayıyoruz tabi).
Uygulamanın Çalıştırılması
NHibernate, veritabanı ile tüm iletişimi Session nesneleri
üzerinden gerçekleştirir. Bağlantıların yönetimi, bağlantı havuzu yönetimi vs.
tamamı Session tarafından bizleri rahatsız etmeyecek şekilde arka planda
gerçekleştirilir. Dolayısıyla, veritabanına ulaşmak için öncelikle yapılması
gereken, Session konfigürasyonunu gerçekleştirmek. NHibernate soyut fabrika
desenini (abstract factory pattern) uygulamaktadır. Bu da demek oluyor ki
aslında konfigürasyon yapılması gereken nokta Session örnekleri değil, Session
üretiminden sorumlu olan Session fabrikasıdır.
private static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008<
.ConnectionString("Server=.\\SQLEXPRESS;Database=FNHSample;Integrated Security=SSPI;"))
.Mappings(m =>m.FluentMappings.AddFromAssemblyOf())
.ExposeConfiguration(cfg => new SchemaExport(cfg).Create(false, true))
.BuildSessionFactory();
}
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008<
.ConnectionString("Server=.\\SQLEXPRESS;Database=FNHSample;Integrated Security=SSPI;"))
.Mappings(m =>m.FluentMappings.AddFromAssemblyOf
.ExposeConfiguration(cfg => new SchemaExport(cfg).Create(false, true))
.BuildSessionFactory();
}
Gördüğünüz üzere, tek bir ifade ile hem Nhibernate
yapılandırılıyor hem de bir ISessionFactory örneği döndürülüyor. Database()
metodunun parametrelerini SQL Server ile çalışacak şekilde değiştirdim.
Database() metodunun hemen ardından gelen Mappings() metodu, ClassMap
tanımlarının nereden alınacağını gösteriyor. Bu durumda Store sınıfının
bulunduğu assembly içinden eşleme tanımlarının okunması sağlanıyor.
ExposeConfiguration ile, SchemaExport nesnesi kullanılarak
veritabanı şemasının hemen üstte belirtilen eşleme kuralları dahilinde
üretilmesi sağlanıyor. SchemaExport.Create metodunun ilk parametresi, arka
planda çalışacak Sql ifadelerinin script olarak üretilmesini kontrol ediyor.
İkinci parametre ise şema üretiminin veritabanı üzerinde gerçekleştirilmesini
kontrol ediyor. Yani bir başka deyişle, yukarıdaki şekilde kullanıldığında, SQL
Server üzerinde model sınıfları için tabloları oluşturmamıza gerek kalmıyor.
NHibernate veritabanı içinde ihtiyaç olan tüm tabloları, tablolar içindeki
ilişkisel bütünlük kurallarını kendisi tanımlıyor. Zaten farketmiş olduğunuz
gibi, modellemeye veritabanından başlamadık. İşin aslı, veritabanı ile hiç
ilgilenmedik bile. Modelleri tasarladıktan sonra kalan herşeyi NHibernate zaten
hallediyor.
Session fabrikası yapılandırması tamamlandığına göre, fabrika
Session üretmeye başlayabilir, biz de böylece nesnelerimizi veritabanında
depolamaya başlayabiliriz:
var sessionFactory = CreateSessionFactory();
var session = sessionFactory.OpenSession();
var session = sessionFactory.OpenSession();
Veritabanında saklanmak üzere bir kaç model örneği:
var barginBasin = new Store { Name = "Bargin Basin" };
var superMart = new Store { Name = "SuperMart" };
var potatoes = new Product { Name = "Potatoes", Price = 3.60, Status = InventoryStatus.AvailableForSale };
var fish = new Product { Name = "Fish", Price = 4.49, Status = InventoryStatus.AvailableForSale, Location = new Location { Aisle = 10, Shelf = 20 } };
var daisy = new Employee { FirstName = "Daisy", LastName = "Harrison" };
var jack = new Employee { FirstName = "Jack", LastName = "Torrance" };
var superMart = new Store { Name = "SuperMart" };
var potatoes = new Product { Name = "Potatoes", Price = 3.60, Status = InventoryStatus.AvailableForSale };
var fish = new Product { Name = "Fish", Price = 4.49, Status = InventoryStatus.AvailableForSale, Location = new Location { Aisle = 10, Shelf = 20 } };
var daisy = new Employee { FirstName = "Daisy", LastName = "Harrison" };
var jack = new Employee { FirstName = "Jack", LastName = "Torrance" };
Örnekler tanımlandıktan sonra Store, Product ve Employee
arasında tanımlanan ilişkilerin veritabanı tarafında kalıcı olması için her
sınıf içinde tanımlanmış olan IList koleksiyonları ile nesneler birbirlerine
bağlanıyor:
barginBasin.AddProduct(potatoes); barginBasin.AddProduct(fish);
superMart.AddEmployee(daisy);
superMart.AddEmployee(jack);
superMart.AddEmployee(daisy);
superMart.AddEmployee(jack);
Burada, Store sınıfı içinde tanımlanmış olan AddProduct ve
AddEmployee metodları ile ilgili söylenmesi gereken birkaç şey var. Metodların
tanımlarına bir bakalım:
public virtual void AddProduct(Product product)
{
product.StoresStockedIn.Add(this);
Products.Add(product);
}
public virtual void AddEmployee(Employee employee)
{
employee.Store = this;
Staff.Add(employee);
}
{
product.StoresStockedIn.Add(this);
Products.Add(product);
}
public virtual void AddEmployee(Employee employee)
{
employee.Store = this;
Staff.Add(employee);
}
AddProduct metodu bir Product örneği alıyor ve Product
nesnesinin StoresStockedIn listesine Store nesnesinin kendisini ekliyor.
Ardından Product nesnesini de Store'un Products listesine atıyor. Böylece çift
taraflı olarak aradaki ilişki sağlanmış oluyor. Aynı şey AddEmployee metodu
için de geçerli. Ancak burada ilişki bire-çok olduğundan Employee'nin Store
özelliğine değer atanıyor ve Employee nesnesi Store'un Staff listesine
ekleniyor.
session.SaveOrUpdate(barginBasin);
session.SaveOrUpdate(superMart);
session.SaveOrUpdate(superMart);
Bu iki satır ifade ile tüm nesne hiyerarşisi bütün ilişkileri
ile birlikte veritabanında depolanmış oluyor. Ayrı ayrı her ürünü (Product) ve
çalışanı (Employee) veritabanında saklamadığımızı fark etmişsinizdir. Store
nesnelerini depolamak, Store nesneleri ile ilişkili olan diğer tüm nesnelerin
de veritabanında depolanması için yeterli.
Son olarak, bütün bu bilginin depolandığını teyid etmek için bir
Session üzerinden Linq kullanarak veritabanını sorgulayalım. NHibernate
üzerinden Linq ile sorgulama yapmak için NHibernate.Linq kütüphanesini
indirmeniz gerekiyor. Yazının sonunda bu adresi paylaştım.
session.Linq().ToList().ForEach(store =>
WriteStorePretty(store));
Herhangi bir Session örneği üzerinden yukarıdaki sorguyu
çalıştırabilirsiniz. NHibernate.Linq ve System.Linq isim uzaylarını tanımlamayı
unutmayın. WriteStorePretty metodu örnek proje içinde tanımlanmış bir metod ve
Store bilgilerini düzgün bir şekilde ekrana basıyor. Ben de tanımlanmış olan bu
metodu kullandım.
Hem OR/M konusunda hem de NHibernate'in Fluent ile kullanımına
dair kısa bir giriş yapmaya çalıştım. Umarım faydası olmuştur. Görüşmek üzere!
Hiç yorum yok:
Yorum Gönder