Hibernate Yeni Baslayanlara Öneriler

Hibernate kullanmaya başladığımda daha yeni çalışmaya başlamıştım. Birkaç ay ya olmuştu ya olmamıştı. Daha doğru düzgün soyutlamalar bile yapamazken bir de üzerine Hibernate kullanmaya çalışıyordum. Aslına bakarsanız kullanıyordum da yani veritabanına bağlanıp istediğim bilgileri çekebiliyor ve istediğim kayıtları atabiliyordum. Tabi benim gibi SQL dilini pek sevmiyorsanız bu sizi çok rahatlatacaktır. Herşey gayet düzgün giderken, ilk hatalarımızı almaya başladık. Genellikle kullandığımız veritabanı ile Hibernate arasındaki iletişimi konu alan problemlerimiz oldu. Yani kodumuz çalışıyordu fakat uzun süre üzerinde işlem yapılmazsa ( MySql veritabanı için bu 8 saat olarak ayarlıdır. Bunu arttırabilir ya da azaltabilirsiniz ) veritabanımız bağlantısını koparıyordu. Veritabanı bağlantısını kopardığı halde hibernate bunun farkına varamıyordu. Böylelikle elinizde bağlantısı olmayan bir SessionFactory ile kala kalıyorduk. Bundan başka yer yer senkronizasyon problemlerimiz oldu. Cascade ile veri silme sorunlarımız oldu. Zamanla acemiliğimizden kaynaklanan bu tip sorunları tek tek çözdük. Fakat bu durum hem hibernate’ten soğumamıza neden oldu hem de bizi müşterilerimize karşı kötü bir duruma soktu. Üstelik patronun gözünde en basit veritabanı işlemlerini bile yapamayan ekip konumuna düşmek ise apayrı bir sorundu. İşte bunların ışığında hibernate’e yeni başlayanlar için, ufak ve yeryer dummy olan tavsiyelerde bulunmaya çalışacağım. Bu tavsiyeler Hibernate üzerinde guru olmuş olanlara  komik bile gelebilir.

Devam Etmeden Önce

Yazıya devam etmeden önce belirtmekte fayda var. Bu yazı baştan sonra hibernate öğrenebileceğiniz bir yazı değil. Yeni hibernate kullanmaya başlamış olanlar için tavsiylerde bulunulduğu bir yazıdır. Ama örnek kod gibi hibernate başlamanızı kolaylaştıracak bilgiler arıyorsanız, yazının sonundaki Git deposundan bu tip örnekler elde edebilirsiniz. Depodaki örnekler zaten bu yazıda anlatılanları temel alarak yazıldığı için çok sorun teşkil etmeyecektir.

Ayrıca burada anlatılan sorunların ve çözümlerin hepsi için Spring ya da benzeri frameworkler içerisinde çözümleri mevcuttur. Fakat sırf veritabanı kısmındaki sorunları çözmek için Spring gibi bir yapıyı web uygulamasına entegre etmenin doğru olduğunu düşünmüyorum. Bu sebepten çözümleri kendim yaratıp uyarlıyorum. Fakat Spring yapsının kattığı diğer özellikleride aktif olarak kullanıyorsanız burada anlatılan sorunlarla zaten hiç karşılaşmayacaksınız.

Session ve SessionFactory

Hibernate tarafından başınıza bela olacak iki sınıftır bunlar. Heryerde söylenir fakat yine söyliyim, SessionFactory sınıfları yaratılması masraflı sınıflardır. Ne tür uygulama geliştirirseniz geliştirin mutlaka ama mutlaka birtane SessionFactory sınıfı kullanın. Biliyorum bu biraz temel bir öğüt ama yeni başlayıpta buna dikkat etmeyenler olabilir. SessionFactory sınıfları masraflı olduğundan uygulama boyunca sadece birtane yaratılacağını garanti edin. Bunun için Singleton Pattern yapısını kullanabilirsiniz.

