Sanırım derginin bu sayısındaki en kısa başlık bu. Sadece
NHibernate. 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<Product> Products { get; set; }
public virtual IList<Employee> Staff { get; set; }
public Store()
{
Products = new List<Product>();
Staff = new List<Employee>();
}
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<Product> 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<Store>
{
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
}
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);
Product tablosundaki kayıtları şu şekilde görecektik:

Ancak Status için eşlemeyi;
Map(x => x.Status).CustomType<int>();
ş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<Program>())
.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();
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" };
Ö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);
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);
}
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);
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<Store>().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!
NHibernate ve NHibernate.Linq: http://sourceforge.net/projects/nhibernate/files/
Fluent NHibernate: http://fluentnhibernate.org/downloads
Fluent NHibernate kaynak kodları: http://github.com/jagregory/fluent-nhibernate