Eğitim Portalı/Spring Boot/Spring Data JPA
Spring Boot03-spring-boot/02-spring-data-jpa

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.yml ile 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şirsen LazyInitializationException alırsın. Örnekte sadelik için EAGER kullandık. Üretimde LAZY + JOIN FETCH (@Query ... join fetch) ya da @EntityGraph tercih edilir; aksi halde her kayıt için ek sorgu atan N+1 problemi doğar.

JpaRepository hiyerarşisi

RepositoryCrudRepository (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 gerekir
Ornek1.java
1// 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> {}
canlı çalıştırma için ortam gerekir

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 gerekir
Ornek2.java
1// 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}
canlı çalıştırma için ortam gerekir

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 gerekir
Ornek3.java
1// 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> {}
canlı çalıştırma için ortam gerekir

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.