Session sınıfları üretilmesi kolay, masrafsız ve kolay harcanabilir sınıflardır. Genel olarak masaüstü uygulamalarında her işlem için birtane kullanılırken Web uygulamalarında kullanıcı başına bir tane üretilmesi uygun olduğu söylenir. Daha doğrusu işe ilk başladığımda bana öğretilen buydu.  Fakat bu kural pratikte işe yaramamaktadır. Yani masaüstü uygulamaları için olan kısmı doğru olsada web kısmı için aynısını söylemek mümkün değil. Bin kişinin aktif olarak kullandığı bir sitede her kişiye ayrı Session ve doğal olarak Transaction vermek sisteminizi aşırı derecede yoracaktır.

Ben ise genel olarak HibernateUtil.java isminde bir sınıf yaratıp, bu sınıfı Singleton Patter kalıbına göre üretiyorum. HibernateUtil sınıfım içerisinde SessionFactory ve Session ile ilgili metodları barındırıyor. Herzaman bir tane üretildiğinden sadece birtane SessionFactory yaratılmış oluyor. Fakat ayrıca, hibernate ile veritabanı sunucusu arasındaki problemlerden ek bir özellik daha ekliyorum. SessionFactory sınıfıma yaşlanma sayacı ekliyorum. Yani eğer uygulamam SessionFactory sınıfıma beş saat boyunca erişmediyse SessionFactory sınıfımı tekrardan oluşturuyorum. Böylelikle veritabanı ile SessionFactory sınıfım arasındaki bağlantıyı hep güncel tutuyorum. Bu tabiki de çok dummy bir çözüm. Yeni başlayanlar için uygulaması kolay. Fakat bu tip bir sorunun gerçek çözümü için C3P0 isimli ek bir hibernate kütüphanesi kullanılıyor.

Session Yönetimi

Eğer modelleriniz arasında N-1, 1-1, N-N gibi ilişkiler varsa genel olarak LAZY ile bu nesnelerinizi çekersiniz. LAZY ile bu nesneleri çekmek(fetch) demek, getter ile nesneye eriştiğiniz zaman nesnenin veritabanından çekilmesi demek. Aynı şekilde EAGER ile nesnelerinizi çekerseniz, nesneleriniz anında veritabanından çekilecektir. Tabi tablo bağımlılıklarınızdan dolayı EAGER sorun yaratabilir. Performans açısından da daha iyi olduğundan genel olarak LAZY ile nesneler, yani tablo satırları çekilir. Fakat LAZY ile nesnelerin çekilebilmesi için, ilk sorgunun gönderildiği Session bilgisinin açık olması gerekmektedir. Bu genelde çok sorun çıkarmasa da her işlem için bir Session harcadığınız durumlarda başınızı ağrıtacaktır.

Bu tip bir sorunun çözümü genel olarak Spring altında mevcut. Spring sizin için Session ve Transaction bilgilerini ayarlayabiliyor. Kullanılmayan Session’ları kapayıp gerektiğinde yenilerini açabiliyor. Fakat benim gibi Spring kullanmıyorsanız bu sizin için sorun olabilir. Hatta benim için sorun oldu da. Session bilgilerini doğru yönetemediğim için ya gereksiz bağlantılarla veritabanımı yavaşlattım ya da gerekli olan Session’ları kapatıp, LAZY ile nesne alımlarında sorunlar yaşadım.

Aslında Hibernate Reference Guide Chapter 2 de hibernate’in sağladığı sınıflar ile Session’ların nasıl kolaylıkla yönetilebileceği anlatılıyor. Hibernate.cfg.xml aşağıdaki satırı ekleyerek, Thread başına session kavramını kullanmaya başlayabilirsiniz. Bu mantıkta thread sonuna kadar tek session kullanılıyor. Yani hem LAZY ile nesne çekebiliyorsunuz hem session bilgileriniz istediğiniz gibi geliyor. Tek fark session bilgisini siz açmıyorsunuz. SessionFactory üzerinden getCurrentSesssion() diyerek session bilgisini alıyorsunuz. Bunun haricinde JTA ile session bilgisini yönetmenizde mümkün. Fakat biraz farklı ve daha karmaşık olduğundan onu başka bir yazıya bırakıyorum.

<property name="hibernate.current_session_context_class">org.hibernate.context.ThreadLocalSessionContext</property>

Hibernate Cache

