A Criteria Builder Idiom for Hibernate Queries

This simple idiom continuingly appears in our search/find use cases, and several other ones, which have parts, in which we find some data to act on it, according to some specific condition.

In search/find use cases, users enter some data pattern, select some options, enter date ranges to narrow or broad result of those queries. We need a way to carry those entries from user to DAO layer for the preperation of queries. Hence, we declared an ICriterionBag interface, which is actually a marker interface. In its subclasses, we define properties and their getters and setters mainly, to carry users? those entries from presentation to DAO layer. It is also possible to specify some other properties for Hibernate queries, such as maximum result count, or match mode for search patterns etc.

In DAO layer, we have to somehow translate those entries into Hibernate Criteria objects, and then execute them to have query results. Therefore, we declared an interface, called ICriteriaBuilder, in which there is only one method decleration, which takes an open Hibernate Session, and ICriterionBag object as input, and finally returns a Hibernate Criteria object as output. In subclasses, we first create a Hibernate Critera object, and add Criterion objects into it, according to specified entries in ICriterionBag object.

The frequency of defining new ICriterionBag, and ICriteriaBuilder subtypes isn?t so much related with the diversity of use cases, but types of returned entities from those queries. As a result, you may reuse already defined types in several previous use cases, becoming some properties in ICriterionBag are only meaningful for some use cases, and some other properties are for others.

HQL ve Criteria Sorgularında FetchMode Farklılıkları

Lazy tanımlanmış 1:M bir ilişkinizinin fetch tipini eager’a çektiğiniz vakit sorgu sonucu dönen kayıtlar arasında duplikasyon olduğunu tecrübe ettiğiniz oldu mu? Eğer sorgunuzda Criteria API’sini kullanmış iseniz bu durumla pek muhtemelen karşılaşmışsınızdır. Sorgunuzu HQL’e çevirdiğiniz takdirde sonuçlardaki duplikasyonların ortadan kalktığını görürsünüz. Peki Hibernate sorgularındaki bu farklılık neden ortaya çıkmaktadır?

Cevabı hemen söyleyelim. HQL sorgusu entity’ler arasındaki ilişkilerin fetch tiplerini sorgu üretimi sırasında dikkate almaz. Criteria API’si ise tam tersine ilişkilerin fetch modunu da hesaba katarak SQL sorgusunu üretir. Konuyu küçük bir örnek ile açıklayalım. A ve B sınıflarımız arasında 1:M bir ilişki tanımlanmış olsun. Önce ilişkiyi LAZY olarak tanımlayalım. Veritabanına da 1 tane A, bu A instance’ı ile ilişkili 2 tane de B instance’ı ekleyelim.

@OneToMany(fetch=FetchType.LAZY)
@JoinColumn(name="a_id")
private Set bSet = new HashSet();

List result = s.createQuery("select a from A a").list();

Yukarıdaki sorguyu çalıştırdığımız vakit eğer Hibernate’in SQL gösterimi aktif ise aşağıdaki SQL ifadesinin üretildiğini, result.size() değerinin ise 1 olduğunu göreceksiniz.

Hibernate: /* select a from A a */ select a0_.id as id3_ from A a0_

Criteria API’sini kullanarak aşağıdaki gibi A entity’lerini sorgularsak, sonuç yine aynı olacak.

List result = s.createCriteria(A.class).list();

Hibernate: /* criteria query */ select this_.id as id3_0_ from A this_

Şimdi ilişkinin fetch modunu EAGER olarak değiştirerek aynı sorguları tekrarlayalım. HQL sorgusu için result.size() 1 olarak kalırken ve aşağıdaki iki sorgu üretilirken;

Hibernate: /* select a from A a */ select a0_.id as id3_ from A a0_
Hibernate: /* load one-to-many com.javaegitimleri.hibernate.fetch.A.bSet */ select bset0_.a_id as a2_1_, bset0_.id as id1_, bset0_.id as id4_0_ from B bset0_ where bset0_.a_id=?

Criteria API’si çalıştırıldığında aşağıdaki tek bir sorgunun üretildiğini, ayrıca result.size() değerinin de 2 olduğunu göreceksiniz.

Hibernate: /* criteria query */ select this_.id as id3_1_, bset2_.a_id as a2_3_, bset2_.id as id3_, bset2_.id as id4_0_ from A this_ left outer join B bset2_ on this_.id=bset2_.a_id

İlginç bir şekilde, HQL sorgusunda, 1:M eager ilişkiyi yüklemek için ikinci bir SELECT kullanılırken, Criteria API’sinde ise A ile B arasında “outer join” yapılmaktadır. Şimdi de ilişkinin eager biçimde yüklenirken hangi yöntemin kullanılacağını söyleyelim.

@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.EAGER)
@JoinColumn(name="a_id")
@Fetch(FetchMode.SELECT)
private Set bSet = new HashSet();

HQL sorgusu çalıştığında üretilen sorgularda ve result.size()’da bir değişiklik olmaz iken, Criteria API’si çalıştırıldığında eager ilişkinin yüklenmesi için “outer join” yerine ikinci bir select SQL ifadesinin üretildiğini, aynı zamanda result.size()’ın da 1 olduğunu görüyoruz.

