Hibernate ve n + 1 selects sorunu

M. Enes Oral
3 min readMar 24, 2020

--

Yazılım tasarımındaki en büyük zorluklardan biri, nesne yönelimli dünya ile veri tabanlarının ilişkisel dünyası arasındaki paradigma farkıdır.

Nesneleri tablolara, tabloları ise nesnelere dönüştürmek object-relational mapping (ORM) olarak adlandırılan olaydır. Kendi ORM çözümünüzü oluşturmak kolay bir iş değildir, büyük çaba gerektirir ve hataya elverişlidir. Bu nedenle kendi ORM çözümünüzü oluşturmak, özellikle güvenilir ve iyi yapılandırılmış alternatifler olduğu düşünüldüğünde önerilen bir yaklaşım değildir. Bu alternatiflerden biri de Java dünyasında önemli bir yere sahip olan Hibernate’dir.

Hibernate, persistence katmanının çoğunu soyutlamasına ve veri tabanında okuma/yazma işlemleri yaparken düşük gecikme süresine sahip olmasına rağmen bazı sorunlara sebep olabilmektedir.

Persistence katmanı: Ekleme, güncelleme, silme ve seçme işlemleri aracılığıyla veri tabanı verilerini işleyen katmandır.

Özellikle, koleksiyonları (list, set..) olan nesneler için sorgular yazılırken çok dikkat edilmelidir. Dikkat edilmediği takdirde ciddi performans sorunları ile karşılaşılabilir.

Temel olarak, top-level nesneye ait koleksiyonları iki farklı şekilde fetchleyebiliriz: bunlardan birincisi eager: top-level nesne yüklendiği zaman kullanılıp kullanılmayacağına bakılmaksızın ilişkili koleksiyon da tamamen yüklenir. İkincisi ise lazy: top-level nesne yüklendiği zaman ilişkili koleksiyon direkt olarak yüklenmez, koleksiyon gerektiğinde uygulama tarafından yüklenir.

Çoğu ilişkilendirmenin lazy olarak yapılması yaygındır ve önerilir. Bu şekilde top-level nesne her yüklendiğinde, ilişkili olduğu koleksiyonların tamamı yüklenmez. Fakat top-level nesne ile ilişkili koleksiyonların, top-level nesne ile birlikte yüklenmemesi ana performans sorunlarından biri olan n + 1 selects problemine sebebiyet verebilir. Gelin bu problemi daha yakından inceleyelim.

Bir e-ticaret uygulaması oluşturduğumuzu varsayalım. Her sipariş birden fazla ürüne sahip olabilir. Bunu modellemek için Product sınıfının örneklerini içeren bir Order sınıfımız olabilir.

Daha önce açıklandığı gibi lazy initialization kullanarak bu ilişkiyi eşlediğimizi varsayalım. Şirketin tüm siparişlerden elde ettiği geliri hesaplamak istersek şu şekilde yapabiliriz:

@Transactional
public int getTotalProfit() {
List<Order> orders = orderRepository.findAll();
int totalProfit = 0; for (Order order : order) {
for (Product product : order.getProducts()) {
totalProfit += product.getPrice();
}
}
return totalProfit;
}

Order-Product ilişkisini lazy initialization ile eşleştirdiğimiz düşünüldüğünde, tüm Order nesnelerini almak için repository’e gittiğimizde her Order için Product koleksiyonu yüklenmeyecektir.

Ancak order.getProducts() metodunu çağırdığımızda bu Product nesnelerinin yüklenmesi gerekir. Hibernate her yinelemede bu verileri almak için veri tabanına tekrar tekrar gider. Burada verileri almak için veri tabanını bir kez çağırmamıza rağmen çok daha fazla okuma işlemi gerçekleştiriyoruz. n adet Order kaydımızın olduğunu düşünürsek n + 1 kere (genel ve her Order için ekstra bir) okuma işlemi gerçekleştiriyoruz. Problemin adı da buradan geliyor.

Şimdi sorunu açıkladığımıza göre, bu okuma işlemi sorununu hafifletmek hatta tamamen çözmek için bazı yaklaşımlara göz atalım.

Bu sorunu hafifletmenin bir yolu, nesneleri toplu olarak çağırmaktır. Bu yaklaşımda Hibernate yüklenmemiş ilk nesneye eriştiğinde koleksiyonun sonraki x öğesini de yükler. Bu yaklaşım, sorunu n + 1 'den n / x + 1 ‘e indirecektir. Aşağıdaki implementasyon Order-Product örneğimiz için bu çözüm yolunu göstermektedir.

@Entity
public class Order {

@OneToMany(fetch = FetchType.LAZY)
@BatchSize(size = 10)
private List<Product> products;
}

BatchSize anotasyonu ile Hibernate’e, henüz yüklenmemiş olan bir Product nesnesine her erişmeye çalıştığında sonraki 10 Product nesnesini de yüklemesini belirtiyoruz. Bu yaklaşımla uygulamanın gecikmesi hafifletilebilir fakat sonraki x tane nesnenin gereksiz yere yüklenmiş olabileceği dezavantajı vardır.

Başka bir çözüm yolu ise aşağıda implemente edildiği üzere fetch tipini eager olarak tanımlamaktır. Bu yol sorunu çözecektir fakat nesneye ait tüm koleksiyon yükleneceğinden çok fazla kaynak harcanacak, veri tabanında kilitlenme süresi artacaktır. Bu nedenle bu çözüm yolu önerilmez.

@Entity
public class Order {

@OneToMany(fetch = FetchType.EAGER)
private List<Product> products;
}

Önerilen çözüm yolu, ilişkiyi sınıf düzeyinde eager fetch olarak tanımlamak yerine, gerektiğinde sorgu düzeyinde eager fetch olarak tanımlamaktır. Bu çözüm yolu, Criteria interface’i ile veya HQL ile implemente edilebilir.

//Criteria interface
public List<Product> findAllProductsWithOrder() {
Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Product.class);

criteria.setFetchMode("Order", FetchMode.EAGER);

return criteria.list();
}
//HQL
@Query("SELECT p FROM Product p JOIN FETCH p.orders")
List<Product> findAllProductsWithOrder();

Bu çözümle, veri tabanında yalnızca bir okuma işlemi gerçekleştirileceğini garanti altına almış oluyoruz.

Son olarak, koleksiyondaki nesnelerin sayısının az olduğu durumlar için bir başka çözüm yolu daha vardır. Eğer nesnelerin sayısı yeterince az ise bu nesneleri Hibernate ikinci düzey önbelleğinde tutmak mümkündür.

@Entity
@Cacheable
@Cache(usage = READ_WRITE)
public class Product {

private int price;
}

Bu çözüm yolu, Product nesnelerine her ihtiyaç duyulduğunda veri tabanına gidilmemesini sağlar. Uygulanabilir olduğunda iyi bir çözüm yoludur.

Sonuç olarak Hibernate, persistence katmanını soyutlamada çok iyi bir iş çıkarsada Hibernate’i bilinçsiz bir şekilde şekilde kullanmamalı, implementasyonumuzu nesneleri verimli bir şekilde okuyacak/yazacak şekilde tasarlamalıyız. list(), iterate() veya load() metodlarını kullanan sorgular yazarken, koleksiyonlara erişmenin n + 1 selects sorununa yol açıp, uygulamanın performansını önemli ölçüde azaltabilecek gecikme sorunlarına yol açabileceğini unutmayın.

End Of File

--

--