Hibernate’in sağladığı bu güzel özellik yeni başlayanlar için saç baş yolduracak derecede sinir bozucu olabiliyor. Özellikle testleriniz için DBUNIT ve HSQLDB kullanmıyorsanız bu sizi yorabilir. Zaten yeni başlayan birinin ne second level cache ne de query cache kullandığını sanmıyorum. Fakat testleriniz sırasında first level cache yani session tarafından taşınan cache bilgileride sizi deli etmeye yetecektir.

Eğer session bilgisini kendiniz yönetiyorsanız bunun basit bir örneğini yapabilirsiniz. Uygulamanız çalışıyorken, veritabanına bilgi girdiğinizde uygulamanızda gözükmeyecektir. İşte bu tamamen session bilgisinin yanlış yönetilmesinden kaynaklanıyor. Bunun çözümü var. Aslında çok basit ve dummy bir çözüm. Veritabanı ile session bilgilerinizi senkronize etmek için kullandığınız işlemi yani commit işlemini kullandığınızda bu sorun ortadan kalkıyor.

Bu normalde başınıza gelmeyecektir. Tabi tek veritabanını tek sizin kullandığınızı varsayıyorum. Fakat bir yanda hibernate ile Java web servislerinin çalıştığı, bir yanda da PHP ile web sitesinin çalıştığı bir uygulama düşünün. Bu uygulamada PHP kısmının veritabanına kaydettiği bilgiler, web servisler üzerinde görünür olmayabilir. Çünkü PHP kısmının güncellediği bilgilerden Session cache’nin haberi olmayacaktır. Tabi siz commit işlemi yapana ya da session’ınızı tazeleyene kadar.

Aynı şekilde Sesssion bilgilerinizin her kullanıcıya bir session gelecek şekilde yönetildiğini varsayalım. Bu durumda kullanıcılardan biri diğerinide ilgilendiren bir değişiklik yaptığında, diğerinin session bilgisi veritabanı ile güncel olmadığından bu bilgiler onda olmayabilir. Buda yine bir senkronizasyon problemi ile karşı karşıya olduğunuz anlamına geliyor. Tabi aynı şekilde session bilgisini tazeleyerek bu sorunu çözebilirisniz. Fakat bu durumda da her kullanıcı için bir session olmasının bir mantığı kalmayacaktır.

Genel olarak Session yönetimini Hibernate kısmına verip Thread başına bir session ile çalışırsanız bu sorunları yaşamayacaksınız. Fakat herzaman bunu kullanamayacaksınız. Bu sebepten DAO (Data Access Object)’larınız ile veri çektiğiniz fonksiyonlara, bir de senkronize etme seçeneği koyabilirsiniz. Çok gerekli olduğu durumlarda ve yeni session yaratamayacağınız durumlarda, senkron bir şekilde veri çekme yoluna gidebilirsiniz. Biliyorum çok işe yaramazmış gibi duruyor fakat yeri geldiğinde hayat kurtarabiliyor. Tek yapmanız gereken yeni bir transaction yaratıp commit işlemini gerçeklemek. Örneği aşağıdaki soyut DAO yapısında mevcut.

Entity ve DAO

Entity ile veritabanındaki tabloların karşılığı olan sınıflar kastedilmektedir. Aynı şekilde DAO yani Data Access Object, veritabanı ile ilgili işlemlerin yapıldığı sınıflardır. Bu iki tip sınıfları oluştururken soyutlama katmanlarınızı düzgün oluşturmanız gerçekten çok önemlidir. Gerekli Interface’i işlemiş bir Abstract DAO sınıfı sizin için olmazsa olmazdır. AbstractDAO ile genel veritabanı işlemlerinizi yapabilmelisiniz. Hatta sınıflara özgü veritabanı işlemlerinizide sizin için kolaylaştırmalıdır. Aşağıda benim genel olarak kullandığım soyut DAO sınıfı görebilirsiniz.

BaseHibernateDAO.java

package com.bahadirakin.persistance.dao.impl;

import java.lang.reflect.ParameterizedType;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.criterion.Criterion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.bahadirakin.persistance.HibernateUtil;
import com.bahadirakin.persistance.dao.IBaseDAO;
import com.bahadirakin.persistance.model.IEntity;

