Spring Data JPA ile veri katmanı yazmak: entity, repository ve sorgu
Beceri rehberi

Spring Data JPA ile veri katmanı yazmak: entity, repository ve sorgu

Hazırlayan: Mindset Institute EditörlüğüGüncellendi: 5 dk okuma

Spring Data JPA ile entity tanımlama, repository yazma, türetilmiş ve özel sorgular, ilişkiler ve performans tuzaklarını örneklerle öğrenin.

JPA, Hibernate ve Spring Data: kim ne yapar?

Bu adlar sık birbirine karışır. JPA, Java nesnelerini ilişkisel tablolara eşlemek için bir belirtimdir; yani kurallar kümesidir. Hibernate bu belirtimin yaygın bir gerçeklemesidir ve Spring Boot'ta varsayılan olarak onunla çalışırsınız. Spring Data JPA ise bu katmanların üstüne oturan ve tekrar eden veri erişim kodunu üreten bir kitaplıktır: arayüz tanımlarsınız, uygulamayı Spring çalışma zamanında üretir.

Bu ayrım pratik sonuç doğurur. Bir sorgu yavaşsa sorunun nerede olduğunu bilmek için hangi katmanın ne ürettiğine bakmanız gerekir: Spring Data hangi metodu çağırdı, Hibernate bunu hangi SQL'e çevirdi, veri tabanı o SQL'i nasıl yürüttü? Spring Data JPA'yı iyi kullanmak, arkasında üretilen SQL'i okuyabilmeyi de gerektirir.

Entity ve repository: ilk çalışan örnek

Bir tabloya karşılık gelen sınıfı @Entity ile işaretleyin, birincil anahtarı @Id ile belirtin:

@Entity class Customer { @Id @GeneratedValue private Long id; private String name; private String email; }

Ardından veri erişimi için bir arayüz tanımlayın:

interface CustomerRepository extends JpaRepository<Customer, Long> { Optional<Customer> findByEmail(String email); }

Bu tanımlarla kaydetme, silme, kimliğe göre bulma, tüm kayıtları sayfalı getirme hazırdır. findByEmail gibi metot adlarından Spring sorguyu türetir: 'findBy' önekinden sonra gelen alan adı, 'And', 'Or', 'OrderBy', 'Containing' gibi anahtar sözcüklerle birleştirilebilir. Metot adı çok uzayıp okunmaz hale gelirse türetilmiş sorgudan vazgeçip açık bir sorguya geçin.

Sorgu yazmanın seçenekleri

Basit aramalarda türetilmiş sorgu yeterlidir. Daha karmaşık koşullar için seçenekleriniz şunlardır:

  • @Query anotasyonu ile JPQL ya da gerekirse yerel SQL yazmak. Sorguyu metodun üstünde açıkça görmek, okunabilirlik açısından türetilmiş uzun adlardan iyidir.
  • Specification ile dinamik koşullar kurmak. Kullanıcının seçtiği filtrelere göre değişen sorgular için uygundur.
  • Projeksiyon kullanmak. Yalnızca birkaç alan gerekiyorsa tüm entity'yi yüklemek yerine arayüz ya da kayıt sınıfı ile sadece o alanları çekin.

Sayfalama için Pageable parametresi eklemek yeterlidir; yanıt Page nesnesi olarak toplam kayıt bilgisiyle gelir. Büyük tablolarda toplam sayıyı hesaplamak ayrı bir sorgu gerektirir; sayıya ihtiyacınız yoksa Slice kullanarak bu maliyeti ortadan kaldırın.

Güncelleme ve silme sorguları için @Modifying kullanılır; bu işlemler entity yaşam döngüsünü atladığı için bellekteki nesnelerle tutarsızlık yaratabilir, kullanırken dikkat edin.

İlişkiler ve N+1 sorgu sorunu

İlişkileri @OneToMany, @ManyToOne ve @ManyToMany ile eşlersiniz. Burada en sık yapılan hata, ilişkili nesnelerin ne zaman yüklendiğini bilmemektir. Koleksiyon ilişkileri varsayılan olarak tembel (lazy) yüklenir: ana kaydı çekersiniz, ilişkili liste ilk eriştiğinizde ayrı bir sorgu ile gelir.

Bir sipariş listesini döngüyle gezip her siparişin kalemlerine eriştiğinizde, ana sorgudan sonra her sipariş için ayrı bir sorgu çalışır: bu N+1 sorunudur ve kayıt sayısı büyüdükçe yavaşlığın baş nedenlerinden biri olur. Çözümler şunlardır: sorguda JOIN FETCH kullanmak, @EntityGraph ile hangi ilişkilerin birlikte yükleneceğini belirtmek ya da yalnızca gereken verileri projeksiyonla çekmek.

Teşhis için geliştirme ortamında üretilen SQL'i günlüğe yazdırın ve bir isteğin kaç sorgu çalıştırdığını sayın. Bu kontrolü testlere eklemek, sorunu üretime çıkmadan yakalar.

İşlem (transaction) sınırlarını doğru çizmek

Veri değiştiren servis metotlarını @Transactional ile işaretleyin. İşlem sınırı, servis katmanında olmalıdır; controller'da ya da repository'de değil. Çünkü bir iş kuralı birden fazla repository çağrısı yapabilir ve hepsinin ya birlikte tamamlanması ya da birlikte geri alınması gerekir.

