Tabel orders sudah dibuat lewat Flyway di bab lalu, dan JpaRepository sudah memberi kita CRUD dasar gratis sejak bab 14. Tapi kebutuhan query di dunia nyata jarang sesederhana "ambil semua" atau "ambil berdasarkan id" — kita perlu cari berdasarkan nama customer, filter berdasarkan status, atau ambil ringkasan tanpa memuat seluruh data. OrderRepository menunjukkan tiga gaya berbeda untuk kebutuhan ini, masing-masing dengan alasan pemakaiannya sendiri.
Gaya 1 — Derived query method
// Derived query method: Spring Data menyusun query dari nama method-nya sendiri.
fun findByCustomerNameContainingIgnoreCase(customerName: String): List<Order>
Tidak ada implementasi, tidak ada anotasi @Query — hanya deklarasi method. Spring Data JPA membaca nama method itu sendiri sebagai spesifikasi query, lalu menyusun JPQL yang sesuai secara otomatis saat aplikasi start:
findBy...— awalan yang memberitahu Spring Data ini adalah query pencarian.CustomerName— nama field di entityOrderyang jadi target pencarian.Containing— diterjemahkan jadiLIKE '%...%', alias pencarian substring.IgnoreCase— pencarian tidak sensitif huruf besar/kecil.
Jadi method ini setara dengan query SELECT * FROM orders WHERE LOWER(customer_name) LIKE LOWER('%...%'), tapi kita tidak menulis SQL atau JPQL apa pun — cukup nama method yang deskriptif. Ini dipakai OrderService.searchByCustomerName, dan juga jadi alat verifikasi utama di test integration bab 17 nanti (mengecek order milik seorang customer benar-benar tersimpan atau tidak).
Pendekatan ini nyaman untuk kondisi sederhana, tapi ada batasnya: begitu kondisinya makin banyak atau makin kompleks (gabungan beberapa field, kondisi OR/AND bertingkat, subquery), nama method jadi sangat panjang dan sulit dibaca. Di titik itulah kita beralih ke gaya kedua.
Gaya 2 — JPQL manual dengan @Query
// JPQL manual lewat @Query - dipakai saat nama method saja tidak cukup ekspresif.
@Query("select o from Order o where o.status = :status")
fun findAllByStatus(@Param("status") status: OrderStatus): List<Order>
JPQL (Jakarta Persistence Query Language) mirip SQL, tapi beroperasi terhadap entity dan propertinya (Order, o.status), bukan langsung terhadap tabel dan kolom (orders, status). @Param("status") menghubungkan parameter method status dengan placeholder :status di query string.
Sebenarnya query ini masih cukup sederhana untuk ditulis sebagai derived method (findAllByStatus(status: OrderStatus) saja sudah otomatis diterjemahkan Spring Data tanpa @Query) — tapi ditulis manual di sini untuk menunjukkan polanya. Dalam praktiknya, kamu akan beralih ke @Query begitu:
- Kondisinya melibatkan join, subquery, atau agregasi (
GROUP BY,HAVING) yang tidak bisa direpresentasikan lewat penamaan method. - Nama method hasil derived query jadi terlalu panjang untuk dibaca dengan nyaman.
- Kamu butuh kontrol penuh atas bentuk query, misalnya urutan kondisi atau query native SQL (
@Query(nativeQuery = true)).
Gaya 3 — Constructor expression, memproyeksikan langsung ke DTO
Ini gaya paling menarik di file ini:
// Constructor expression: JPQL langsung memetakan hasil query ke DTO (OrderSummary),
// bukan ke entity Order - lebih hemat karena kolom yang diambil cuma yang dibutuhkan.
@Query(
"""
select new com.indokoding.orderservice.OrderSummary(o.id, o.customerName, o.totalPrice, o.status)
from Order o
where o.totalPrice > :minTotal
order by o.totalPrice desc
"""
)
fun findSummariesWithTotalPriceAbove(@Param("minTotal") minTotal: BigDecimal): List<OrderSummary>
Perhatikan klausa select new com.indokoding.orderservice.OrderSummary(...) — ini disebut constructor expression. Alih-alih hasil query dipetakan ke entity Order penuh (semua kolom, semua field), JPQL memanggil constructor OrderSummary langsung dengan kolom-kolom yang disebutkan: id, customerName, totalPrice, status. OrderSummary sendiri adalah data class biasa, bukan entity:
// Projection: hasil query JPQL constructor expression (lihat OrderRepository),
// bukan entity - dipakai saat kita cuma butuh sebagian kolom, bukan seluruh Order.
data class OrderSummary(
val id: Long,
val customerName: String,
val totalPrice: BigDecimal,
val status: OrderStatus
)
Kenapa proyeksi ini lebih efisien?
Kalau kita memakai query biasa yang mengembalikan List<Order>, Hibernate akan mengambil semua kolom tabel orders (termasuk itemName, quantity, unitPrice yang di kasus ini tidak dibutuhkan) dan menghidrasi setiap baris jadi objek Order lengkap — proses yang disebut entity hydration. Untuk kasus di mana kita cuma butuh menampilkan ringkasan (misalnya daftar order bernilai tinggi untuk keperluan review), memuat seluruh entity itu kerja ekstra yang sia-sia: lebih banyak data ditransfer dari database, lebih banyak memory dipakai untuk objek yang sebagian besar field-nya tidak akan disentuh.
Dengan constructor expression, database hanya diminta mengembalikan empat kolom yang benar-benar dibutuhkan, dan hasilnya langsung berbentuk OrderSummary — tidak ada tahap "load entity lengkap lalu buang sebagian". Ini pola umum yang layak diingat: kalau endpoint kamu cuma butuh sebagian kecil kolom dari sebuah tabel besar, pertimbangkan projection alih-alih memuat entity penuh.
Endpoint yang memakainya
OrderController mengekspos query ini lewat endpoint GET /api/orders/high-value:
@GetMapping("/high-value")
fun getHighValueOrders(@RequestParam minTotal: BigDecimal): List<OrderSummary> =
orderService.findHighValueOrders(minTotal)
minTotal diterima sebagai query parameter, diteruskan ke OrderService.findHighValueOrders, yang pada akhirnya memanggil orderRepository.findSummariesWithTotalPriceAbove(minTotal) — persis method yang kita bedah di atas. Endpoint lain di OrderController (seperti inventory-check) adalah bagian dari topik Fase 4 dan tidak relevan di sini.
Tiga gaya query sudah kita kuasai. Bab berikutnya kita bahas topik yang justru krusial ketika salah satu method ini dipanggil berkali-kali dalam satu alur bisnis: bagaimana memastikan sekumpulan operasi database berhasil atau gagal bersama-sama, lewat @Transactional.
Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata
Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...
Lihat Series Lengkap