Spring Data JPA
JDBC ve JdbcTemplate ile veritabanına SQL yazarak eriştik. Spring Data JPA, bir adım daha ileri gider: nesneleri tablolara eşler (ORM, Hibernate ile) ve veri erişim kodunun büyük kısmını sen hiç yazmadan üretir. Bir a…
Spring Data JPA
JDBC ve JdbcTemplate ile veritabanına SQL yazarak eriştik. Spring Data JPA, bir adım daha ileri gider: nesneleri tablolara eşler (ORM, Hibernate ile) ve veri erişim kodunun büyük kısmını sen hiç yazmadan üretir. Bir arayüz tanımlarsın, Spring onun implementasyonunu çalışma anında oluşturur. Bu, Spring Boot uygulamalarında veritabanıyla çalışmanın en yaygın yoludur.
Bu bölümün örnekleri Boot'un otomatik yapılandırdığı gömülü H2 veritabanını kullanır; tablolar uygulama açılışında otomatik oluşturulur. Gerçek projede
application.ymlile MySQL/PostgreSQL bağlantısı verilir — kod aynı kalır.
Entity: nesne ↔ tablo eşlemesi
Bir sınıfı @Entity ile işaretlersin; Hibernate onu bir tabloya eşler. @Id birincil anahtarı,
@GeneratedValue ise id'nin nasıl üretileceğini belirtir:
@Entity class Gorev { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String baslik; private boolean tamamlandi; }
Repository: yazmadığın kod
Asıl sihir burada. JpaRepository<Entity, IdTipi>'yi genişleten bir arayüz tanımlarsın —
gövde yok — ve Spring Data sana hazır bir implementasyon verir:
interface GorevRepository extends JpaRepository<Gorev, Long> {}
Bu tek satırla save, findAll, findById, deleteById, count ve daha fazlası gelir. Örnek 1
(./Ornek1.java) tek satır SQL yazmadan tam bir CRUD döngüsü çalıştırır.
Türetilmiş sorgular (derived queries) ve @Query
İhtiyacın hazır metotların ötesine geçtiğinde, Spring Data metot adından sorgu üretir:
List<Urun> findByKategori(String kategori); List<Urun> findByFiyatLessThan(double fiyat); List<Urun> findByAdContainingIgnoreCase(String parca); long countByKategori(String kategori); List<Urun> findTop2ByOrderByFiyatDesc();
findBy, And, Or, LessThan, Containing, IgnoreCase, OrderBy, Top/First gibi anahtar
kelimeleri birleştirebilirsin. Metot adının ifade edemeyeceği karmaşık sorgular için @Query
ile JPQL (veya nativeQuery = true ile saf SQL) yazarsın:
@Query("select u from Urun u where u.fiyat between :min and :max order by u.fiyat") List<Urun> fiyatAraliginda(@Param("min") double min, @Param("max") double max);
Örnek 2 (./Ornek2.java) hem türetilmiş sorguları hem de @Query'yi canlı gösterir.
İlişkiler
Gerçek veriler ilişkilidir: bir yazarın çok kitabı, bir siparişin çok kalemi vardır. JPA bunları anotasyonlarla modeller:
@OneToMany— bir → çok (bir yazar, çok kitap)@ManyToOne— çok → bir (her kitap, bir yazar)@ManyToMany— çok → çok (öğrenci ↔ ders)
@Entity class Yazar { @OneToMany(mappedBy = "yazar", cascade = CascadeType.ALL) private List<Kitap> kitaplar; } @Entity class Kitap { @ManyToOne private Yazar yazar; }
mappedBy ilişkinin sahibini (foreign key'in hangi tarafta olduğunu), cascade ise işlemlerin
ilişkili kayıtlara yayılıp yayılmayacağını belirler (yazarı kaydetmek kitaplarını da kaydeder).
Örnek 3 (./Ornek3.java) yazar-kitap ilişkisini kurar, cascade ile birlikte kaydeder ve her
yazarın kitaplarını listeler.
Lazy vs Eager + N+1 tuzağı: İlişkiler varsayılan olarak (
@OneToMany) LAZY yüklenir; ilişkili veriye transaction dışında erişirsenLazyInitializationExceptionalırsın. Örnekte sadelik içinEAGERkullandık. Üretimde LAZY + JOIN FETCH (@Query ... join fetch) ya da@EntityGraphtercih edilir; aksi halde her kayıt için ek sorgu atan N+1 problemi doğar.
JpaRepository hiyerarşisi
Repository ← CrudRepository (temel CRUD) ← PagingAndSortingRepository (sayfalama/sıralama)
← JpaRepository (JPA'ya özel toplu işlemler, flush, saveAll...). Çoğu zaman doğrudan
JpaRepository kullanırsın. Sayfalama için metoda bir Pageable parametresi ekleyip Page<T>
döndürebilirsin.
Özet
Spring Data JPA ile @Entity eşlemesini, JpaRepository arayüzünün sağladığı kodsuz CRUD'u
(Örnek 1), türetilmiş sorguları ve @Query'yi (Örnek 2) ve @OneToMany/@ManyToOne ilişkilerini
(Örnek 3) gerçek, çalışan bir uygulamada gördük; lazy/eager ve N+1 tuzağına değindik. Veri
katmanı artık çok güçlü. Sırada, bu API'lere gelen veriyi doğrulamak ve hataları zarifçe yönetmek:
validation ve global exception handling.
▶ Kod Örnekleri(3)
Ornek1
ortam gerekir1// Ornek1: Spring Data JPA temelleri — @Entity ve JpaRepository ile sıfır SQL yazmadan CRUD.
2// Gömülü H2 veritabanı Boot tarafından otomatik yapılandırılır.
3// Çalıştırma: portal derleyip gömülü Tomcat + H2 ile başlatır.
4package com.egitim.springboot.jpa;
5
6import jakarta.persistence.Entity;
7import jakarta.persistence.GeneratedValue;
8import jakarta.persistence.GenerationType;
9import jakarta.persistence.Id;
10import org.springframework.boot.CommandLineRunner;
11import org.springframework.boot.SpringApplication;
12import org.springframework.boot.autoconfigure.SpringBootApplication;
13import org.springframework.context.annotation.Bean;
14import org.springframework.data.jpa.repository.JpaRepository;
15
16@SpringBootApplication
17public class Ornek1 {
18
19 public static void main(String[] args) { SpringApplication.run(Ornek1.class, args); }
20
21 @Bean
22 CommandLineRunner selfTest(GorevRepository repo) {
23 return args -> {
24 System.out.println("\n================ SPRING DATA JPA ================");
25
26 // CREATE — save() (tek satır, SQL yok)
27 Gorev g1 = repo.save(new Gorev("Spring Boot öğren"));
28 repo.save(new Gorev("JPA öğren"));
29 Gorev g3 = repo.save(new Gorev("REST API yaz"));
30 System.out.println("Kaydedildi. Toplam: " + repo.count() + " görev. İlkinin id'si: " + g1.getId());
31
32 // READ — findAll, findById
33 System.out.println("Tüm görevler:");
34 repo.findAll().forEach(g -> System.out.println(" " + g));
35 System.out.println("findById(" + g3.getId() + "): " + repo.findById(g3.getId()).orElse(null));
36
37 // UPDATE — nesneyi değiştir, save ile kaydet
38 g1.setTamamlandi(true);
39 repo.save(g1);
40 System.out.println("Güncellendi: " + repo.findById(g1.getId()).orElse(null));
41
42 // DELETE
43 repo.deleteById(g3.getId());
44 System.out.println("Silme sonrası toplam: " + repo.count());
45 System.out.println("=================================================");
46 System.out.println("Dikkat: tek satır SQL yazmadık. JpaRepository tüm CRUD'u sağladı.");
47 };
48 }
49}
50
51// @Entity: bu sınıf bir veritabanı tablosuna eşlenir (Hibernate ile).
52@Entity
53class Gorev {
54 @Id
55 @GeneratedValue(strategy = GenerationType.IDENTITY) // id'yi veritabanı üretir
56 private Long id;
57 private String baslik;
58 private boolean tamamlandi;
59
60 protected Gorev() {} // JPA boş constructor ister
61 Gorev(String baslik) { this.baslik = baslik; }
62
63 Long getId() { return id; }
64 String getBaslik() { return baslik; }
65 boolean isTamamlandi() { return tamamlandi; }
66 void setTamamlandi(boolean t) { this.tamamlandi = t; }
67
68 @Override public String toString() {
69 return "Gorev{id=" + id + ", baslik='" + baslik + "', tamamlandi=" + tamamlandi + "}";
70 }
71}
72
73// Repository: SADECE bir arayüz! Spring Data JPA implementasyonu otomatik üretir.
74// JpaRepository<Entity, IdTipi> ile save/findAll/findById/deleteById/count hazır gelir.
75interface GorevRepository extends JpaRepository<Gorev, Long> {}Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.
Ornek2
ortam gerekir1// Ornek2: Türetilmiş sorgular (derived queries) ve @Query ile özel JPQL.
2// Spring Data, metot ADINDAN sorgu üretir; karmaşık durumlar için @Query yazarsın.
3// Çalıştırma: portal derleyip gömülü Tomcat + H2 ile başlatır.
4package com.egitim.springboot.jpa;
5
6import jakarta.persistence.Entity;
7import jakarta.persistence.GeneratedValue;
8import jakarta.persistence.GenerationType;
9import jakarta.persistence.Id;
10import org.springframework.boot.CommandLineRunner;
11import org.springframework.boot.SpringApplication;
12import org.springframework.boot.autoconfigure.SpringBootApplication;
13import org.springframework.context.annotation.Bean;
14import org.springframework.data.jpa.repository.JpaRepository;
15import org.springframework.data.jpa.repository.Query;
16import org.springframework.data.repository.query.Param;
17
18import java.util.List;
19
20@SpringBootApplication
21public class Ornek2 {
22
23 public static void main(String[] args) { SpringApplication.run(Ornek2.class, args); }
24
25 @Bean
26 CommandLineRunner selfTest(UrunRepository repo) {
27 return args -> {
28 repo.save(new Urun("Klavye", "aksesuar", 450));
29 repo.save(new Urun("Mouse", "aksesuar", 250));
30 repo.save(new Urun("Monitör", "ekran", 3200));
31 repo.save(new Urun("Laptop", "bilgisayar", 28000));
32 repo.save(new Urun("Mousepad", "aksesuar", 80));
33
34 System.out.println("\n================ DERIVED QUERIES + @Query ================");
35 // Metot adından üretilen sorgular:
36 System.out.println("findByKategori('aksesuar') -> " + adlar(repo.findByKategori("aksesuar")));
37 System.out.println("findByFiyatLessThan(500) -> " + adlar(repo.findByFiyatLessThan(500)));
38 System.out.println("findByAdContainingIgnoreCase('mo') -> " + adlar(repo.findByAdContainingIgnoreCase("mo")));
39 System.out.println("countByKategori('aksesuar') -> " + repo.countByKategori("aksesuar"));
40 System.out.println("findTop2ByOrderByFiyatDesc -> " + adlar(repo.findTop2ByOrderByFiyatDesc()));
41
42 // @Query ile özel JPQL:
43 System.out.println("fiyatAraliginda(100, 1000) -> " + adlar(repo.fiyatAraliginda(100, 1000)));
44 System.out.println("ortalamaFiyat() -> " + repo.ortalamaFiyat());
45 System.out.println("==========================================================");
46 System.out.println("Metot adı (findBy...) basit sorgular için; @Query karmaşık/özel sorgular için.");
47 };
48 }
49
50 static String adlar(List<Urun> liste) { return liste.stream().map(Urun::getAd).toList().toString(); }
51}
52
53@Entity
54class Urun {
55 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
56 private Long id;
57 private String ad;
58 private String kategori;
59 private double fiyat;
60
61 protected Urun() {}
62 Urun(String ad, String kategori, double fiyat) { this.ad = ad; this.kategori = kategori; this.fiyat = fiyat; }
63 String getAd() { return ad; }
64}
65
66interface UrunRepository extends JpaRepository<Urun, Long> {
67 // Metot adından otomatik sorgu (derived query):
68 List<Urun> findByKategori(String kategori);
69 List<Urun> findByFiyatLessThan(double fiyat);
70 List<Urun> findByAdContainingIgnoreCase(String parca);
71 long countByKategori(String kategori);
72 List<Urun> findTop2ByOrderByFiyatDesc();
73
74 // Karmaşık/özel sorgu için JPQL (@Query):
75 @Query("select u from Urun u where u.fiyat between :min and :max order by u.fiyat")
76 List<Urun> fiyatAraliginda(@Param("min") double min, @Param("max") double max);
77
78 @Query("select avg(u.fiyat) from Urun u")
79 Double ortalamaFiyat();
80}Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.
Ornek3
ortam gerekir1// Ornek3: İlişkiler — @OneToMany / @ManyToOne (bir yazar, birden çok kitap).
2// Çalıştırma: portal derleyip gömülü Tomcat + H2 ile başlatır.
3package com.egitim.springboot.jpa;
4
5import jakarta.persistence.CascadeType;
6import jakarta.persistence.Entity;
7import jakarta.persistence.FetchType;
8import jakarta.persistence.GeneratedValue;
9import jakarta.persistence.GenerationType;
10import jakarta.persistence.Id;
11import jakarta.persistence.ManyToOne;
12import jakarta.persistence.OneToMany;
13import org.springframework.boot.CommandLineRunner;
14import org.springframework.boot.SpringApplication;
15import org.springframework.boot.autoconfigure.SpringBootApplication;
16import org.springframework.context.annotation.Bean;
17import org.springframework.data.jpa.repository.JpaRepository;
18
19import java.util.ArrayList;
20import java.util.List;
21
22@SpringBootApplication
23public class Ornek3 {
24
25 public static void main(String[] args) { SpringApplication.run(Ornek3.class, args); }
26
27 @Bean
28 CommandLineRunner selfTest(YazarRepository yazarlar) {
29 return args -> {
30 // Bir yazar ve kitapları; cascade sayesinde yazarı kaydetmek kitapları da kaydeder.
31 Yazar yazar = new Yazar("Yaşar Kemal");
32 yazar.kitapEkle(new Kitap("İnce Memed"));
33 yazar.kitapEkle(new Kitap("Yer Demir Gök Bakır"));
34 yazarlar.save(yazar);
35
36 Yazar yazar2 = new Yazar("Sabahattin Ali");
37 yazar2.kitapEkle(new Kitap("Kürk Mantolu Madonna"));
38 yazarlar.save(yazar2);
39
40 System.out.println("\n================ İLİŞKİLER (@OneToMany / @ManyToOne) ================");
41 System.out.println("Toplam yazar: " + yazarlar.count());
42 yazarlar.findAll().forEach(y -> {
43 System.out.println(" " + y.getAd() + " (" + y.getKitaplar().size() + " kitap):");
44 y.getKitaplar().forEach(k -> System.out.println(" - " + k.getBaslik()));
45 });
46 System.out.println("=====================================================================");
47 System.out.println("@OneToMany: bir yazarın çok kitabı; @ManyToOne: her kitabın bir yazarı.");
48 System.out.println("cascade = ALL: yazarı kaydetmek/silmek kitaplarını da kapsar.");
49 };
50 }
51}
52
53@Entity
54class Yazar {
55 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
56 private Long id;
57 private String ad;
58
59 // Bir yazarın birden çok kitabı. Karşı taraftaki "yazar" alanı sahipliği tutar (mappedBy).
60 // Demo basit olsun diye EAGER; üretimde genelde LAZY + JOIN FETCH tercih edilir.
61 @OneToMany(mappedBy = "yazar", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
62 private List<Kitap> kitaplar = new ArrayList<>();
63
64 protected Yazar() {}
65 Yazar(String ad) { this.ad = ad; }
66
67 void kitapEkle(Kitap k) { k.setYazar(this); kitaplar.add(k); } // iki yönü de bağla
68 String getAd() { return ad; }
69 List<Kitap> getKitaplar() { return kitaplar; }
70}
71
72@Entity
73class Kitap {
74 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
75 private Long id;
76 private String baslik;
77
78 // Her kitabın bir yazarı (foreign key bu tarafta tutulur).
79 @ManyToOne
80 private Yazar yazar;
81
82 protected Kitap() {}
83 Kitap(String baslik) { this.baslik = baslik; }
84
85 String getBaslik() { return baslik; }
86 void setYazar(Yazar y) { this.yazar = y; }
87}
88
89interface YazarRepository extends JpaRepository<Yazar, Long> {}Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.