Hibernate: /* criteria query */ select this_.id as id3_0_ from A this_
Hibernate: /* load one-to-many com.javaegitimleri.hibernate.fetch.A.bSet */ select bset0_.a_id as a2_1_, bset0_.id as id1_, bset0_.id as id4_0_ from B bset0_ where bset0_.a_id=?

Eğer @Fetch(FetchMode.JOIN) olarak değiştirilirse HQL sorgusunda birşey değişmezken, Criteria API sorgusunun ilişkiyi join ile yüklediği görülecektir. Kısacası HQL sorguları entity’ler arasındaki ilişkilerin fetch stratejilerinden hiç bir şekilde etkilenmezken, Criteria sorguları ise fetch stratejisine göre değişiklik göstermektedir.

Sorgu sonuçlarındaki duplikasyon ise tamamen join işleminin bir sonucudur. İster HQL, ister Criteria API’si kullanılsın, eğer sorgunuz içerisinde join söz konusu ise duplikasyon kaçınılmazdır. Duplikasyonu ortadan kaldırmak için HQL ve Criteria API’si için değişik yöntemler vardır. HQL için select ifadesinden sonra “distinct” kullanarak duplikasyonun önüne geçebiliriz. Criteria sorgularında ise Criteria.DISTINCT_ROOT_ENTITY ResultTransformer set edilmelidir.

Sadece Ne Gerekiyorsa Onu Kullanın

2004 yılının başlarında askerliğimi yaptığım yerde bizden bir yazılım geliştirmemizi istemişlerdi. İnternet bağlantısının bile çok sorunlu olduğu, hiyerarşinin ve bürokrasinin yoğun olduğu bir ortamda sıfırdan enterprise Java geliştirme ortamını toparlamak, değişik enterprise Java frameworklerini ve kütüphanelerini bir araya getirip uygulamayı geliştirmeye başlamak daha zor olacağı için, daha önce hiç proje geliştirmeme rağmen hazır ve entegre bir geliştirme ve runtime ortamı sunduğundan ötürü, üstlerime .NET’i kullanmayı önerdim. Projeyi geliştirmeden sorumlu diğer asteğmen arkadaşım Visual Studio konusunda, bende uygulamanın tasarımı, mimarisi, altyapı servisleri noktasında konuya gayet hakim olduğumuzdan, birkaç haftada uygulamayı gayet başarılı biçimde geliştirdik, deploy ettik. Geliştirme süresince de Visual Studio ve C# dışında da herhangi bir framework, kütüphane veya araç kullanmadık. Zaten daha .NET dünyasında Spring.NET, NHibernate, NAnt, Maverick vb frameworkler’de ya hiç çıkmamıştı, yada yeni yeni ortalıkta belirmeye başlıyorlardı. Bu framework ve araçların çoğu Java dünyasında bile o dönemlerde yeni sayılırdı.
Bu başarılı proje çalışmasından sonra görev yaptığım şubedeki üstlerim ikinci bir projeyi daha geliştirmemi istediler. İlk projedeki başarının getirdiği özgüven ve rahatlık ile bu projeyi de .NET ile yapmaya karar verdik. Ancak bu sefer ilk projede kullanmadığımız hemen hemen bütün bu framrework, kütüphane ve araçları ikinci projede kullanmaya karar verdim. Projenin çok basit bir domain modeli olmasına rağmen persistence framework olarak NHibernate, ASP.NET ile tam manası ile uyuşmamasına rağmen MVC framework olarak Maverick, build aracı olarak NAnt’ı günler boyu uğraşarak bir araya getirerek entegre ettim. Uygulamanın iskeletini oluşturdum ve vertical bir slice’ı da örnek senaryo olarak implement ettim. Ortaya çıkan durum tam olarak “Second System Effect” kavramı ile özetlenebilirdi. Fred Brooks’un ünlü kitabı, Mythical Man Month’da bahsettiği gibi hemen her mühendis gibi bende bu tuzağa düşüvermiştim. Ortaya çıkan mimari ve tasarım da bu basit projeyi efektif biçimde geliştirmekten uzaktı. Uzun lafın kısası, kısa bir dönem sonra da terhis olacağım için projeyi o hali ile benden sonraki asteğmen arkadaşıma devrettim. Malesef bu arkadaşım da haklı olarak bu “overdesign” ile uğraşmak yerine projeyi sıfırdan geliştirmek zorunda kaldı.
Geçenlerde InfoQ’da Dan North’un “Simplicity The Way Of The Unusual Architect” isimli çok hoş sunumu ile karşılaşınca aklıma hemen yukarıdaki macera geliverdi. Sunumun içerisinde bir problemi çözmek için sisteme herhangi bir teknoloji veya aracı dahil ettiğimizde elimizdeki problem sayısının birden ikiye çıktığından bahsediliyor. Gerçekten de sadece kullanmış olmak için hiçbir framework, kütüphane veya araç uygulama içerisinde kullanılmamalı. Bunlardan herhangi birini kullanmayı düşündüğümüzde öncelikle ulaşmaya çalıştığımız hedefi, hangi problemi çözmeye çalıştığımızı ve kullanacağımız teknoloji veya aracın hedeflediğimiz noktaya ulaşmada işimizi ne kadar kolaylaştıracağını dikkatli biçimde etüt etmemiz şart. Başarılı bir projenin ardından başlayan her yeni projenin mutlaka bir “second system effect” riski taşıdığını unutmayalım.