Spring Data JPA, Java uygulamalarında veritabanı erişimini doğrudan ortadan kaldırmaz; bağlantı yönetimi, sorgu çalıştırma, sonuçları nesnelere dönüştürme ve kaynakların yönetilmesi gibi tekrar eden kodları uygulama katmanından büyük ölçüde ayırır. Bunun karşılığında entity eşlemelerini, repository sözleşmesini, sorgu üretimini ve JPA’nın çalışma mantığını anlamanız gerekir.
Bu nedenle repository yaklaşımı “daha az kod yazmak” ile sınırlı bir kolaylık değildir. Sorumluluk ortadan kalkmaz; JDBC kodundan Spring Data JPA altyapısına taşınır. Başarılı bir kullanım için önce Java, nesne yönelimli programlama ve SQL temellerini sağlamlaştırmak, ardından uygulamalı Java özel eğitim içeriği gibi bir sonraki öğrenme adımıyla repository, entity ve servis katmanlarını birlikte çalıştırmak gerekir.
Spring Data JPA repository yapısı neyi basitleştirir?
Geleneksel JDBC yaklaşımında bir veritabanı işlemine başlamadan önce bağlantı nesnesine erişmeniz, PreparedStatement oluşturmanız, parametreleri bağlamanız, sorguyu çalıştırmanız ve dönen ResultSet içindeki satırları Java nesnelerine dönüştürmeniz gerekir. İşlem tamamlandığında da ResultSet, statement ve bağlantı gibi kaynakların doğru biçimde kapatılması beklenir.
Spring Data JPA repository yapısı bu tekrar eden veri erişim kodlarının önemli bir bölümünü framework katmanına taşır. Spring Data’nın repository soyutlamasının temel amacı, veri erişim katmanında tekrar eden “boilerplate” kod miktarını azaltmaktır. Repository arayüzü, hangi domain sınıfıyla ve hangi kimlik tipiyle çalıştığını belirttiğinde, temel veri erişim işlemleri için hazır bir sözleşme sunabilir. ([docs.spring.io](https://docs.spring.io/spring-data/jpa/reference/jpa.html?utm_source=openai))
JDBC ile uygulama kodunda kalan sorumluluklar
Örneğin bir kitabı ISBN değerine göre bulmak istediğinizi düşünün. Ham JDBC kullanan bir uygulamada aşağıdaki işlemler uygulama kodunuzda açıkça görünür:
- Veritabanı bağlantısının alınması,
- SQL cümlesinin hazırlanması,
- Parametrenin
PreparedStatementnesnesine bağlanması, - Sorgunun çalıştırılması,
ResultSetiçinde satır bulunup bulunmadığının kontrol edilmesi,- Her sütunun
Booknesnesindeki doğru alana eşlenmesi, - Kaynakların hata durumlarında bile kapatılması,
- SQL veya bağlantı hatalarının uygulamanın hata modeline aktarılması.
Bu iş akışının her adımı tek başına karmaşık görünmeyebilir. Ancak aynı uygulamada kitap ekleme, kitap silme, başlığa göre arama, yazara göre filtreleme ve sayfalı listeleme gibi işlemler çoğaldıkça benzer kod blokları tekrar eder. Tek bir sorgu için yaklaşık 20-30 satır sürebilen JDBC kodunun, farklı işlemlerle birlikte yüzlerce satıra ulaşması mümkündür. Buradaki sayı belirli bir framework ölçümü değil, bağlantı kurulumu, sorgu hazırlama, eşleme ve kaynak yönetimini ayrı ayrı yazan tipik bir uygulama akışını somutlaştıran yaklaşık bir karşılaştırmadır.
Repository yaklaşımında hangi işler framework katmanına taşınır?
Spring Data JPA kullandığınızda uygulama kodunuz çoğu temel işlem için SQL yürütme ayrıntılarını doğrudan barındırmaz. Siz repository arayüzünde hangi entity ile çalıştığınızı ve ihtiyaç duyduğunuz sorgu sözleşmesini ifade edersiniz. Repository proxy’si, bu sözleşmeyi JPA altyapısı ve bir JPA sağlayıcısı üzerinden veritabanı işlemlerine dönüştürür.
Böylece aşağıdaki sorumluluklar büyük ölçüde repository altyapısına bırakılır:
- JPA persistence context ve
EntityManagerüzerinden veri erişiminin yürütülmesi, - Entity alanlarıyla sorgu sonuçları arasındaki eşleme,
- Temel CRUD işlemleri için sorgu yürütme akışı,
- Parametrelerin sorguya bağlanması,
- Veri erişim istisnalarının Spring’in veri erişim hata modeline uyarlanması,
- Repository çağrılarının uygulama katmanında sade metot çağrıları olarak kullanılabilmesi.
Buradaki kolaylık, “veritabanı hakkında hiçbir şey bilmeye gerek yok” anlamına gelmez. Tam tersine, repository katmanı daha az kod yazdırdığı için altta gerçekleşen işlemi doğru yorumlamak daha önemli hâle gelir. Bir findAll() çağrısının büyük bir tabloyu belleğe taşıyabileceğini, bir ilişkinin beklenmedik sorgular üretebileceğini veya uygun indeks olmayan bir filtrelemenin yavaşlayabileceğini bilmek hâlâ geliştiricinin sorumluluğundadır.
Daha az kod, daha az sorumluluk demek değildir
Repository soyutlamasının bedeli, bazı kararların görünür Java kodundan çıkarak eşleme ve framework kurallarına taşınmasıdır. JDBC’de SQL cümlesini doğrudan görür ve sonuç eşlemesini kendiniz yazarsınız. JPA’da ise entity’nin hangi alanının hangi kalıcı veriye karşılık geldiğini anotasyonlarla ve sınıf yapısıyla ifade edersiniz.
Bu değişim iki farklı sonuç doğurur:
- Olumlu taraf: Tekrarlanan bağlantı, statement, result set ve eşleme kodları azalır; CRUD işlemleri daha okunabilir bir arayüz üzerinden çağrılır.
- Dikkat edilmesi gereken taraf: Sorgunun ne zaman çalıştığını, entity yaşam döngüsünü, transaction sınırlarını, ilişki yükleme davranışını ve üretilen SQL’i anlamadan yapılan kullanım hatalı veya verimsiz olabilir.
Bu yüzden Spring Data JPA’yı yalnızca “SQL yazmadan veritabanı kullanma yöntemi” olarak tanımlamak eksik kalır. Daha doğru tanım şudur: JPA entity modelini ve repository sözleşmesini kullanarak veri erişim kodunu soyutlayan, ancak geliştiriciden kalıcılık modelini ve veritabanı davranışını anlamasını bekleyen bir katmandır.
Entity ve repository katmanı nasıl birlikte çalışır?

JPA’da entity, uygulamadaki bir Java sınıfının kalıcı veritabanı kaydıyla ilişkilendirilmesini sağlayan modeldir. @Entity anotasyonu sınıfın bir JPA entity’si olarak ele alınacağını belirtir. @Id ise entity’nin kimlik alanını gösterir. Alanların tablo ve kolonlarla nasıl eşleşeceği; adlandırma kuralları, açık anotasyonlar ve uygulama yapılandırması gibi etkenlere bağlı olabilir. Bu nedenle varsayılan tablo veya kolon adları hakkında her proje için değişmez bir kural varmış gibi düşünmek yerine, kritik eşlemeleri açıkça tanımlamak daha güvenlidir.
Aşağıdaki örnekte bir kitap modeli oluşturulmuştur. Kimlik, başlık, yazar ve ISBN alanları, repository işlemlerini göstermek için yeterlidir:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private String isbn;
protected Book() { }
public Book(String title, String author, String isbn) {
this.title = title;
this.author = author;
this.isbn = isbn;
}
}
Bu sınıfta korumalı boş constructor bulunmasının nedeni JPA’nın entity örneği oluştururken ihtiyaç duyabileceği yapıyı sağlamaktır. Gerçek bir uygulamada getter ve setter yöntemleri, alan doğrulamaları, benzersizlik kısıtları, açık kolon adları, eşleme stratejisi ve uygun transaction yapılandırması ayrıca ele alınabilir.
@Entity ve @Id neyi ifade eder?
@Entity, Book sınıfını JPA’nın yönettiği domain modelinin bir parçası hâline getirir. Sınıftaki kalıcı alanlar, JPA sağlayıcısının eşleme kuralları doğrultusunda veritabanındaki kolonlarla ilişkilendirilir. @Id ile işaretlenen alan, bir Book kaydını diğer kayıtlardan ayıran kimlik değeridir.
@GeneratedValue kullanımı, kimlik değerinin nasıl üretileceğine ilişkin stratejiyi belirtir. Ancak kimlik üretiminin veritabanında nasıl gerçekleşeceği, kullanılan veritabanı ve uygulama yapılandırmasıyla birlikte değerlendirilmelidir. Entity anotasyonlarını eklemek tek başına bağlantı kurulumu, şema oluşturma veya transaction yönetiminin tamamlandığı anlamına gelmez.
JpaRepository temel CRUD işlemlerini nasıl sunar?
JpaRepository<Book, Long> ifadesi repository’nin Book entity’siyle ve Long türündeki kimlik alanıyla çalıştığını belirtir. Bu arayüz, temel repository sözleşmelerinden miras alınan hazır işlemlerle birlikte kullanılabilir. Spring Data JPA dokümantasyonunda JpaRepository, JPA’ya özgü repository soyutlamalarından biri olarak tanımlanır. ([docs.spring.io](https://docs.spring.io/spring-data/jpa/reference/4.2/repositories/core-concepts.html?utm_source=openai))
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
Optional<Book> findByIsbn(String isbn);
}
Buradaki findByIsbn metodu, repository’nin yalnızca hazır CRUD işlemleriyle sınırlı kalmadığını gösterir. Metot adındaki Isbn bölümü, Book entity’sindeki isbn özelliğine karşılık gelir. Bu tür türetilmiş metotların ayrıntılı çalışma mantığı bir sonraki bölümde incelenebilir; temel fikir, sorgu sözleşmesinin metot adı üzerinden ifade edilmesidir.
Aşağıdaki servis kullanımında temel işlemler doğrudan repository metotlarıyla gösterilmiştir:
Book saved = bookRepository.save(
new Book("Clean Code", "Robert C. Martin", "9780132350884"));
Optional<Book> book = bookRepository.findById(saved.getId());
List<Book> books = bookRepository.findAll();
Optional<Book> byIsbn = bookRepository.findByIsbn("9780132350884");
bookRepository.deleteById(saved.getId());
Bu örnekte beş farklı veri erişim ihtiyacı karşılanır:
save: yeni bir entity’nin kalıcı hâle getirilmesi veya uygun durumda mevcut bir entity’nin kaydedilmesi için kullanılır.findById: kimliğe göre tek bir kaydı arar ve sonucuOptional<Book>olarak taşır.findAll: repository kapsamındaki entity kayıtlarını listelemek için kullanılır.findByIsbn: entity özelliğine göre özel bir arama sözleşmesi sunar.deleteById: belirtilen kimliğe sahip kaydın silinmesi için kullanılır.
findById sonucunun Optional olması, kaydın bulunamama durumunu açıkça ele almayı teşvik eder. Buna karşılık findAll kullanılırken sonuç kümesinin boyutu ve sayfalama ihtiyacı ayrıca düşünülmelidir. Büyük tabloları koşulsuz biçimde belleğe almak, repository kullanımının sade olmasına rağmen uygulama açısından uygun olmayabilir.
@Repository anotasyonu ile Spring Data repository’si arasındaki fark
@Repository, Spring’in veri erişim katmanındaki sınıfları ifade eden bir stereotype anotasyonudur. Component scanning mekanizmasının bu sınıfları Spring bileşeni olarak bulmasına yardımcı olabilir ve veri erişim istisnalarının çevrilmesiyle ilişkilendirilebilir. Bu rol, özellikle veri erişim işlemlerini kendisi uygulayan somut DAO veya repository sınıflarında önemlidir. ([docs.spring.io](https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html?utm_source=openai))
Ancak JpaRepository kullanan arayüzde çoğu temel senaryoda bağlantı açma, statement oluşturma veya metot gövdesi yazılmaz. Spring Data JPA, repository arayüzü için çalışma zamanında bir proxy oluşturur ve bu proxy üzerinden veri erişim işlemlerini yürütür. Dolayısıyla @Repository anotasyonunun genel Spring anlamını, JpaRepository arayüzünün Spring Data tarafından sağlanan özel repository altyapısını ve entity’nin JPA eşleme rolünü birbirine karıştırmamak gerekir.
Özetle katmanlar şu şekilde ayrılır:
- Entity katmanı: Kalıcı verinin Java nesnesi olarak nasıl temsil edileceğini açıklar.
- Repository katmanı: Entity üzerinde hangi veri erişim işlemlerinin çağrılabileceğini tanımlar.
- JPA altyapısı: Repository çağrılarını persistence context ve entity yönetimi üzerinden yürütür.
- Veritabanı sürücüsü ve veritabanı: Son SQL işlemlerini gerçekleştirir ve veriyi kalıcı olarak saklar.
Gerçek bir Spring Boot uygulamasında bu örneğin çalışması için yalnızca iki Java dosyası yeterli değildir. Projede Spring Data JPA bağımlılığı, bir veritabanı sürücüsü, bağlantı ayarları, entity tarama kapsamı ve repository tarama yapılandırması bulunmalıdır. Ayrıca işlem sınırları, şema yönetimi ve üretim ortamındaki veritabanı ayarları uygulamanın ihtiyaçlarına göre belirlenmelidir. Bu ayrıntılar sürümden bağımsız tek bir reçete değildir; kullanılan Spring Boot, Spring Data JPA, JPA sağlayıcısı ve veritabanı kombinasyonuna göre doğrulanmalıdır.
Türetilmiş sorgu metotları isimlerden nasıl sorgu üretir?
Spring Data JPA’da türetilmiş sorgu metotları, repository içindeki Java metot adını okuyarak entity özelliklerine dayalı bir sorgu oluşturur. Temel kural şudur: ilk “By” ifadesinden sonraki bölüm sorgu koşullarını tanımlar; bu koşullar entity içindeki alan adlarıyla eşleşir ve aralarındaki mantıksal bağlaçlar sorgunun yapısını belirler.
Örneğin findByTitle metodu, Book entity’sinde title adında bir özellik bulunduğunu varsayar. Spring Data JPA bu adı kontrol eder ve uygun bir JPQL sorgusu üretir. Resmî dokümantasyondaki çalışma modelinde, findByEmailAddressAndLastname gibi bir metot adı sırasıyla entity özelliklerine ayrıştırılır ve koşullar AND ile bağlanır. Spring Data JPA Query Methods dokümantasyonu da türetilmiş sorguların entity özellikleri üzerinden oluşturulduğunu belirtir. ([docs.spring.io](https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html?utm_source=openai))
Book entity alanları ile metot adları arasındaki ilişki
Aşağıdaki entity, repository metotlarının hangi özellik adlarına göre çözümlendiğini göstermek için yeterlidir:
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private String isbn;
private boolean available;
}
Bu sınıfta sorgu metotlarının kullanabileceği doğrudan özellikler title, author, isbn ve available alanlarıdır. Metot adında bu alanlar Java’daki özellik adlarıyla uyumlu biçimde yazılmalıdır. Örneğin entity alanı bookTitle ise findByTitle değil, findByBookTitle kullanılması gerekir.
Alan adındaki büyük-küçük harf geçişleri metot adında camel case biçiminde ifade edilir. İç içe nesnelerde Spring Data JPA özellik yollarını da takip edebilir; ancak bu durumda ilişki adlarının, hedef entity özelliklerinin ve sorgu yolunun birlikte doğru olması gerekir. Bir özellik çözümlenemediğinde sorun genellikle uygulama çalışırken rastgele değil, repository sorgusu oluşturulurken ortaya çıkar. Bu nedenle metot adını entity alanlarıyla karşılaştırmak, derleme ve uygulama başlatma kontrollerinin önemli bir parçasıdır. ([docs.spring.io](https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html?utm_source=openai))
findByTitle: tek koşuldan liste döndürmek
findByTitle metodu, kitapların başlığına göre arama yapar:
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findByTitle(String title);
Optional<Book> findByIsbn(String isbn);
boolean existsByIsbn(String isbn);
}
findByTitle(String title) ifadesi şu şekilde okunur:
find: kayıt veya kayıtlar getirilecek.By: filtre koşulları bundan sonra başlıyor.Title:Book.titleözelliği üzerinden eşitlik koşulu oluşturulacak.List<Book>: aynı başlığa sahip birden fazla kayıt dönebileceği kabul ediliyor.
Başlık veritabanında benzersiz değilse List<Book> uygun bir dönüş tipidir. Birden fazla kayıt bulunması normal kabul edilir ve sonuçlar liste halinde alınır. Eğer iş kuralı gereği başlık benzersiz olsaydı Optional<Book> tercih edilebilirdi; fakat bu durumda aynı başlığa sahip birden fazla kayıt bulunması beklenmeyen sonuç boyutu hatasına yol açabilir.
Tek bir kayıt aranırken dönüş tipini açıkça belirtmek önemlidir. Ham Book döndüren bir sorgu en fazla bir sonuç bekler ve kayıt bulunamazsa null dönebilir. Optional<Book> kullanıldığında bulunamayan kayıt Optional.empty() olarak ifade edilir. Böylece çağıran kod, eksik kayıt ihtimalini kontrol etmeye zorlanır. ([docs.spring.io](https://docs.spring.io/spring-data/jpa/reference/repositories/query-return-types-reference.html?utm_source=openai))
ISBN genellikle kitap için benzersiz bir iş kuralına daha yakın olduğundan, findByIsbn için Optional<Book> anlamlı bir seçimdir:
Optional<Book> result = bookRepository.findByIsbn(isbn);
Book book = result.orElseThrow(() ->
new IllegalArgumentException("ISBN ile kitap bulunamadı"));
findByAuthorAndAvailable: koşulları And ile birleştirmek
findByAuthorAndAvailable metodu iki entity özelliğini aynı sorguda kullanır:
List<Book> findByAuthorAndAvailable(
String author,
boolean available
);
Bu metot aşağıdaki mantığa karşılık gelir:
Author→Book.authorAnd→ iki koşulun birlikte sağlanmasıAvailable→Book.availableList<Book>→ koşulları sağlayan birden fazla kitap olabilir
Örneğin findByAuthorAndAvailable("Ursula Le Guin", true) çağrısı, yazarı belirtilen ve available değeri true olan kitapları arar. Burada parametrelerin sırası da metot adındaki özelliklerin sırasını izler: ilk parametre author, ikinci parametre available alanına bağlanır.
Birden fazla koşul eklemek mümkün olsa da metot adı hızla okunması zor bir hale gelebilir. Örneğin findByAuthorAndAvailableAndTitleContainingIgnoreCaseOrderByTitleAsc teknik olarak anlamlı olabilir; ancak bu uzunluk, sorgunun bakım maliyetini artırır. Böyle bir noktada @Query, specification veya daha açık bir repository yaklaşımı tercih edilebilir. Spring Data JPA, metot adının gereksiz biçimde uzadığı ya da ihtiyaç duyulan ifade desteklenmediği durumlarda açıkça tanımlanmış sorgular kullanılmasını önerir. ([docs.spring.io](https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html?utm_source=openai))
existsByIsbn: nesne yerine boolean döndürmek
Bir kaydın yalnızca var olup olmadığını kontrol etmek için tüm entity nesnesini yüklemek yerine existsBy öneki kullanılabilir:
boolean isbnExists = bookRepository.existsByIsbn("978-0000000000");
if (isbnExists) {
throw new IllegalStateException("Bu ISBN zaten kayıtlı");
}
existsByIsbn şu şekilde yorumlanır:
exists: sonuç olarak varlık kontrolü yapılacak.By: filtre koşulu başlıyor.Isbn:Book.isbnalanı kontrol edilecek.boolean: kayıt varsatrue, yoksafalsedönecek.
Bu dönüş tipi, “kitabı getir ve sonra null mı diye bak” yaklaşımından daha açık bir niyet taşır. exists…By repository sorgu öneki, genel Spring Data repository kurallarında varlık projeksiyonu ve tipik olarak boolean dönüşüyle tanımlanır. ([docs.spring.io](https://docs.spring.io/spring-data/commons/reference/repositories/query-keywords-reference.html?utm_source=openai))
Türetilmiş sorgu metodu yazma akışı
Yeni bir metot tanımlarken aşağıdaki kısa akış, isimlendirme hatalarını azaltır:
- Entity alanını belirleyin: Sorgunun hangi özelliğe göre çalışacağını Book sınıfından kontrol edin.
- Doğru öneki seçin: Kayıt getirmek için
findBy, varlık kontrolü içinexistsBy, sayım içincountBykullanılabilir. - Koşulları bağlayın: Birden fazla özellik için
AndveyaOrkullanın. - Dönüş tipini ihtiyaca göre seçin: Çoklu sonuç için
List<Book>, tek ve bulunmayabilir kayıt içinOptional<Book>, varlık kontrolü içinbooleantercih edin. - Sonucu kontrol edin: SQL loglarını veya repository entegrasyon testlerini inceleyerek beklenen koşulların üretildiğini doğrulayın.
Desteklenen anahtar sözcükler arasında And, Or, Between, LessThan, GreaterThan, Containing, Like, IsNull, OrderBy ve IgnoreCase gibi ifadeler bulunur. Ancak anahtar sözcüklerin desteklenmesi ve davranışın ayrıntıları kullanılan Spring Data modülüne göre değerlendirilmelidir. Bu nedenle karmaşık bir metot adı oluştururken yalnızca kelimeleri art arda eklemek yerine kullanılan Spring Data JPA dokümantasyonundaki sorgu anahtar sözcüklerini kontrol etmek gerekir. ([docs.spring.io](https://docs.spring.io/spring-data/commons/reference/repositories/query-keywords-reference.html?utm_source=openai))
Üretilen sorguyu kontrol etmek için iki pratik yöntem vardır. Birincisi, geliştirme ortamında JPA ve veritabanı SQL loglarını açıp çağrı sırasında oluşan sorguyu incelemektir. İkincisi, belirli test verileri hazırlayarak repository entegrasyon testi yazmaktır. Test, örneğin aynı yazara ait iki kitaptan yalnızca kullanılabilir olanın döndüğünü veya bulunmayan ISBN için Optional.empty() alındığını doğrulayabilir.
Aynı Book sorgusu JDBC ile kaç satır kod gerektirir?

ISBN ile tek bir Book kaydı arama senaryosunda repository tarafındaki uygulama kodu genellikle tek satırlık bir çağrıya indirgenebilir:
Optional<Book> book = bookRepository.findByIsbn(isbn);
Bu tek satırın arkasında bağlantı yönetimi, SQL hazırlama, parametre bağlama, sonuç kümesini okuma ve sütunları Book nesnesine eşleme gibi işlemler vardır. Repository soyutlaması bu işlemleri geliştirici kodundan kaldırır; veritabanına erişimin gerçekleşmediği anlamına gelmez.
Klasik JDBC’de aynı iş akışı daha fazla uygulama kodu gerektirir. JDBC’nin temel süreci bağlantı kurma, statement veya prepared statement oluşturma, sorguyu çalıştırma, ResultSet içeriğini okuma ve kaynakları kapatma adımlarından oluşur. try-with-resources kullanmak bağlantı, statement ve result set gibi kaynakların hata durumlarında da kapatılmasını sağlar. ([docs.oracle.com](https://docs.oracle.com/javase/tutorial/jdbc/basics/processingsqlstatements.html?utm_source=openai))
Optional<Book> findByIsbn(String isbn) {
String sql = "SELECT id, title, author, isbn, available " +
"FROM books WHERE isbn = ?";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, isbn);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return Optional.empty();
Book book = new Book();
book.setId(rs.getLong("id"));
book.setTitle(rs.getString("title"));
book.setAuthor(rs.getString("author"));
book.setIsbn(rs.getString("isbn"));
book.setAvailable(rs.getBoolean("available"));
return Optional.of(book);
}
} catch (SQLException e) {
throw new DataAccessException("Book sorgusu başarısız", e);
}
}
Bu örnekte yalnızca sorguyu çalıştıran metot gövdesindeki boş olmayan uygulama satırları sayıldığında yaklaşık 16 satırlık bir JDBC akışı görülür. Satır sayısı; SQL’in ayrı satırlara bölünüp bölünmemesine, özel hata sınıfı kullanılıp kullanılmamasına ve Book eşleme işleminin ayrı bir yardımcı metoda taşınıp taşınmamasına göre değişir. Bu nedenle “JDBC her zaman tam olarak şu kadar satırdır” demek yerine karşılaştırmayı yukarıdaki örnekteki kod kapsamıyla sınırlamak daha doğrudur.
| işlem | repository yaklaşımı | klasik JDBC yaklaşımı | uygulama kodundaki yük |
|---|---|---|---|
| ISBN ile arama | findByIsbn(isbn) |
SQL metni, bağlantı, statement ve parametre hazırlama | Repository tarafında yaklaşık 1 çağrı satırı; JDBC’de örnekte yaklaşık 16 satır |
| Parametre bağlama | Metot parametresi Spring Data tarafından sorguya bağlanır | ps.setString(1, isbn) açıkça yazılır |
JDBC’de parametre sırası ve tipi için ek kontrol gerekir |
| Sonucu okuma | Entity eşlemesi JPA sağlayıcısı tarafından yapılır | ResultSet sütunları tek tek okunur |
Her yeni Book alanı için JDBC eşleme kodu genişler |
| Bulunamayan kayıt | Optional.empty() ile ifade edilebilir |
rs.next() sonucu elle kontrol edilir |
JDBC’de boş sonuç davranışı geliştirici tarafından yazılır |
| Kaynak kapatma | Repository altyapısı tarafından yönetilir | try-with-resources veya açık close() çağrıları gerekir |
JDBC’de kaynak sızıntısı riskini azaltmak için ek yapı gerekir |
Tablodaki satır farkı doğrudan performans farkı olarak yorumlanmamalıdır. Daha az uygulama kodu, veritabanının daha hızlı çalışacağı anlamına gelmez. Buradaki kazanç; tekrar eden bağlantı, parametre ve eşleme işlemlerinin merkezi bir altyapıya bırakılmasıdır. Gerçek performans; sorgunun niteliği, indeksler, veri miktarı, fetch davranışı, transaction sınırları ve kullanılan JPA sağlayıcısı gibi unsurlarla ayrıca değerlendirilmelidir.
Book CRUD akışında ortadan kalkan tekrarlar
JpaRepository<Book, Long> kullanıldığında temel CRUD işlemleri için her işlemde SQL hazırlamak ve sonuçları elle eşlemek gerekmez:
Book saved = bookRepository.save(book);
Optional<Book> found = bookRepository.findById(saved.getId());
saved.setAvailable(false);
bookRepository.save(saved);
bookRepository.deleteById(saved.getId());
Bu örnekte:
save, yeni entity’nin kaydedilmesi veya mevcut entity’nin güncellenmesi için kullanılır.findById, kimlik üzerinden arama sonucunuOptional<Book>ile ifade eder.- Güncelleme sırasında entity alanı değiştirilir ve değişiklik repository üzerinden kalıcı hale getirilir.
deleteById, kimlik üzerinden silme akışını uygulama kodunda tek metoda indirger.
Klasik JDBC yaklaşımında bu dört işlem için ayrı SQL ifadeleri, parametre bağlama kodları, sonuç okuma veya güncelleme sayısı kontrolü, SQLException yönetimi ve kaynak kapatma blokları bulunur. Örneğin güncelleme işleminde yalnızca available alanının değiştirilmesi için bile UPDATE books SET available = ? WHERE id = ? sorgusunun hazırlanması ve etkilenen satır sayısının kontrol edilmesi gerekir.
Bu tekrarların kaldırılması hata yüzeyini daraltır; çünkü her repository çağrısında aynı bağlantı ve eşleme kodunu yeniden yazmazsınız. Ancak bu, ORM’nin her durumda daha iyi seçim olduğu anlamına gelmez. Çok özel SQL ifadeleri, veritabanına özgü fonksiyonlar, karmaşık rapor sorguları veya hassas sorgu planı kontrolü gerektiğinde @Query, native SQL ya da doğrudan JDBC daha uygun olabilir. Türetilmiş metot adı okunabilirliğini kaybetmeye başladığında açık sorguya geçmek, kısa kod uğruna sorgu niyetini saklamaktan daha sağlıklı bir tasarım tercihidir.
JPA ne zaman avantajlı, ne zaman @Query veya ham SQL gerekir?
Spring Data JPA repository, uygulamanın temel veri erişim işlemleri belirli bir düzen içinde ilerliyorsa güçlü bir tercihtir. Kitap ekleme, ISBN ile arama, kayıt güncelleme, silme, tek bir entity üzerinden filtreleme ve standart sayfalama gibi işlemlerde repository katmanı tekrar eden kodu azaltır. Ancak JPA, veritabanı bilgisinin yerine geçen sihirli bir katman değildir. Sorgunun karmaşıklığı, performans beklentisi ve kullanılan veritabanına ne kadar bağımlı kalınabileceği kararın merkezinde olmalıdır.
Bu nedenle seçim yaparken “JPA mı, SQL mi?” sorusunu tek seferde cevaplamak yerine, her sorguyu ayrı değerlendirmek daha sağlıklıdır. Aynı uygulama içinde bazı işlemler türetilmiş repository metotlarıyla, bazıları @Query ile, bazıları da native SQL veya ayrı bir JDBC erişim katmanıyla yazılabilir. Hatta gerektiğinde farklı yaklaşımların aynı projede birlikte kullanılması, tek bir yöntemi her yerde zorlamaktan daha sürdürülebilir olabilir.
JPA repository kullanımının güçlü olduğu durumlar
JPA repository özellikle entity yaşam döngüsünün merkezde olduğu ve sorguların uygulama nesneleri üzerinden anlamlı biçimde ifade edilebildiği senaryolarda avantaj sağlar. Örneğin Book entity’si için ISBN aramak, stokta bulunan kitapları listelemek veya belirli bir yayıncıya ait kitapları sayfalı biçimde getirmek, repository katmanına uygun işlemlerdir.
- Basit CRUD işlemleri: Kaydetme, kimliğe göre bulma, güncelleme ve silme gibi işlemler için tekrar tekrar bağlantı açma, parametre bağlama ve sonuç dönüştürme kodu yazılmaz.
- Tek entity odaklı filtreleme: Bir entity’nin birkaç alanına göre yapılan ölçülü filtreler, türetilmiş sorgu metotlarıyla veya JPQL tabanlı
@Queryifadeleriyle okunabilir biçimde tanımlanabilir. - Standart sayfalama ve sıralama: Büyük bir kitap listesini tek seferde çekmek yerine sayfa ve sıralama bilgisiyle almak, repository arayüzünde açık bir kullanım modeli oluşturur.
- Takım içinde tutarlı erişim katmanı: Veritabanına erişimin servis katmanına dağılması yerine repository arayüzlerinde toplanması, sorumlulukların daha kolay izlenmesini sağlar.
- Entity ilişkilerinin yönetilmesi: İlişkili nesneler gerektiğinde yüklenebilir; fakat bu özellik, ilişkilerin nasıl ve ne zaman yüklendiği dikkatle tasarlanmadığında performans sorunlarına da yol açabilir.
Örneğin bir kitap yönetim uygulamasında kitap ekleme ve ISBN arama işlemleri için repository kullanmak, servis kodunu iş kurallarına ayırmayı kolaylaştırır. Servis katmanı stok değerinin negatif olup olmadığını kontrol ederken repository yalnızca veriye erişim sorumluluğunu taşır. Bu ayrım, özellikle Java ve veritabanı temellerini birlikte öğrenen geliştiriciler için önemlidir; birebir Java özel ders desteği gibi öğrenme süreçlerinde de kodun yalnızca çalışması değil, sorumluluklarının doğru ayrılması ele alınmalıdır.
Özgün karar çerçevesi: Sorguyu hangi katmanda yazmalısınız?
Bir sorgu için yöntem seçerken aşağıdaki dört soruya sırayla cevap verilebilir:
- Sorgu tek bir entity’nin doğal alanlarıyla ifade edilebiliyor mu? Cevap evetse türetilmiş sorgu metodu veya basit bir repository metodu yeterli olabilir.
- Sorgunun niyeti metot adından açıkça anlaşılabiliyor mu? Metot adı uzayıp iş kuralını gizlemeye başladıysa, sorguyu
@Queryile açıkça yazmak daha okunabilir olabilir. - Sorgu veritabanına özgü bir özelliğe veya karmaşık SQL yapısına ihtiyaç duyuyor mu? Pencere fonksiyonları, özel indeks ipuçları, veritabanına özel operatörler veya gelişmiş raporlama ifadeleri gerekiyorsa native SQL değerlendirilebilir.
- Bu sorgu performans açısından kritik mi? Çok sık çalışan, çok sayıda satır tarayan veya birden fazla ilişkiyi birleştiren işlemlerde üretilen SQL ve sorgu planı incelenmeden yalnızca repository soyutlamasına güvenilmemelidir.
| Senaryo | İlk tercih | Dikkat edilmesi gereken sınır |
|---|---|---|
| Tek entity üzerinde ISBN veya durum filtreleme | Türetilmiş sorgu metodu | Metot adı karmaşıklaşıyorsa sorgu niyeti görünmez hâle gelebilir. |
| Birden fazla entity arasında kontrollü join | @Query |
Join sonucunun entity, DTO veya projection olarak nasıl döneceği belirlenmelidir. |
| Veritabanına özgü fonksiyon veya özel SQL | Native SQL | Veritabanı bağımlılığı ve taşınabilirlik maliyeti belgelenmelidir. |
| Toplu güncelleme veya raporlama | @Query, native SQL veya ayrı erişim yaklaşımı |
Transaction sınırı, etkilenen satırlar ve entity önbelleğinin durumu kontrol edilmelidir. |
| Performans açısından kritik listeleme | Ölçüm sonucuna göre JPA veya SQL | Üretilen SQL, indeksler ve sorgu planı birlikte değerlendirilmelidir. |
@Query ne zaman daha anlaşılırdır?
Türetilmiş sorgu metotları, alan adları ve basit operatörlerle ifade edilebilen sorgularda oldukça okunaklıdır. Fakat sorguya birden fazla koşul, ilişki, özel sıralama veya yalnızca belirli alanları döndürme ihtiyacı eklendiğinde metot adı hızla büyüyebilir. Bu durumda @Query, sorgunun niyetini doğrudan repository içinde göstermenin daha iyi yolu olabilir.
Örneğin “yayıncısı belirli olan, stok miktarı sıfırdan büyük kitapları başlığa göre sırala” gibi bir sorgu, kısa bir metot adıyla ifade edilebilse bile ekipteki herkes tarafından aynı kolaylıkla okunmayabilir. Açık bir JPQL sorgusu, hangi ilişkinin kullanıldığını ve hangi alanların filtrelendiğini daha görünür hâle getirir. Burada amaç her sorguyu elle yazmak değil, isimlendirme yoluyla anlatılması zorlaşan sorguları açık bir ifade hâline getirmektir.
Toplu güncelleme sorgularında ise yalnızca veritabanındaki satırların değişmesi yeterli olmayabilir. Aynı entity’lerin mevcut persistence context içinde tutulması, daha sonra eski değerlerin okunmasına neden olabilir. Bu yüzden toplu işlem tasarımında transaction sınırı, güncelleme sonrası persistence context’in nasıl ele alınacağı ve işlem sonucunun nasıl doğrulanacağı birlikte düşünülmelidir.
Native SQL hangi durumda mantıklıdır?
Native SQL, JPA’nın sunduğu soyutlama sorguyu gereksiz biçimde karmaşıklaştırıyor veya veritabanının özgün yeteneklerinden yararlanmak gerekiyor ise anlamlıdır. Gelişmiş raporlama sorguları, veritabanına özel fonksiyonlar, özel tarih ve metin işlemleri, karmaşık toplu güncellemeler veya sorgu planı üzerinde hassas kontrol ihtiyacı bu kapsama girebilir.
Ancak native SQL kullanmak, sorgunun otomatik olarak daha hızlı olacağı anlamına gelmez. Kötü indeks seçimi, geniş bir tablo taraması veya yanlış join koşulu, SQL doğrudan yazıldığında da aynı şekilde sorun yaratır. Ayrıca native sorgular veritabanı motoruna daha fazla bağımlılık getirebilir. Taşınabilirlik önemliyse kullanılan fonksiyonların ve veri tipi davranışlarının ayrıca değerlendirilmesi gerekir.
Bir sorgu native SQL ile yazıldığında şu sorular cevaplanmalıdır:
- Sorgu hangi veritabanı özelliğine ihtiyaç duyuyor?
- Bu özellik JPQL veya Criteria tabanlı bir çözümle neden yeterince açık ifade edilemiyor?
- Dönen sonuç doğrudan entity’ye mi, projection’a mı, yoksa özel bir DTO’ya mı eşlenecek?
- Sorgunun test ortamı ile üretim ortamındaki veritabanı davranışı uyumlu mu?
- Sorgu değiştiğinde veritabanı bağımlılığı ve bakım sorumluluğu kimde olacak?
ORM kullanırken bilinmesi gereken performans konuları
ORM kullanımı SQL bilgisini ortadan kaldırmaz; yalnızca uygulama kodu ile SQL arasına bir eşleme katmanı ekler. Geliştirici, entity alanlarını ve ilişkileri tanımlarken veritabanında hangi sorguların oluşacağını zihninde canlandırabilmelidir.
- İndeksler: Sık filtrelenen veya benzersiz olması gereken alanlar için veritabanı şemasıyla uyumlu indeks tasarımı yapılmalıdır. Repository metodu doğru olsa bile uygun indeks yoksa sorgu gereksiz satırları tarayabilir.
- Join kardinalitesi: Bir kitabın birden fazla kategoriyle ilişkili olması gibi durumlarda join sonucu beklenenden fazla satır üretebilir. Bu durum hem sonuçların tekrarlanmasına hem de sayfalama davranışının karmaşıklaşmasına neden olabilir.
- Transaction sınırları: Okuma ve yazma işlemlerinin hangi transaction içinde gerçekleştiği, tutarlılık ve hata durumunda geri alma davranışı açısından açık olmalıdır.
- Lazy ve eager yükleme tercihleri: İlişkili verinin ne zaman yükleneceği kullanım senaryosuna göre belirlenmelidir. Bir liste ekranı için gerekmeyen ilişkileri otomatik biçimde yüklemek gereksiz veri erişimine yol açabilir.
- N+1 sorgu riski: Önce kitapların, ardından her kitap için ayrı kategorilerin çekilmesi gibi akışlarda tek bir listeleme işlemi çok sayıda SQL sorgusuna dönüşebilir.
- Sorgu planı: Özellikle büyük tablolar veya çoklu join içeren sorgularda yalnızca Java kodunu incelemek yeterli değildir. Gerçek SQL, parametreler ve veritabanının sorgu planı birlikte ele alınmalıdır.
Pratik bir yaklaşım olarak JPA’yı varsayılan başlangıç noktası, @Queryyi açıklık sağlayan kontrollü bir araç, native SQL’i ise gerçekten ihtiyaç duyulan veritabanı gücüne erişim yöntemi olarak konumlandırabilirsiniz. Bu üç yaklaşım arasında seçim, alışkanlığa göre değil; sorgunun okunabilirliği, ölçülen davranışı, bakım maliyeti ve veritabanı bağımlılığına göre yapılmalıdır.
Repository kodunu üretime almadan önce kontrol listesi
Bir repository’nin derlenmesi, veri erişim tasarımının tamamlandığı anlamına gelmez. Üretim öncesi kontrol, entity ile veritabanı şemasının uyumundan sorgu davranışına ve ilişki performansına kadar birkaç farklı katmanı kapsamalıdır.
- Entity alanlarını şemayla karşılaştırın. Alan adları, veri tipleri, uzunluk sınırları, otomatik üretilen kimlikler ve tablo adları veritabanındaki gerçek yapı ile uyumlu olmalıdır.
- Benzersiz alanları belirleyin. ISBN gibi alanların uygulama seviyesinde benzersiz görünmesi yeterli değildir. Aynı anda çalışan iki isteğin çakışmasını önlemek için benzersizlik kuralı veritabanı katmanında da düşünülmelidir.
- Null ve boş değer kurallarını netleştirin. Boş bir ISBN,
nullISBN ve yalnızca boşluk karakterlerinden oluşan ISBN aynı anlamı taşımayabilir. Bu ayrım entity doğrulamasında, servis katmanında ve veritabanı kısıtlarında tutarlı olmalıdır. - Transaction sınırlarını inceleyin. Kitabın stok değerini azaltan bir işlem, okuma ve yazma adımlarının hangi transaction içinde yürüdüğünü açıkça tanımlamalıdır. Eşzamanlı isteklerde kayıp güncelleme ihtimali ayrıca değerlendirilmelidir.
- Türetilmiş sorgu adlarını okuyun. Metot adı işlevi açıkça anlatmıyorsa veya koşullar arttıkça okunması zorlaşıyorsa sorguyu
@Queryile açık biçimde tanımlamayı değerlendirin. - Oluşan SQL’i inceleyin. Repository kodu yazıldıktan sonra gerçekten hangi SQL’in üretildiği, gereksiz kolon veya ilişki yüklemesi yapılıp yapılmadığı kontrol edilmelidir.
- Gerekli indeksleri doğrulayın. ISBN araması, stok durumuna göre listeleme veya yayıncı filtresi gibi sık kullanılan işlemler için sorgu koşulları ile indeks tasarımı birlikte ele alınmalıdır.
- İlişkilerde N+1 riskini test edin. Kitap listesi dönerken her kitap için ayrıca kategori, yazar veya yayınevi sorgusu çalışıp çalışmadığını gözlemleyin.
- Karmaşık sorguları sınırlandırın. Raporlama, çoklu join veya veritabanına özgü fonksiyon içeren işlemleri yalnızca uzun bir türetilmiş metot adına dönüştürmek yerine
@Queryveya ham SQL ile açıkça ayırın.
Book senaryosu: listeleme, ISBN arama ve stok güncelleme
Basit bir kitap backend’inde repository kararını şu akışla uygulayabilirsiniz:
- Kitap listeleme için sayfalama ve temel sıralama yeterliyse repository’nin standart sayfalama yetenekleriyle başlayın.
- ISBN araması tek bir entity alanına dayanıyorsa, benzersiz sonuç döndüren açık bir repository metodu kullanın.
- Stok güncelleme işleminde yalnızca yeni değeri yazmak yerine, stok azaltma kuralının transaction ve eşzamanlılık davranışını kontrol edin.
- Listeleme ekranı ilişkili alanları da çekmeye başladığında oluşan SQL sayısını inceleyin.
- Join sayısı, dönen veri miktarı veya sorgu süresi beklenenden fazla büyüyorsa sorguyu yeniden tasarlayın; gerektiğinde
@Query, projection veya native SQL kullanın.
public interface BookRepository extends JpaRepository<Book, Long> {
Page<Book> findByTitleContainingIgnoreCase(String title, Pageable pageable);
Optional<Book> findByIsbn(String isbn);
@Modifying
@Query("update Book b set b.stock = b.stock - :amount " +
"where b.id = :id and b.stock >= :amount")
int decreaseStock(Long id, int amount);
}
Bu örnekte başlığa göre listeleme, ISBN ile arama ve koşullu stok azaltma farklı risk seviyelerine sahiptir. İlk iki metot repository yaklaşımıyla kolayca ifade edilebilir. Stok azaltma ise koşullu bir güncelleme yaptığı için etkilenen satır sayısının kontrol edilmesini gerektirir. Sonuç sıfırsa kitap bulunamamış, stok yetersiz kalmış veya işlem beklenen koşulu sağlamamış olabilir; servis katmanı bu sonucu anlamlı bir uygulama davranışına dönüştürmelidir.
Ayrıca toplu güncelleme sonrasında aynı entity’nin daha önce persistence context içinde okunmuş bir kopyası varsa, bellekteki değer ile veritabanındaki değer farklılaşabilir. Bu nedenle güncelleme işleminin ardından entity durumunun nasıl yönetileceği, transaction tasarımının bir parçası olarak belirlenmelidir.
Repository tasarımını ve Java temellerini uygulamalı biçimde gözden geçirmek isteyen okuyucu, kendi seviyesini görmek için Java bilgi testi ile başlangıç ve tekrar gerektiren konularını belirleyebilir.
Son kontrol: “Çalışıyor” ile “hazır” arasındaki fark
Repository metotları test verisiyle doğru sonuç döndürüyor olabilir; fakat üretime hazır sayılmaları için sorgu davranışlarının gerçek kullanım senaryolarıyla da sınanması gerekir. Özellikle boş sonuçlar, yinelenen kayıtlar, eşzamanlı stok güncellemeleri, büyük sayfa numaraları, eksik ilişkiler ve hatalı girişler ayrı ayrı ele alınmalıdır.
- Başarılı sorgu kadar boş sonuç davranışı da tanımlı mı?
- Benzersiz olması gereken alanlarda veritabanı kısıtı bulunuyor mu?
- Sayfalı sorguda sıralama kararlı mı?
- Stok güncellemesinde negatif değere düşme engelleniyor mu?
- İlişkili entity’ler gereksiz sorgu üretmeden yükleniyor mu?
- Karmaşık sorguların neden repository metodu,
@Queryveya native SQL olarak seçildiği belgeli mi? - Üretilen SQL ve sorgu planı, kritik akışlar için incelendi mi?
Sık Sorulan Sorular
JpaRepository ile CrudRepository arasındaki temel fark nedir?
CrudRepository temel oluşturma, okuma, güncelleme ve silme işlemlerini tanımlayan daha dar bir arayüzdür. JpaRepository ise bu temel işlemlere ek olarak JPA odaklı daha geniş kullanım imkânları ve sayfalama gibi ihtiyaçlarda kullanılan yetenekler sunar. Seçim, uygulamanın gerçekten ihtiyaç duyduğu repository davranışına göre yapılmalıdır.
Türetilmiş sorgu metodu hangi noktada @Query ile değiştirilmelidir?
Metot adı birden fazla ilişkiyi, karmaşık koşulu, özel sıralamayı veya projection ihtiyacını anlatmakta zorlanmaya başladığında @Query daha okunabilir bir seçenek olabilir. Buradaki ölçüt yalnızca karakter sayısı değildir; sorgunun niyetinin ekipteki başka bir geliştirici tarafından kolayca anlaşılması ve değiştirilebilmesidir.
Spring Data JPA kullanırken JDBC veya ham SQL bilmek neden hâlâ gereklidir?
JPA repository’nin ürettiği sorguyu değerlendirmek, indeks sorunlarını anlamak, join kaynaklı veri çoğalmasını fark etmek, transaction davranışını incelemek ve N+1 sorgu riskini teşhis etmek için SQL bilgisi gerekir. JDBC veya ham SQL bilgisi ayrıca ORM soyutlamasının uygun olmadığı raporlama ve veritabanına özgü işlemlerde doğru aracı seçmeyi sağlar.
JPA repository kullanımı performans sorunlarını otomatik olarak çözer mi?
Hayır. Repository katmanı tekrar eden erişim kodunu azaltabilir; ancak performans, sorgunun yapısına, indekslere, dönen veri miktarına, ilişkilerin yüklenme biçimine, transaction tasarımına ve veritabanının sorgu planına bağlıdır. Kritik işlemlerde üretilen SQL’i ölçmeden veya incelemeden JPA kullanımını tek başına performans çözümü kabul etmek doğru değildir.
Doğru yaklaşım, JPA repository’yi anlaşılır ve sürdürülebilir veri erişimi için kullanırken gerektiğinde @Query veya ham SQL’e bilinçli biçimde geçmektir.