/**
 * Contains common operations for all Entities DAO
 *
 * @author Bahadır AKIN
 *
 * @param IEntity
 *            Class
 */
@SuppressWarnings("unchecked")
public abstract class BaseHibernateDAO<T extends IEntity> implements
		IBaseDAO<T> {

	private static final long serialVersionUID = 1L;

	private static final Logger LOG = LoggerFactory
			.getLogger(BaseHibernateDAO.class);

	private HibernateUtil hibernateUtil;
	private Class<T> persistentClass;

	public BaseHibernateDAO() {
		super();
		hibernateUtil = HibernateUtil.getInstance();
		this.persistentClass = (Class<T>) ((ParameterizedType) getClass()
				.getGenericSuperclass()).getActualTypeArguments()[0];
	}
	protected Session getCurrentSession() {
		return hibernateUtil.getCurrentSession();
	}

	protected void synchronize() {
		hibernateUtil.synchronize();
	}

	public Class<T> getPersistentClass() {
		return persistentClass;
	}

	public void save(T entity) {
		if (entity == null) {
			throw new IllegalArgumentException("Entity must not be null");
		}

		try {
			Session session = this.getCurrentSession();
			Transaction transaction = session.beginTransaction();
			session.save(entity);
			transaction.commit();
		} catch (HibernateException e) {
			LOG.error("Error while saving Entity. M: " + e.getMessage()
					+ " C: " + e.getCause());
		}
	}

	public void saveOrUpdate(T entity) {
		if (entity == null) {
			throw new IllegalArgumentException("Entity must not be null");
		}

		try {
			Session session = this.getCurrentSession();
			Transaction transaction = session.beginTransaction();
			session.saveOrUpdate(entity);
			transaction.commit();
		} catch (HibernateException e) {
			LOG.error("Error while saveOrUpdate Entity. M: " + e.getMessage()
					+ " C: " + e.getCause());
		}
	}

	public void delete(T entity) {
		if (entity == null) {
			throw new IllegalArgumentException("Entity Must not be Null");
		}

		try {
			Session session = this.getCurrentSession();
			Transaction transaction = session.beginTransaction();
			session.delete(entity);
			transaction.commit();
		} catch (HibernateException e) {
			LOG.error("Error while delete Entity. M: " + e.getMessage()
					+ " C: " + e.getCause());
		}
	}

	public void detach(T entity) {
		if (entity == null) {
			throw new IllegalArgumentException("Entity Must not be null");
		}

		try {
			Session session = this.getCurrentSession();
			Transaction transaction = session.beginTransaction();
			session.evict(entity);
			transaction.commit();
		} catch (Exception e) {
			LOG.error("Error while detach Entity. M: " + e.getMessage()
					+ " C: " + e.getCause());
		}
	}

	public void refresh(T entity) {
		if (entity == null) {
			throw new IllegalArgumentException("Entity Must not be null");
		}

		try {
			Session session = this.getCurrentSession();
			Transaction transaction = session.beginTransaction();
			session.refresh(entity);
			transaction.commit();
		} catch (Exception e) {
			LOG.error("Error while refresh Entity. M: " + e.getMessage()
					+ " C: " + e.getCause());
		}
	}

	public T getById(Integer id, boolean synchronize) {
		T entity = null;
		try {
			if (synchronize) {
				this.synchronize();
			}
			Session session = this.getCurrentSession();
			session.beginTransaction();
			entity = (T) session.load(getPersistentClass(), id);
		} catch (Exception e) {
			LOG.error("Error while getById Entity. M: " + e.getMessage()
					+ " C: " + e.getCause());
		}
		return entity;
	}

	public List<T> getAll(boolean synchronize) {
		try {
			if (synchronize) {
				this.synchronize();
			}
			Session session = this.getCurrentSession();
			session.beginTransaction();
			List<T> list = session.createCriteria(getPersistentClass()).list();
			return list;
		} catch (Exception e) {
			LOG.error("Error while getAll Entities. M: " + e.getMessage()
					+ " C: " + e.getCause());
		}
		return null;
	}

	public T getBySql(String query, boolean synchronize) {
		T entity = null;
		try {
			if (synchronize) {
				this.synchronize();
			}
			Session session = this.getCurrentSession();
			session.beginTransaction();
			entity = (T) session.createSQLQuery(query)
					.addEntity(getPersistentClass()).uniqueResult();
		} catch (Exception e) {
			LOG.error("Error while getWithSql Entity. M: " + e.getMessage()
					+ " C: " + e.getCause() + " SQL: " + query);
		}
		return entity;
	}

	public List<T> getAllBySql(String query, boolean synchronize) {
		try {
			if (synchronize) {
				this.synchronize();
			}
			Session session = this.getCurrentSession();
			session.beginTransaction();
			return session.createSQLQuery(query)
					.addEntity(getPersistentClass()).list();
		} catch (Exception e) {
			LOG.error("Error while getAllWithSql Entities. M: "
					+ e.getMessage() + " C: " + e.getCause() + " SQL: " + query);
		}
		return null;
	}

	public void executeSQLQuery(String query) {
		try {
			Session session = this.getCurrentSession();
			session.beginTransaction();
			session.createSQLQuery(query).addEntity(getPersistentClass())
					.executeUpdate();
		} catch (Exception e) {
			LOG.error("Error while executeSQLQuery Entities. M: "
					+ e.getMessage() + " C: " + e.getCause() + " SQL: " + query);
		}
	}

	protected List<T> findAllByCriteria(boolean synchronize,
			Criterion... criterions) {
		try {
			if (synchronize) {
				this.synchronize();
			}
			Session session = this.getCurrentSession();
			session.beginTransaction();
			Criteria criteria = session.createCriteria(getPersistentClass());
			for (Criterion criterion : criterions) {
				criteria.add(criterion);
			}
			return criteria.list();
		} catch (Exception e) {
			LOG.error("Error while findAllByCriteria Entities. M: "
					+ e.getMessage() + " C: " + e.getCause());
		}
		return null;
	}

	protected T findByCriteria(boolean synchronize, Criterion... criterions) {
		try {
			if (synchronize) {
				this.synchronize();
			}
			Session session = this.getCurrentSession();
			session.beginTransaction();
			Criteria criteria = session.createCriteria(getPersistentClass());
			for (Criterion criterion : criterions) {
				criteria.add(criterion);
			}
			return (T) criteria.uniqueResult();
		} catch (Exception e) {
			LOG.error("Error while findByCriteria Entities. M: "
					+ e.getMessage() + " C: " + e.getCause());
		}
		return null;
	}

}
Gördüğünüz gibi bu sınıfı kullanarak, her türlü genel bilgiyi çekebilir ve özel bilgileri çekmeyide kolalaştırabilirim. Bunun gibi kendinize kolay gelen bir soyut DAO sınıfı oluşturup projelerinizde bunu kullanın. Hatta Entity sınıfları için de bir Interface ve soyut sınıf oluşturup onları kullanmalısınız. Bu hem DAO sınıflarının yazmamı kolaylaştırıyor hemde kodun test edilmesini kolaylaştırıyor. Bu tip sınıfların nasıl olması gerektiği ve nasıl kullanıldığı ile ilgili örneklere Git Deposu üzerinden erişebilirsiniz. Aynı şekilde Hibernate kullanan açık kaynak kodlu projeleride inceleyebilirisniz. Itracker buna güzel bir örnek. Spring kullanıyor olması biraz farklılaştırsa da Entity ve DAO kısımlarında yapılan soyutlama katmanları gerçekten çok başarılı.

Son

Umarım azda olsa yeni başlayanlara yardımı dokunmuştur. Neyin nasıl yapıldığını doğrudan örnek üzerinde görmek isteyen arkadaşlar için;

Git Repository: Hibernate-Examples

Ayrıca yeni bir tool, araç, engine artık her neyse öğrenirken mutlaka ama mutlaka kitapçığını okuyun. En çok hatayı, en çok yanlışlığı kitapçığını okumadığımdan dolayı yaşadım. Uzun olabilir, işinize yaramıyor olabilir ama yinede kitapçığı okuyun.

End Of Line