JPA ve Hibernate Performans Optimizasyonu
Neden JPA Performansı Bu Kadar Kritik?
JPA ve Hibernate, Java dünyasında veritabanı işlemleri için standart ORM çözümüdür. Ancak "sihirli" çalışan bu araçlar, dikkatli kullanılmazsa ciddi performans sorunlarına yol açar. Bir uygulamanın production'da yavaşlamasının en sık nedeni optimize edilmemiş veritabanı sorgularıdır.
Bu yazıda en yaygın JPA performans sorunlarını ve çözümlerini gerçek örneklerle inceleyeceğiz.
N+1 Problemi: En Yaygın Performans Katili
N+1 problemi, bir sorgu ile N adet ana kayıt çekilirken, her kayıt için ayrı bir sorgu daha çalıştırılması durumudur. Yani toplam N+1 sorgu çalışır.
// Entity tanımları
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}
@Entity
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
private Product product;
private int quantity;
private BigDecimal price;
}N+1 sorunu:
// 1 sorgu: SELECT * FROM orders
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
// Her order için 1 sorgu daha: SELECT * FROM order_items WHERE order_id = ?
System.out.println("Items: " + order.getItems().size());
}
// Toplam: 1 + N sorgu (N = order sayısı)100 sipariş varsa 101 sorgu çalışır. 1000 sipariş varsa 1001 sorgu. Bu bir felaket senaryosudur.
Çözüm 1: JOIN FETCH
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.items")
List<Order> findAllWithItems();
@Query("SELECT o FROM Order o JOIN FETCH o.items JOIN FETCH o.customer WHERE o.id = :id")
Optional<Order> findByIdWithDetails(@Param("id") Long id);
}Çözüm 2: @EntityGraph
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findAll();
@EntityGraph(attributePaths = {"items", "customer"})
Optional<Order> findById(Long id);
}Çözüm 3: @BatchSize
@Entity
public class Order {
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@BatchSize(size = 20) // 20'şerli gruplar halinde yükle
private List<OrderItem> items;
}@BatchSize(size = 20) kullanıldığında, 100 sipariş için 1 + 5 = 6 sorgu çalışır (100 / 20 = 5 batch). N+1'den çok daha iyi.
Tavsiye: Global batch size ayarı için
application.ymldosyasınaspring.jpa.properties.hibernate.default_batch_fetch_size: 20ekleyebilirsiniz.
Fetch Stratejileri: LAZY vs EAGER
Hibernate'de iki fetch stratejisi vardır:
LAZY: İlişki yalnızca erişildiğinde yüklenir (varsayılan: @OneToMany, @ManyToMany)
EAGER: İlişki ana sorgu ile birlikte hemen yüklenir (varsayılan: @ManyToOne, @OneToOne)
// YANLIS: Eager loading her yerde
@Entity
public class Product {
@ManyToOne(fetch = FetchType.EAGER) // Her product sorgusu category'yi de çeker
private Category category;
@OneToMany(mappedBy = "product", fetch = FetchType.EAGER) // Her product TÜM review'ları çeker
private List<Review> reviews;
@ManyToMany(fetch = FetchType.EAGER) // Her product TÜM tag'leri çeker
private Set<Tag> tags;
}Bu entity'yi findAll() ile çektiğinizde Hibernate devasa bir JOIN sorgusu oluşturur ve belki megabyte'larca gereksiz veri çekersiniz.
// DOGRU: Her yerde Lazy, ihtiyaç halinde fetch
@Entity
public class Product {
@ManyToOne(fetch = FetchType.LAZY)
private Category category;
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
private List<Review> reviews;
@ManyToMany(fetch = FetchType.LAZY)
private Set<Tag> tags;
}Altın kural: Tüm ilişkileri LAZY yapın. İhtiyaç duyduğunuz yerlerde JOIN FETCH veya @EntityGraph ile yükleyin.
DTO Projection ile Sadece İhtiyacınız Olanı Çekin
Bir entity'nin tüm alanlarına her zaman ihtiyacınız olmaz. Örneğin bir ürün listesi sayfasında yalnızca id, name ve price yeterlidir. DTO projection ile sadece gerekli alanları çekersiniz:
Interface Projection
public interface ProductSummary {
Long getId();
String getName();
BigDecimal getPrice();
String getCategoryName();
}
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p.id AS id, p.name AS name, p.price AS price, " +
"c.name AS categoryName " +
"FROM Product p JOIN p.category c " +
"WHERE p.price > :minPrice")
List<ProductSummary> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
}Record DTO Projection
public record ProductDto(Long id, String name, BigDecimal price) {}
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT new com.example.dto.ProductDto(p.id, p.name, p.price) " +
"FROM Product p WHERE p.category.id = :categoryId")
List<ProductDto> findByCategoryAsDto(@Param("categoryId") Long categoryId);
}DTO projection'ın faydaları:
Daha az veri transfer edilir — network ve memory tasarrufu
Hibernate change detection çalışmaz — performans artışı
Lazy loading riski yoktur — çünkü entity değil, düz veri dönüyor
Pagination ile Büyük Veri Setlerini Yönetmek
Binlerce kaydı tek seferde çekmek yerine sayfalama kullanın:
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findByCategory(Category category, Pageable pageable);
@Query("SELECT p FROM Product p WHERE p.price > :minPrice")
Slice<Product> findExpensive(@Param("minPrice") BigDecimal minPrice, Pageable pageable);
}@Service
public class ProductService {
public Page<ProductDto> getProducts(int page, int size, String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).descending());
return productRepository.findAll(pageable)
.map(p -> new ProductDto(p.getId(), p.getName(), p.getPrice()));
}
}Page vs Slice:
Pagetoplam eleman sayısını hesaplar (COUNT sorgusu çalıştırır).Slicesadece "bir sonraki sayfa var mı?" bilgisini verir. Toplam sayıya ihtiyacınız yoksaSlicekullanın — bir sorgu tasarrufu.
Keyset Pagination (Cursor-Based)
Offset-based pagination büyük offset'lerde yavaşlar (OFFSET 100000 tüm önceki kayıtları taramak zorundadır). Keyset pagination bu sorunu çözer:
@Query("SELECT p FROM Product p WHERE p.id > :lastId ORDER BY p.id ASC")
List<Product> findNextPage(@Param("lastId") Long lastId, Pageable pageable);// Kullanım
Long lastSeenId = 0L;
List<Product> page = productRepository.findNextPage(lastSeenId, PageRequest.of(0, 20));
lastSeenId = page.get(page.size() - 1).getId(); // Sonraki sayfa içinQuery Cache ve Second-Level Cache
Hibernate iki seviyeli cache mekanizması sunar:
First-Level Cache: Session (EntityManager) seviyesinde, otomatik aktif
Second-Level Cache: SessionFactory seviyesinde, yapılandırma gerektirir
Second-Level Cache için EHCache veya Caffeine kullanabilirsiniz:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
use_query_cache: true
region:
factory_class: org.hibernate.cache.jcache.JCacheRegionFactory@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}Dikkat: Second-level cache, sık okunan ve nadiren güncellenen veriler için idealdir (kategoriler, konfigürasyonlar). Sık güncellenen veriler için cache invalidation karmaşıklığı sorun yaratır.
Bulk Operations ile Toplu İşlemler
Tek tek entity güncellemek yerine bulk operation kullanın:
// YANLIS: N sorgu
List<Product> products = productRepository.findByCategory(category);
for (Product p : products) {
p.setDiscounted(true);
productRepository.save(p); // Her biri için UPDATE
}
// DOGRU: Tek sorgu
@Modifying
@Query("UPDATE Product p SET p.discounted = true WHERE p.category = :category")
int bulkDiscount(@Param("category") Category category);JDBC batch insert için:
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 50
order_inserts: true
order_updates: true@Transactional
public void saveAll(List<Product> products) {
for (int i = 0; i < products.size(); i++) {
entityManager.persist(products.get(i));
if (i % 50 == 0) {
entityManager.flush();
entityManager.clear(); // Memory temizle
}
}
}Performans İzleme
Hibernate istatistiklerini aktif ederek sorgu performansını izleyebilirsiniz:
spring:
jpa:
properties:
hibernate:
generate_statistics: true
show-sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.stat: DEBUGProduction'da p6spy veya datasource-proxy ile sorgu sürelerini ve sayılarını loglayabilirsiniz.
Özet
N+1 problemini JOIN FETCH, @EntityGraph veya @BatchSize ile çözün
Tüm ilişkileri LAZY yapın, ihtiyaç halinde fetch edin
DTO Projection ile sadece gerekli alanları çekin — entity dönmeyin
Pagination kullanın, büyük offset'ler için keyset pagination tercih edin
Bulk operations ile toplu işlemleri tek sorguda yapın
Second-level cache sık okunan, az güncellenen veriler için kullanın
Performansı izleyin: Hibernate istatistikleri ve slow query log aktif edin
Bu yazıyı beğendiniz mi?
Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.
Bu konuyu derinlemesine öğrenmek ister misin?
Java Programlama: Sıfırdan İleri Seviyeye
İlgili Yazılar
Java Optional: NullPointerException'a Kesin Çözüm ve Doğru Kullanım Rehberi
Java Optional sınıfını sıfırdan ileri seviyeye öğrenin. NullPointerException'a kesin çözüm, doğru kullanım kalıpları, an...
Java Nedir? Kapsamlı Başlangıç Rehberi 2026
Java nedir, ne işe yarar, nasıl çalışır? JVM, JDK, JRE farkları, Java ile neler yapılır, kariyer fırsatları ve öğrenme y...
Java Stream API: Koleksiyonları Fonksiyonel Programlama ile Yönetmenin Tam Rehberi
Java Stream API'yi sıfırdan ileri seviyeye kadar öğrenin. Gerçek dünya örnekleri, yaygın hatalar, performans ipuçları ve...