@Query ile JPQL ve Native SQL
Giriş
Spring Data JPA'nın derived query method'ları (findByNameAndAgeGreaterThan gibi) basit sorgular için harikadır. Ama gerçek dünyada sorgular basit kalmaz. JOIN'ler, alt sorgular, aggregate fonksiyonlar, veritabanına özel optimizasyonlar... Bir noktada method adı o kadar uzar ki okunmaz hale gelir:
// Bu method adı gerçekten okunabilir mi?
List<User> findByActiveAndRoleAndAgeGreaterThanEqualAndCreatedAtAfterOrderByNameAsc(
boolean active, Role role, int age, LocalDateTime date);İşte @Query annotation'ı burada devreye girer. JPQL (Java Persistence Query Language) veya Native SQL yazarak istediğiniz karmaşıklıkta sorgular oluşturabilirsiniz.
Gerçek Dünya Analojisi
Bir restoranda düşünün. Menüdeki yemekleri sipariş etmek kolaydır (derived query methods = menü). Ama "Şef, bana az pişmiş biftek ama yanında patates yerine pilav, sos olarak da özel karışımınızdan" demek istiyorsanız (karmaşık sorgu), menü yetmez — şefle doğrudan konuşmanız gerekir. @Query, şefle doğrudan konuşmanızı sağlayan kanaldır.
Ne Zaman @Query Kullanmalı?
| Senaryo | Tercih |
|---|---|
| Basit WHERE koşulları | Derived query methods |
| JOIN gerektiren sorgular | @Query JPQL |
| Aggregate (SUM, AVG, COUNT) | @Query JPQL |
| Veritabanına özel fonksiyonlar | @Query Native SQL |
| Full-text search | @Query Native SQL |
| Bulk UPDATE/DELETE | @Query + @Modifying |
JPQL — Java Persistence Query Language
JPQL, SQL'e çok benzer ama kritik bir fark vardır: tablo ve sütun yerine entity ve field adları kullanılır. Yani users tablosu değil User entity'si, user_name sütunu değil name field'ı.
JPQL vs SQL Karşılaştırma
-- SQL (tablo ve sütun adları)
SELECT u.user_name, u.email_address
FROM users u
WHERE u.is_active = true AND u.user_age > 25;
-- JPQL (entity ve field adları)
SELECT u.name, u.email
FROM User u
WHERE u.active = true AND u.age > 25;Temel JPQL Sorguları
public interface UserRepository extends JpaRepository<User, Long> {
// Named parameter — :paramName şeklinde
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmailAddress(@Param("email") String email);
// Positional parameter — ?1, ?2 şeklinde (sıra numarası)
@Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age > ?2")
List<User> findByNameAndMinAge(String name, int minAge);
// LIKE sorgusu — case-insensitive arama
@Query("SELECT u FROM User u WHERE LOWER(u.name) LIKE LOWER(CONCAT('%', :keyword, '%'))")
List<User> searchByName(@Param("keyword") String keyword);
// Birden fazla koşul
@Query("""
SELECT u FROM User u
WHERE u.active = true
AND u.role = :role
AND u.age BETWEEN :minAge AND :maxAge
ORDER BY u.name ASC
""")
List<User> findActiveUsersByRoleAndAge(
@Param("role") Role role,
@Param("minAge") int minAge,
@Param("maxAge") int maxAge
);
// IN sorgusu
@Query("SELECT u FROM User u WHERE u.role IN :roles AND u.active = true")
List<User> findActiveUsersByRoles(@Param("roles") Collection<Role> roles);
// IS NULL / IS NOT NULL
@Query("SELECT u FROM User u WHERE u.deletedAt IS NULL")
List<User> findNonDeletedUsers();
}💡 İpucu: Named parameter (:email) tercih edin. Positional parameter (?1) metot parametrelerinin sırasına bağlıdır — parametre sırası değişirse sorgu bozulur. Named parameter ise parametre adına bağlıdır, sıra önemli değildir.
JOIN Sorguları
public interface UserRepository extends JpaRepository<User, Long> {
// INNER JOIN — Siparişi olan kullanıcılar
@Query("SELECT u FROM User u JOIN u.orders o WHERE o.totalAmount > :amount")
List<User> findUsersWithLargeOrders(@Param("amount") BigDecimal amount);
// LEFT JOIN — Tüm kullanıcılar (siparişi olmayanlar dahil)
@Query("SELECT u FROM User u LEFT JOIN u.orders o WHERE o IS NULL")
List<User> findUsersWithNoOrders();
// JOIN FETCH — N+1 problemini çözer (eager loading)
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);
// Çoklu JOIN FETCH
@Query("""
SELECT DISTINCT u FROM User u
JOIN FETCH u.orders o
JOIN FETCH o.orderItems
WHERE u.active = true
""")
List<User> findActiveUsersWithOrderDetails();
}⚠️ Dikkat: JOIN FETCH ile birden fazla collection'ı aynı anda fetch etmeyin! Hibernate MultipleBagFetchException fırlatır. Çözüm: birini Set olarak tanımlayın veya ayrı sorgularda fetch edin.
Aggregate Fonksiyonlar
public interface OrderRepository extends JpaRepository<Order, Long> {
// COUNT
@Query("SELECT COUNT(o) FROM Order o WHERE o.status = :status")
long countByStatus(@Param("status") OrderStatus status);
// SUM
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.user.id = :userId")
BigDecimal calculateTotalSpentByUser(@Param("userId") Long userId);
// AVG
@Query("SELECT AVG(o.totalAmount) FROM Order o WHERE o.createdAt > :since")
Double calculateAverageOrderAmount(@Param("since") LocalDateTime since);
// MIN, MAX
@Query("SELECT MIN(o.totalAmount), MAX(o.totalAmount) FROM Order o")
Object[] findOrderAmountRange();
// GROUP BY
@Query("SELECT o.status, COUNT(o) FROM Order o GROUP BY o.status")
List<Object[]> countOrdersByStatus();
// GROUP BY + HAVING
@Query("""
SELECT u.name, COUNT(o), SUM(o.totalAmount)
FROM User u JOIN u.orders o
GROUP BY u.name
HAVING COUNT(o) >= :minOrders
ORDER BY SUM(o.totalAmount) DESC
""")
List<Object[]> findTopCustomers(@Param("minOrders") long minOrders);
}DTO Projection — Temiz Veri Dönüşümü
Object[] döndürmek yerine doğrudan DTO'ya map edebilirsiniz:
// DTO sınıfı — record veya class olabilir
public record UserSummary(String name, String email, Role role) {}
public interface UserRepository extends JpaRepository<User, Long> {
// Constructor expression ile DTO dönüşümü
@Query("""
SELECT new com.example.dto.UserSummary(u.name, u.email, u.role)
FROM User u
WHERE u.active = true
""")
List<UserSummary> findActiveUserSummaries();
}⚠️ Dikkat: new ifadesinde tam paket adı (fully qualified name) kullanmanız zorunludur. new UserSummary(...) yazmak yetmez, new com.example.dto.UserSummary(...) olmalıdır.
Interface-based projection alternatifi (daha pratik):
// Interface projection — getter metotları tanımlayın
public interface UserSummaryView {
String getName();
String getEmail();
Role getRole();
}
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.active = true")
List<UserSummaryView> findActiveUserProjections();
}Native SQL — Veritabanına Özel Sorgular
JPQL yetmediğinde Native SQL kullanırsınız. nativeQuery = true parametresi ile aktifleşir:
public interface UserRepository extends JpaRepository<User, Long> {
// Temel native query
@Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
Optional<User> findByEmailNative(@Param("email") String email);
// MySQL full-text search
@Query(value = """
SELECT * FROM users
WHERE MATCH(name, bio) AGAINST(:term IN BOOLEAN MODE)
""", nativeQuery = true)
List<User> fullTextSearch(@Param("term") String term);
// PostgreSQL JSON sorgusu
@Query(value = """
SELECT * FROM users
WHERE preferences->>'theme' = :theme
""", nativeQuery = true)
List<User> findByPreferenceTheme(@Param("theme") String theme);
// Window functions
@Query(value = """
SELECT u.*, RANK() OVER (ORDER BY u.score DESC) as user_rank
FROM users u
WHERE u.active = true
""", nativeQuery = true)
List<Object[]> getUsersWithRank();
// CTE (Common Table Expression) — PostgreSQL
@Query(value = """
WITH active_users AS (
SELECT * FROM users WHERE active = true
)
SELECT au.*, COUNT(o.id) as order_count
FROM active_users au
LEFT JOIN orders o ON o.user_id = au.id
GROUP BY au.id
ORDER BY order_count DESC
LIMIT :limit
""", nativeQuery = true)
List<Object[]> findTopActiveUsers(@Param("limit") int limit);
}Native Query ile Pagination
Native SQL'de Page dönerken countQuery belirtmeniz zorunludur:
@Query(
value = "SELECT * FROM users WHERE role = :role ORDER BY created_at DESC",
countQuery = "SELECT COUNT(*) FROM users WHERE role = :role",
nativeQuery = true
)
Page<User> findByRoleNative(@Param("role") String role, Pageable pageable);JPQL'de Spring count sorgusunu otomatik türetir, ama native SQL'de veritabanına özel olduğu için türetemez.
JPQL vs Native SQL Karşılaştırma
| Özellik | JPQL | Native SQL |
|---|---|---|
| Entity/field adları | ✅ Java field adları | ❌ Tablo/sütun adları |
| Veritabanı bağımsız | ✅ Taşınabilir | ❌ Veritabanına özel |
| Otomatik count query | ✅ Var | ❌ Manuel belirtmeli |
| Veritabanı fonksiyonları | ⚠️ Sınırlı | ✅ Tam destek |
| Window functions | ❌ Yok | ✅ Var |
| Full-text search | ❌ Yok | ✅ Var |
| JSON/Array sorgulama | ❌ Yok | ✅ Var |
💡 İpucu: Önce JPQL ile yazın. JPQL yetersiz kalırsa native SQL'e geçin. Native SQL kullanıyorsanız, veritabanı değişikliğinde bu sorguları güncellemeniz gerekecektir.
@Modifying — UPDATE ve DELETE İşlemleri
Normal @Query sadece SELECT yapar. UPDATE veya DELETE için @Modifying eklenmelidir:
public interface UserRepository extends JpaRepository<User, Long> {
// Bulk UPDATE — tüm inaktif kullanıcıları deaktif et
@Modifying
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
// Bulk DELETE
@Modifying
@Transactional
@Query("DELETE FROM User u WHERE u.active = false AND u.createdAt < :date")
int deleteOldInactiveUsers(@Param("date") LocalDateTime date);
// Spesifik alan güncelleme
@Modifying
@Transactional
@Query("UPDATE User u SET u.role = :newRole WHERE u.id = :userId")
int updateUserRole(@Param("userId") Long userId, @Param("newRole") Role newRole);
// Toplu e-posta değişikliği
@Modifying
@Transactional
@Query("UPDATE User u SET u.email = CONCAT(u.email, '.old') WHERE u.active = false")
int archiveInactiveUserEmails();
}@Modifying Kuralları
`@Transactional` zorunludur — Yoksa
TransactionRequiredExceptionalırsınızDönüş tipi:
int(etkilenen satır sayısı) veyavoidclearAutomatically: Persistence context'i temizler
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivateUser(@Param("id") Long id);clearAutomatically Neden Önemli?
@Transactional
public void deactivateAndCheck(Long id) {
// 1. Kullanıcıyı çek → persistence context'e girer
User user = userRepository.findById(id).orElseThrow();
System.out.println(user.isActive()); // true
// 2. Bulk update ile deaktif et
userRepository.deactivateUser(id);
// Veritabanında active = false oldu AMA...
// 3. Tekrar çek — persistence context'ten gelir (STALE DATA!)
User sameUser = userRepository.findById(id).orElseThrow();
System.out.println(sameUser.isActive()); // true!!! (eski veri!)
// clearAutomatically = true ile persistence context temizlenir
// ve 3. adımda veritabanından taze veri çekilir
}⚠️ Dikkat: @Modifying sorguları persistence context'i bypass eder (doğrudan SQL çalıştırır). Bu yüzden clearAutomatically = true kullanmak en güvenli yaklaşımdır.
SpEL (Spring Expression Language) ile @Query
public interface BaseRepository<T> extends JpaRepository<T, Long> {
// #{#entityName} → entity adını dinamik olarak alır
@Query("SELECT e FROM #{#entityName} e WHERE e.active = true")
List<T> findAllActive();
@Modifying
@Transactional
@Query("UPDATE #{#entityName} e SET e.active = false WHERE e.id = :id")
int softDelete(@Param("id") Long id);
}
// UserRepository artık findAllActive() ve softDelete() metotlarına sahip
public interface UserRepository extends BaseRepository<User> { }
// ProductRepository da aynı metotlara sahip
public interface ProductRepository extends BaseRepository<Product> { }Bu pattern, ortak sorguları tekrar yazmadan tüm repository'lerde kullanmanızı sağlar.
Bütünleşik Gerçek Dünya Örneği: Raporlama Servisi
// ===== Repository =====
public interface OrderRepository extends JpaRepository<Order, Long> {
// Aylık gelir raporu
@Query("""
SELECT new com.example.dto.MonthlyRevenue(
YEAR(o.createdAt), MONTH(o.createdAt), SUM(o.totalAmount), COUNT(o)
)
FROM Order o
WHERE o.status = 'COMPLETED'
AND o.createdAt BETWEEN :startDate AND :endDate
GROUP BY YEAR(o.createdAt), MONTH(o.createdAt)
ORDER BY YEAR(o.createdAt) DESC, MONTH(o.createdAt) DESC
""")
List<MonthlyRevenue> getMonthlyRevenue(
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate
);
// En çok satan ürünler (Native SQL — window function)
@Query(value = """
SELECT p.name, SUM(oi.quantity) as total_sold,
SUM(oi.quantity * oi.unit_price) as total_revenue,
RANK() OVER (ORDER BY SUM(oi.quantity) DESC) as sales_rank
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN orders o ON o.id = oi.order_id
WHERE o.status = 'COMPLETED'
AND o.created_at >= :since
GROUP BY p.id, p.name
ORDER BY total_sold DESC
LIMIT :limit
""", nativeQuery = true)
List<Object[]> findTopSellingProducts(
@Param("since") LocalDateTime since,
@Param("limit") int limit
);
// İptal edilmiş siparişleri temizle
@Modifying
@Transactional
@Query("""
DELETE FROM Order o
WHERE o.status = 'CANCELLED'
AND o.createdAt < :cutoffDate
""")
int cleanupCancelledOrders(@Param("cutoffDate") LocalDateTime cutoffDate);
}
// ===== DTO =====
public record MonthlyRevenue(int year, int month, BigDecimal totalRevenue, long orderCount) {}
// ===== Service =====
@Service
@RequiredArgsConstructor
public class ReportService {
private final OrderRepository orderRepository;
public List<MonthlyRevenue> getYearlyReport(int year) {
LocalDateTime start = LocalDateTime.of(year, 1, 1, 0, 0);
LocalDateTime end = LocalDateTime.of(year, 12, 31, 23, 59, 59);
return orderRepository.getMonthlyRevenue(start, end);
}
@Transactional
public int cleanupOldCancelledOrders(int daysOld) {
LocalDateTime cutoff = LocalDateTime.now().minusDays(daysOld);
return orderRepository.cleanupCancelledOrders(cutoff);
}
}Yaygın Hatalar ve Çözümleri
Hata 1: JPQL'de Tablo Adı Kullanmak
// ❌ YANLIŞ — "users" tablo adı, JPQL entity adı kullanır
@Query("SELECT u FROM users u WHERE u.email = :email")
// ✅ DOĞRU — "User" entity sınıfının adı
@Query("SELECT u FROM User u WHERE u.email = :email")Hata 2: @Modifying Olmadan UPDATE/DELETE
// ❌ YANLIŞ — @Modifying yok
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivate(@Param("id") Long id);
// Hata: "Not supported for DML operations"
// ✅ DOĞRU
@Modifying
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
int deactivate(@Param("id") Long id);Hata 3: @Transactional Unutmak
// ❌ @Transactional yok
@Modifying
@Query("DELETE FROM User u WHERE u.active = false")
int deleteInactive();
// TransactionRequiredException!
// ✅ DOĞRU — Service veya Repository seviyesinde @Transactional
@Modifying
@Transactional
@Query("DELETE FROM User u WHERE u.active = false")
int deleteInactive();Hata 4: DTO Constructor Projection'da Paket Adı Unutmak
// ❌ YANLIŞ — Sadece sınıf adı
@Query("SELECT new UserSummary(u.name, u.email) FROM User u")
// ✅ DOĞRU — Tam paket adı
@Query("SELECT new com.example.dto.UserSummary(u.name, u.email) FROM User u")Hata 5: Native Query'de Entity Field Adı Kullanmak
// ❌ YANLIŞ — "createdAt" Java field adı, native SQL'de sütun adı kullanılır
@Query(value = "SELECT * FROM users WHERE createdAt > :date", nativeQuery = true)
// SQL hatası: Unknown column 'createdAt'
// ✅ DOĞRU — Gerçek sütun adı
@Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true)Özet
Derived methods basit sorgular için, @Query JPQL karmaşık sorgular için, nativeQuery=true veritabanına özel sorgular için kullanın
JPQL entity ve field adları, native SQL tablo ve sütun adları kullanır
Named parameter (
:email) tercih edin, positional parameter (?1) sıraya bağımlıdırUPDATE/DELETE için @Modifying + @Transactional zorunludur — dönüş tipi
intveyavoidDTO projection ile sadece ihtiyaç duyduğunuz alanları çekin — tam entity çekmekten daha performanslı
Native SQL ile Page dönerken countQuery belirtmeyi unutmayın
clearAutomatically = true ile persistence context tutarsızlığını önleyin
Performans İpuçları
SELECT * yerine sadece gerekli alanları çekin — DTO projection ile ağ trafiğini ve bellek kullanımını azaltın
JOIN FETCH kullanarak N+1 problemini çözün — İlişkili verileri tek sorguda çekin
Native SQL'i son çare olarak kullanın — JPQL yeterliyse native'e geçmeyin, taşınabilirliğinizi koruyun
Bulk UPDATE/DELETE'lerde clearAutomatically = true kullanın — Stale data'dan kaçının
@Param ile named parameter kullanın — Hem okunabilir hem refactor-safe
Karmaşık sorguları test edin — @DataJpaTest ile repository metodlarınızı mutlaka birim testi yazın
AI Asistan
Sorularını yanıtlamaya hazır