← Kursa Dön
📄 Text · 18 min

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, delete

  • Limit: First, Top, Top5

  • By: 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 NULL

Boolean 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 gerektirmez

Koleksiyon İş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 ASC

Sı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ÖrnekJPQL Karşılığı
AndfindByNameAndEmailWHERE x.name = ?1 AND x.email = ?2
OrfindByNameOrEmailWHERE x.name = ?1 OR x.email = ?2
Is, EqualsfindByName / findByNameIsWHERE x.name = ?1
NotfindByNameNotWHERE x.name <> ?1
BetweenfindByAgeBetweenWHERE x.age BETWEEN ?1 AND ?2
LessThanfindByAgeLessThanWHERE x.age < ?1
LessThanEqualfindByAgeLessThanEqualWHERE x.age <= ?1
GreaterThanfindByAgeGreaterThanWHERE x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqualWHERE x.age >= ?1
AfterfindByCreatedAtAfterWHERE x.createdAt > ?1
BeforefindByCreatedAtBeforeWHERE x.createdAt < ?1
LikefindByNameLikeWHERE x.name LIKE ?1
NotLikefindByNameNotLikeWHERE x.name NOT LIKE ?1
ContainingfindByNameContainingWHERE x.name LIKE %?1%
StartingWithfindByNameStartingWithWHERE x.name LIKE ?1%
EndingWithfindByNameEndingWithWHERE x.name LIKE %?1
IgnoreCasefindByNameIgnoreCaseWHERE UPPER(x.name) = UPPER(?1)
OrderByfindByRoleOrderByNameAsc... ORDER BY x.name ASC
NotfindByNameNotWHERE x.name <> ?1
InfindByRoleInWHERE x.role IN ?1
NotInfindByRoleNotInWHERE x.role NOT IN ?1
TruefindByActiveTrueWHERE x.active = true
FalsefindByActiveFalseWHERE x.active = false
IsNullfindByPhoneIsNullWHERE x.phone IS NULL
IsNotNullfindByPhoneIsNotNullWHERE x.phone IS NOT NULL
First/TopfindFirst5By...... LIMIT 5
DistinctfindDistinctByRoleSELECT 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üvenli

Hata 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, deleteBy ile başlayan method adları + property adı + keyword kombinasyonu

  • IgnoreCase 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çin

  • Nested property ile ilişkili entity alanlarına sorgu yazılabilir (findByUserEmail)

  • existsBy... performans açısından countBy... > 0'dan daha iyidir

  • Method 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?

DurumTercihNeden
1-2 koşul, basit eşitlik✅ Derived queryHızlı, okunabilir
3+ koşul⚠️ @Query JPQLMethod adı çok uzar
JOIN gerektiren sorgu✅ @Query JPQLDerived query JOIN yapmaz
Aggregate (SUM, AVG)✅ @Query JPQLDerived query desteklemez
Veritabanına özel fonksiyon✅ @Query NativeJPQL'de yok
Dinamik koşullar✅ Specification/CriteriaDerived query statiktir