İşlem içinde yüklenen entity yönetilen durumdadır: alanını değiştirirseniz, işlem bitişinde değişiklik veri tabanına yansır; ayrıca save çağırmanız şart değildir. Bu kolaylık, istemeden güncelleme yapma riskini de getirir. Salt okuma yapan metotlarda readOnly seçeneği hem niyeti belli eder hem de bazı iyileştirmelere imkân tanır.

İşlem dışına çıkan nesneye eriştiğinizde tembel yükleme hatası alırsınız. Çözüm, nesneyi işlem içinde gereken yere kadar yüklemek ve dışarıya bir yanıt nesnesi olarak vermektir. Bağlantıyı açık tutan ayarlarla bu hatayı gizlemek, performans sorunlarını sonraya erteler.

Şema yönetimi ve test

Hibernate'in şemayı otomatik güncellemesi geliştirme için kullanışlıdır, ancak üretimde şema değişikliklerini sürümlenmiş betiklerle yönetmek gerekir. Bu amaçla Flyway ya da Liquibase gibi geçiş (migration) araçları kullanılır; her değişiklik bir dosyadır ve sırayla uygulanır. Üretimde Hibernate'in şemayı değiştirmesine izin vermeyin.

Repository testlerinde yalnızca JPA katmanını ayağa kaldıran test anotasyonu hızlıdır. Ancak yerleşik bellek içi veri tabanı ile gerçek veri tabanı davranış farklıdır: özel SQL fonksiyonları, indeks davranışı ve kilitleme gerçek motorda farklı çalışır. Konteyner ile gerçek veri tabanı motorunu test sırasında ayağa kaldırmak, bu farkı kapatır.

Bir repository için önce davranışı belirleyen testi yazın: kayıt bulunuyor mu, olmayan kayıt boş dönüyor mu, sıralama doğru mu, sayfalama sınırları nasıl çalışıyor?

Sık yapılan hatalar

  • Entity'yi API'ye doğrudan dönmek. İlişkiler istemeden yüklenir, döngüsel serileştirme çıkar.
  • Her ilişkiyi eager yapmak. Tüm sorgular gereksiz veri çeker.
  • equals ve hashCode'u kimlik alanına göre yanlış yazmak. Yeni kayıtlar kümelerde yanlış davranır.
  • Büyük listeleri sayfalamadan çekmek. Bellek ve gecikme sorunları ortaya çıkar.
  • Toplu işlemleri tek tek save ile yapmak. Toplu ekleme ve güncelleme için toplu işlem ayarlarını ve uygun kimlik üretimi stratejisini düşünün.
  • Yerel SQL'i her yerde kullanmak. Taşınabilirlik düşer; yalnızca JPQL'in yetmediği yerde başvurun.
  • Veri tabanı indekslerini hiç düşünmemek. Türetilmiş sorgu çalışsa bile, aranan alanda indeks yoksa tablo taraması yapılır.

Bu beceriyi Mindset programlarında nerede çalışırsınız?

Java Spring Boot ile Backend programında Spring Data JPA ile veri katmanı ayrı bir modül olarak yer alıyor ve Spring Core ile Web MVC modülleri bu katmanın üzerine kuruluyor. Program, Java bilen ve kurumsal bir backend servisinin veri erişimini yazmak isteyen geliştiriciler ile bu standardı ekibine yerleştirmek isteyen teknik liderler için uygun görünüyor.

ORM kullanımını Spring'den bağımsız, tasarım açısından ele almak isteyenler için ORM ve Veri Erişim Katmanı Tasarımı (Prisma, Hibernate) programı da bu sayfanın konusuyla örtüşüyor. Ekip düzeyinde en çok kazandıran şey, ortak bir repository ve işlem sınırı kuralıdır.

İlgili Mindset programı

Java Spring Boot ile Backend

Sık sorulanlar

Sık sorulan sorular

Spring Data JPA ile Hibernate'i ayrı ayrı öğrenmem gerekir mi?

Spring Data JPA'yı kullanırken Hibernate arka planda çalışır. Başlangıçta repository kullanımı yeterlidir; ancak lazy yükleme, işlem yönetimi ve üretilen SQL'i okuma gibi konularda Hibernate'in nasıl çalıştığını bilmek, sorun giderirken kritik hale gelir.

findAll yerine ne kullanmalıyım?

Büyük tablolarda findAll tüm kayıtları belleğe alır. Bunun yerine Pageable alan sayfalı sorgular kullanın. Ayrıca tüm alanlara ihtiyacınız yoksa projeksiyonla yalnızca gereken kolonları getirmek hem bellek hem ağ yükünü azaltır.

Lazy yükleme hatasını nasıl çözerim?

Hata, işlem dışında tembel ilişkiye erişmekten gelir. İlişkiyi işlem içinde yükleyin: JOIN FETCH ya da EntityGraph kullanın, ya da yanıt nesnesine dönüştürmeyi işlem içinde yapın. Bağlantıyı istek boyunca açık tutan ayar hatayı gizler ama sorguları arttırabilir.

Türetilmiş sorgu ile @Query arasında nasıl seçim yaparım?

Birkaç basit koşul için türetilmiş sorgu okunaklıdır. Koşul sayısı arttıkça ya da birleştirme gerektiğinde metot adı uzar ve hata yapmak kolaylaşır; bu noktada @Query ile açık yazılmış bir sorgu daha anlaşılırdır ve ayrıca doğrudan test edilebilir.

Spring Data JPA Nasıl Kullanılır: Repository ve Sorgu