Derived Query Methods
Giriş
Spring Data JPA'nın en büyüleyici özelliklerinden biri, method adından otomatik SQL sorgusu üretmesidir. Hiçbir SQL veya annotation yazmadan, sadece method adı kurallarına (convention) uyarak karmaşık sorgular oluşturabilirsiniz. Bir nevi Spring'e İngilizce konuşuyorsunuz ve o bunu SQL'e çeviriyor.
Gerçek Dünya Analojisi
Bir Google araması düşünün. Arama kutusuna "İstanbul'da 2024'ten sonra açılan İtalyan restoranları" yazıyorsunuz ve Google bunu anlıyor. Spring Data JPA da benzer şekilde çalışır: findByCityAndOpenedAfterAndCuisine(...) gibi bir method adı yazıyorsunuz ve Spring bunu SELECT * FROM restaurants WHERE city = ? AND opened_at > ? AND cuisine = ? sorgusuna çeviriyor. Kural basit: doğru kelimeleri doğru sırayla kullanın.
Neden Derived Query Methods?
Sıfır SQL kodu — Hata yapma şansınız dramatik düşer
Refactoring-friendly — Entity field adı değişirse, compiler hata verir
Hızlı geliştirme — Basit sorgular saniyeler içinde hazır
Okunabilir — Method adı sorgunun ne yaptığını anlatır
Temel Yapı
Derived query method'lar şu pattern'i takip eder:
[action][Limit][By][Property][Keyword][And/Or][Property][Keyword][OrderBy][Property][Direction]action:
find,read,get,query,search,stream,count,exists,deleteLimit:
First,Top,Top5By: Property filtresinin başlangıcı
Property: Entity field adı
Keyword: Karşılaştırma operatörü (GreaterThan, Like, Between, vb.)
İlk Örnekler
public interface UserRepository extends JpaRepository<User, Long> {
// En basit: Tek alana göre sorgulama
Optional<User> findByEmail(String email);
// → SELECT * FROM users WHERE email = ?
List<User> findByName(String name);
// → SELECT * FROM users WHERE name = ?
List<User> findByRole(Role role);
// → SELECT * FROM users WHERE role = ?
List<User> findByActive(boolean active);
// → SELECT * FROM users WHERE active = ?
}💡 İpucu: findBy, readBy, getBy, queryBy, searchBy hepsi aynı şeyi yapar — kişisel tercihinize bağlı. Proje genelinde tutarlı olun. En yaygın kullanılan findBy'dır.
Karşılaştırma Keyword'leri
Eşitlik ve Eşitsizlik
// Eşitlik (= operatörü) — keyword belirtmeye gerek yok
List<User> findByName(String name);
// WHERE name = ?
// Eşitsizlik
List<User> findByNameNot(String name);
// WHERE name <> ?
// Büyük/Küçük karşılaştırma
List<User> findByAgeGreaterThan(int age); // WHERE age > ?
List<User> findByAgeGreaterThanEqual(int age); // WHERE age >= ?
List<User> findByAgeLessThan(int age); // WHERE age < ?
List<User> findByAgeLessThanEqual(int age); // WHERE age <= ?
// Aralık
List<User> findByAgeBetween(int min, int max); // WHERE age BETWEEN ? AND ?String İşlemleri
// LIKE sorguları
List<User> findByNameContaining(String keyword);
// WHERE name LIKE '%keyword%'
List<User> findByNameStartingWith(String prefix);
// WHERE name LIKE 'prefix%'
List<User> findByEmailEndingWith(String domain);
// WHERE email LIKE '%domain'
List<User> findByNameLike(String pattern);
// WHERE name LIKE ? (pattern'i kendiniz belirtirsiniz: "%Ali%")
// Case-insensitive arama
List<User> findByNameContainingIgnoreCase(String keyword);
// WHERE UPPER(name) LIKE UPPER('%keyword%')
List<User> findByEmailIgnoreCase(String email);
// WHERE UPPER(email) = UPPER(?)⚠️ Dikkat: Containing otomatik olarak %keyword% ekler ama Like eklemez. Like kullandığınızda % karakterini kendiniz eklemeniz gerekir: findByNameLike("%Ali%").
Null Kontrolleri
List<User> findByPhoneIsNull();
// WHERE phone IS NULL
List<User> findByPhoneIsNotNull();
// WHERE phone IS NOT NULL
List<User> findByDeletedAtNull(); // IsNull yazmaya gerek yok
// WHERE deleted_at IS NULLBoolean Kontrolleri
List<User> findByActiveTrue();
// WHERE active = true
List<User> findByActiveFalse();
// WHERE active = false
// Not: findByActive(true) ile findByActiveTrue() aynı sonucu verir
// Ama True/False versiyonu parametre gerektirmezKoleksiyon İşlemleri
// IN — Belirli değerler listesinde mi?
List<User> findByRoleIn(Collection<Role> roles);
// WHERE role IN ('ADMIN', 'MODERATOR')
List<User> findByRoleNotIn(List<Role> roles);
// WHERE role NOT IN ('ADMIN', 'MODERATOR')
List<User> findByIdIn(List<Long> ids);
// WHERE id IN (1, 2, 3, 5, 8)Tarih İşlemleri
List<User> findByCreatedAtAfter(LocalDateTime date);
// WHERE created_at > ?
List<User> findByCreatedAtBefore(LocalDateTime date);
// WHERE created_at < ?
List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
// WHERE created_at BETWEEN ? AND ?Çoklu Koşullar: And / Or
// AND — İki koşulun ikisi de sağlanmalı
List<User> findByNameAndEmail(String name, String email);
// WHERE name = ? AND email = ?
List<User> findByRoleAndActiveTrue(Role role);
// WHERE role = ? AND active = true
List<User> findByAgeGreaterThanAndAgeLessThan(int min, int max);
// WHERE age > ? AND age < ?
// OR — Koşullardan en az biri sağlanmalı
List<User> findByNameOrEmail(String name, String email);
// WHERE name = ? OR email = ?
// Karmaşık kombinasyonlar
List<User> findByRoleAndActiveTrueAndAgeGreaterThan(Role role, int age);
// WHERE role = ? AND active = true AND age > ?⚠️ Dikkat: And ve Or kombinasyonu karmaşıklaştığında method adı okunamaz hale gelir. 3'ten fazla koşulda @Query annotation'ına geçin:
// ❌ Okunamaz method adı
List<User> findByActiveAndRoleAndAgeGreaterThanAndCreatedAtAfterAndNameContaining(...);
// ✅ @Query ile çok daha okunabilir
@Query("""
SELECT u FROM User u
WHERE u.active = :active
AND u.role = :role
AND u.age > :minAge
AND u.createdAt > :since
AND LOWER(u.name) LIKE LOWER(CONCAT('%', :name, '%'))
""")
List<User> searchUsers(...);Sıralama: OrderBy
// Ascending (A→Z, küçük→büyük, eski→yeni)
List<User> findByRoleOrderByNameAsc(Role role);
// WHERE role = ? ORDER BY name ASC
// Descending (Z→A, büyük→küçük, yeni→eski)
List<User> findByActiveOrderByCreatedAtDesc(boolean active);
// WHERE active = ? ORDER BY created_at DESC
// Çoklu sıralama
List<User> findByActiveTrueOrderByRoleAscNameAsc();
// WHERE active = true ORDER BY role ASC, name ASCSıralama parametresi ile (daha esnek):
// Sort parametresi ile dinamik sıralama
List<User> findByRole(Role role, Sort sort);
// Kullanım:
List<User> users = userRepository.findByRole(Role.ADMIN,
Sort.by("name").ascending().and(Sort.by("age").descending()));Sonuç Sınırlama: First, Top
// İlk N sonuç
List<User> findTop5ByOrderByCreatedAtDesc();
// → En son kayıt olan 5 kullanıcı
List<User> findFirst10ByRoleOrderByNameAsc(Role role);
// → O roldeki ilk 10 kullanıcı (isme göre)
// Tek sonuç
User findFirstByOrderByAgeDesc();
// → En yaşlı kullanıcı (tek sonuç)
Optional<User> findTopByOrderByCreatedAtDesc();
// → En son kaydolan kullanıcıFarklı Dönüş Tipleri
Spring Data JPA, derived query method'larda çeşitli dönüş tipleri destekler:
public interface UserRepository extends JpaRepository<User, Long> {
// Tek sonuç
Optional<User> findByEmail(String email); // 0 veya 1 sonuç
User findByUsername(String username); // null veya 1 (null-unsafe)
// Koleksiyon
List<User> findByRole(Role role); // 0-N sonuç (List)
Collection<User> findByActive(boolean a); // Collection da olur
Set<User> findByAgeGreaterThan(int age); // Set — tekrarsız
// Stream (büyük veri setleri için)
@QueryHints(@QueryHint(name = HINT_FETCH_SIZE, value = "50"))
Stream<User> findByActiveTrue(); // Try-with-resources ile kullanın!
// Sayfalama
Page<User> findByRole(Role role, Pageable pageable);
Slice<User> findByActive(boolean active, Pageable pageable);
// Async
@Async
CompletableFuture<List<User>> findByName(String name);
}⚠️ Dikkat: Stream dönen method'ları mutlaka try-with-resources ile kullanın ve @Transactional içinde çağırın:
@Transactional(readOnly = true)
public void processActiveUsers() {
try (Stream<User> stream = userRepository.findByActiveTrue()) {
stream.filter(u -> u.getAge() > 18)
.forEach(this::processUser);
}
}count, exists, delete Varyantları
Derived query sadece find ile sınırlı değil:
public interface UserRepository extends JpaRepository<User, Long> {
// === Sayma (COUNT) ===
long countByRole(Role role);
// SELECT COUNT(*) FROM users WHERE role = ?
long countByActiveTrue();
// SELECT COUNT(*) FROM users WHERE active = true
long countByAgeGreaterThan(int age);
// SELECT COUNT(*) FROM users WHERE age > ?
// === Varlık Kontrolü (EXISTS) ===
boolean existsByEmail(String email);
// SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END FROM users WHERE email = ?
boolean existsByNameAndRole(String name, Role role);
// === Silme (DELETE) ===
@Transactional
void deleteByEmail(String email);
// SELECT + DELETE (entity yüklenir, lifecycle callback çalışır)
@Transactional
long deleteByActiveFalse();
// Silinen kayıt sayısını döner
}💡 İpucu: existsBy... metotları countBy... > 0 yazmaktan çok daha performanslıdır. Bazı veritabanları EXISTS sorgusunu ilk eşleşmede durdurur, COUNT ise tüm tabloyu tarar.
Keyword Referans Tablosu
| Keyword | Örnek | JPQL Karşılığı |
|---|---|---|
And | findByNameAndEmail | WHERE x.name = ?1 AND x.email = ?2 |
Or | findByNameOrEmail | WHERE x.name = ?1 OR x.email = ?2 |
Is, Equals | findByName / findByNameIs | WHERE x.name = ?1 |
Not | findByNameNot | WHERE x.name <> ?1 |
Between | findByAgeBetween | WHERE x.age BETWEEN ?1 AND ?2 |
LessThan | findByAgeLessThan | WHERE x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | WHERE x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | WHERE x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | WHERE x.age >= ?1 |
After | findByCreatedAtAfter | WHERE x.createdAt > ?1 |
Before | findByCreatedAtBefore | WHERE x.createdAt < ?1 |
Like | findByNameLike | WHERE x.name LIKE ?1 |
NotLike | findByNameNotLike | WHERE x.name NOT LIKE ?1 |
Containing | findByNameContaining | WHERE x.name LIKE %?1% |
StartingWith | findByNameStartingWith | WHERE x.name LIKE ?1% |
EndingWith | findByNameEndingWith | WHERE x.name LIKE %?1 |
IgnoreCase | findByNameIgnoreCase | WHERE UPPER(x.name) = UPPER(?1) |
OrderBy | findByRoleOrderByNameAsc | ... ORDER BY x.name ASC |
Not | findByNameNot | WHERE x.name <> ?1 |
In | findByRoleIn | WHERE x.role IN ?1 |
NotIn | findByRoleNotIn | WHERE x.role NOT IN ?1 |
True | findByActiveTrue | WHERE x.active = true |
False | findByActiveFalse | WHERE x.active = false |
IsNull | findByPhoneIsNull | WHERE x.phone IS NULL |
IsNotNull | findByPhoneIsNotNull | WHERE x.phone IS NOT NULL |
First/Top | findFirst5By... | ... LIMIT 5 |
Distinct | findDistinctByRole | SELECT DISTINCT ... |
Nested Property ile Sorgulama
İlişkili entity'lerin alanlarına da sorgu yazabilirsiniz:
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private User user;
private BigDecimal totalAmount;
}
public interface OrderRepository extends JpaRepository<Order, Long> {
// user.email alanına göre sorgulama (nested property)
List<Order> findByUserEmail(String email);
// → SELECT o FROM Order o WHERE o.user.email = ?
// user.name alanında arama
List<Order> findByUserNameContaining(String keyword);
// → SELECT o FROM Order o WHERE o.user.name LIKE '%keyword%'
// Çoklu nested
List<Order> findByUserRoleAndTotalAmountGreaterThan(Role role, BigDecimal amount);
}⚠️ Dikkat: Nested property'lerde belirsizlik olabilir. Eğer Order entity'sinde hem userEmail field'ı hem de user.email ilişkisi varsa, Spring hangisini kullanacağını bilemez. Böyle durumlarda _ ile ayırın: findByUser_Email.
Yaygın Hatalar ve Çözümleri
Hata 1: Property Adı Yanlış Yazmak
// ❌ Entity'de "name" var ama "username" yazdınız
List<User> findByUsername(String name);
// PropertyReferenceException: No property 'username' found for type 'User'
// ✅ Entity field adını doğru yazın
List<User> findByName(String name);Hata 2: Dönüş Tipi Uyumsuzluğu
// ❌ Birden fazla sonuç dönebilecek sorgu için Optional
Optional<User> findByRole(Role role);
// Birden fazla ADMIN varsa → NonUniqueResultException!
// ✅ Koleksiyon döndürün
List<User> findByRole(Role role);
// ✅ Tek sonuç garantisiyse Optional
Optional<User> findByEmail(String email); // email unique ise güvenliHata 3: delete Metodunda @Transactional Unutmak
// ❌ @Transactional yok
void deleteByEmail(String email);
// TransactionRequiredException!
// ✅ @Transactional ekleyin (service veya repository seviyesinde)
@Transactional
void deleteByEmail(String email);Hata 4: Method Adı Çok Uzun
// ❌ Okunamaz hale gelmiş
List<User> findByActiveTrueAndRoleNotAndAgeGreaterThanEqualAndCreatedAtAfterAndNameContainingIgnoreCaseOrderByNameAsc(
Role role, int age, LocalDateTime date, String name);
// ✅ @Query kullanın — okunabilirlik her şeyden önemli
@Query("""
SELECT u FROM User u
WHERE u.active = true AND u.role <> :role
AND u.age >= :minAge AND u.createdAt > :since
AND LOWER(u.name) LIKE LOWER(CONCAT('%', :name, '%'))
ORDER BY u.name ASC
""")
List<User> searchActiveUsers(@Param("role") Role excludeRole,
@Param("minAge") int minAge,
@Param("since") LocalDateTime since,
@Param("name") String name);Özet
Spring Data JPA, method adı convention'ından otomatik SQL üretir — sıfır SQL yazma
findBy,countBy,existsBy,deleteByile başlayan method adları + property adı + keyword kombinasyonuIgnoreCase ile case-insensitive, OrderBy ile sıralı, Top/First ile limitli sorgular yazılır
3'ten fazla koşulda method adı okunamaz hale gelir —
@Query'ye geçinNested property ile ilişkili entity alanlarına sorgu yazılabilir (
findByUserEmail)existsBy...performans açısındancountBy... > 0'dan daha iyidirMethod adındaki property adı hatalıysa uygulama başlamaz — fail-fast davranış
Bütünleşik Gerçek Dünya Örneği: E-Ticaret Ürün Arama
Tüm derived query özelliklerini birleştiren kapsamlı bir repository:
@Entity
@Table(name = "products")
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String brand;
private String category;
private BigDecimal price;
private int stockQuantity;
private boolean active;
private double rating;
private LocalDateTime createdAt;
@ManyToOne
private Seller seller;
}
public interface ProductRepository extends JpaRepository<Product, Long> {
// === Temel Sorgular ===
Optional<Product> findByNameIgnoreCase(String name);
List<Product> findByCategory(String category);
List<Product> findByBrand(String brand);
// === Fiyat Filtreleri ===
List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
List<Product> findByPriceLessThanEqual(BigDecimal maxPrice);
List<Product> findByPriceGreaterThanOrderByPriceAsc(BigDecimal minPrice);
// === Stok Durumu ===
List<Product> findByStockQuantityGreaterThan(int minStock);
List<Product> findByStockQuantityEquals(int quantity); // Tam stok = 0 → tükenen ürünler
List<Product> findByActiveTrueAndStockQuantityGreaterThan(int minStock);
// === Arama ===
List<Product> findByNameContainingIgnoreCaseOrBrandContainingIgnoreCase(
String nameKeyword, String brandKeyword);
// === Sıralama ve Limit ===
List<Product> findTop10ByActiveTrueOrderByRatingDesc(); // En iyi 10 ürün
List<Product> findTop5ByCategoryOrderByCreatedAtDesc(String category); // Kategorideki en yeni 5
// === İstatistikler ===
long countByCategory(String category);
long countByActiveTrueAndPriceLessThan(BigDecimal price);
boolean existsByNameIgnoreCase(String name);
// === Satıcı bazlı (nested property) ===
List<Product> findBySellerNameContaining(String sellerName);
long countBySellerIdAndActiveTrue(Long sellerId);
// === Sayfalama ===
Page<Product> findByCategoryAndActiveTrue(String category, Pageable pageable);
Slice<Product> findByPriceBetweenAndActiveTrue(
BigDecimal min, BigDecimal max, Pageable pageable);
}// Service'te kullanım örnekleri
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProductSearchService {
private final ProductRepository productRepository;
// Kategorideki uygun fiyatlı ürünleri getir
public Page<Product> getAffordableProducts(String category, BigDecimal maxPrice, Pageable pageable) {
return productRepository.findByCategoryAndActiveTrue(category, pageable);
}
// En popüler ürünler
public List<Product> getTopRatedProducts() {
return productRepository.findTop10ByActiveTrueOrderByRatingDesc();
}
// Stokta kalan ürün sayısı
public long getInStockCount(String category) {
return productRepository.countByCategory(category);
}
// Ürün adı benzersiz mi kontrol et
public boolean isProductNameAvailable(String name) {
return !productRepository.existsByNameIgnoreCase(name);
}
}Bu örnekte 20'den fazla derived query method tanımladık — hiçbirinde SQL yazmadık, hiçbirinde @Query kullanmadık. Spring Data JPA tüm SQL'leri otomatik üretti.
Ne Zaman Derived Query, Ne Zaman @Query?
| Durum | Tercih | Neden |
|---|---|---|
| 1-2 koşul, basit eşitlik | ✅ Derived query | Hızlı, okunabilir |
| 3+ koşul | ⚠️ @Query JPQL | Method adı çok uzar |
| JOIN gerektiren sorgu | ✅ @Query JPQL | Derived query JOIN yapmaz |
| Aggregate (SUM, AVG) | ✅ @Query JPQL | Derived query desteklemez |
| Veritabanına özel fonksiyon | ✅ @Query Native | JPQL'de yok |
| Dinamik koşullar | ✅ Specification/Criteria | Derived query statiktir |
AI Asistan
Sorularını yanıtlamaya hazır