Spring Data JPA adalah modul Spring yang mengabstraksi akses data JPA. Kita mendefinisikan interface repository dengan method seperti findById atau findByNama, dan Spring menghasilkan implementasi otomatis. Hal ini menghapus boilerplate JDBC/SQL dan memungkinkan kita fokus pada query yang penting. Pada chapter ini kita akan memahami JpaRepository<T, ID>, derived query methods, dan @Query untuk query kustom.
JpaRepository<T, ID> #
JpaRepository<T, ID> adalah interface dasar yang menyediakan CRUD lengkap untuk entitas T dengan primary key bertipe ID. Spring menghasilkan implementasi pada runtime via proxy — kita tidak perlu menulis satu baris SQL pun untuk operasi dasar.
interface UserRepository : JpaRepository<User, Int> {
// Spring menghasilkan findById, findAll, save, delete, count otomatis
fun findByEmail(email: String): User?
fun findByNamaContaining(keyword: String): List<User>
}Interface di atas otomatis memiliki method save(user), findById(1), findAll(), delete(user), count() — semua tanpa implementasi. Spring memakai Hibernate sebagai implementasi JPA default untuk menerjemahkan method menjadi SQL.
Derived Query Methods #
Derived query adalah method yang namanya mengikuti pola tertentu — Spring menerjemahkan nama method menjadi query otomatis. Contoh:
| Method | Query SQL yang dihasilkan |
|---|---|
findByEmail(email) | WHERE email = ? |
findByNamaContaining(keyword) | WHERE nama LIKE %?% |
findByHargaBetween(min, max) | WHERE harga BETWEEN ? AND ? |
findByStokLessThan(threshold) | WHERE stok < ? |
findByKategoriOrderByHargaAsc(kat) | WHERE kategori = ? ORDER BY harga ASC |
Prefix findBy wajib. Setelah prefix, kita sebutkan field (kapitalisasi Pertama). Keyword operator (Containing, Between, LessThan, OrderBy) menentukan klausa SQL.
@Query untuk Query Kustom #
Untuk query yang tidak dapat diekspresikan via derived query, gunakan @Query dengan JPQL atau native SQL:
@Query("SELECT p FROM Produk p WHERE p.stok > :threshold ORDER BY p.stok DESC")
fun findProdukStokLebihDari(@Param("threshold") threshold: Int): List<Produk>@Query cocok untuk query dengan JOIN, GROUP BY, atau subquery. Parameter diberi nama via :nama dan diikat dengan @Param.
Entitas JPA #
Entitas adalah class yang dipetakan ke tabel database. Anotasi @Entity menandai class sebagai entitas. @Id menandai primary key. @Column(name="...") menyesuaikan nama kolom.
@Entity
data class Produk(
@Id val id: Int = 0,
@Column(name = "nama") val nama: String = "",
@Column(name = "harga") val harga: Int = 0,
@Column(name = "stok") val stok: Int = 0,
@Column(name = "kategori") val kategori: String = ""
)Pada Kotlin, data class cocok untuk entitas — immutability default aman untuk Hibernate walau ada beberapa batasan (mis. @OneToMany inverse memerlukan collection mutable).
Demo Spring Data JPA #
Karena Spring Data JPA nyata membutuhkan Hibernate dan database, kita akan memodelkannya sebagai class InMemoryJpaRepository<T, ID> yang meniru perilaku derived query. Program ini mendemonstrasikan lima pola query yang umum.
// spring_data_jpa.kt
// Mendemonstrasikan Spring Data JPA: JpaRepository<T, ID>, derived query
// methods, dan @Query. Pada kode produksi, Spring memakai Hibernate sebagai
// implementasi JPA. Di sini kita memodelkannya sebagai repository in-memory
// yang meniru perilaku derived query.
// Mock anotasi JPA
@Target(AnnotationTarget.CLASS) annotation class Entity
@Target(AnnotationTarget.PROPERTY) annotation class Id
@Target(AnnotationTarget.PROPERTY) annotation class Column(val name: String = "")
@Entity
data class Produk(
@Id val id: Int = 0,
@Column(name = "nama") val nama: String = "",
@Column(name = "harga") val harga: Int = 0,
@Column(name = "stok") val stok: Int = 0,
@Column(name = "kategori") val kategori: String = ""
)
// JpaRepository<T, ID>: interface dasar dengan CRUD + paging/sorting
interface JpaRepository<T, ID> {
fun save(entity: T): T
fun findById(id: ID): T?
fun findAll(): List<T>
fun delete(entity: T)
fun count(): Long
}
// Implementasi dasar yang dipakai semua repository turunan
abstract class InMemoryJpaRepository<T, ID>(
private val idSelector: (T) -> ID
) : JpaRepository<T, ID> {
protected val store = mutableListOf<T>()
protected abstract fun nextId(): ID
override fun save(entity: T): T {
val id = idSelector(entity)
val existing = store.find { idSelector(it) == id }
if (existing != null) {
val idx = store.indexOf(existing)
store[idx] = entity
} else {
store.add(entity)
}
return entity
}
override fun findById(id: ID): T? = store.find { idSelector(it) == id }
override fun findAll(): List<T> = store.toList()
override fun delete(entity: T) { store.remove(entity) }
override fun count(): Long = store.size.toLong()
}
// ProdukRepository: derived query methods + @Query custom
class ProdukRepository : InMemoryJpaRepository<Produk, Int>(
idSelector = { it.id }
) {
override fun nextId(): Int = (store.maxOfOrNull { it.id } ?: 0) + 1
// Helper simpan dengan auto-id
fun saveNew(nama: String, harga: Int, stok: Int, kategori: String): Produk {
val p = Produk(nextId(), nama, harga, stok, kategori)
save(p)
return p
}
// Derived: findBy<Kategori>(String kategori)
fun findByKategori(kategori: String): List<Produk> =
store.filter { it.kategori == kategori }
// Derived: findBy<Nama>Containing(String keyword)
fun findByNamaContaining(keyword: String): List<Produk> =
store.filter { it.nama.contains(keyword, ignoreCase = true) }
// Derived: findBy<Harga>Between(int min, int max)
fun findByHargaBetween(min: Int, max: Int): List<Produk> =
store.filter { it.harga in min..max }
// Derived: findBy<Stok>LessThan(int threshold)
fun findByStokLessThan(threshold: Int): List<Produk> =
store.filter { it.stok < threshold }
// Derived: findBy<Kategori>OrderBy<Harga>Asc
fun findByKategoriOrderByHargaAsc(kategori: String): List<Produk> =
store.filter { it.kategori == kategori }.sortedBy { it.harga }
// @Query custom: cari produk dengan stok > threshold
fun findProdukStokLebihDari(threshold: Int): List<Produk> =
store.filter { it.stok > threshold }.sortedByDescending { it.stok }
}
fun main() {
val repo = ProdukRepository()
// Seed data
repo.saveNew("Kopi Susu", 18000, 50, "minuman")
repo.saveNew("Es Teh Manis", 8000, 100, "minuman")
repo.saveNew("Nasi Goreng", 25000, 20, "makanan")
repo.saveNew("Mie Goreng", 22000, 15, "makanan")
repo.saveNew("Keripik Singkong", 12000, 5, "snack")
println("=== findAll() — Count: ${repo.count()} ===")
repo.findAll().forEach { println(" $it") }
println("\n=== findByKategori('minuman') ===")
repo.findByKategori("minuman").forEach { println(" $it") }
println("\n=== findByNamaContaining('goreng') ===")
repo.findByNamaContaining("goreng").forEach { println(" $it") }
println("\n=== findByHargaBetween(10000, 22000) ===")
repo.findByHargaBetween(10000, 22000).forEach { println(" $it") }
println("\n=== findByStokLessThan(20) ===")
repo.findByStokLessThan(20).forEach { println(" $it") }
println("\n=== findByKategoriOrderByHargaAsc('makanan') ===")
repo.findByKategoriOrderByHargaAsc("makanan").forEach { println(" $it") }
println("\n=== findById(2) ===")
println(" ${repo.findById(2)}")
println("\n=== @Query: stok > 20, urut stok desc ===")
repo.findProdukStokLebihDari(20).forEach { println(" $it") }
}Output yang Dihasilkan:
=== findAll() — Count: 5 ===
Produk(id=1, nama=Kopi Susu, harga=18000, stok=50, kategori=minuman)
Produk(id=2, nama=Es Teh Manis, harga=8000, stok=100, kategori=minuman)
Produk(id=3, nama=Nasi Goreng, harga=25000, stok=20, kategori=makanan)
Produk(id=4, nama=Mie Goreng, harga=22000, stok=15, kategori=makanan)
Produk(id=5, nama=Keripik Singkong, harga=12000, stok=5, kategori=snack)
=== findByKategori('minuman') ===
Produk(id=1, nama=Kopi Susu, harga=18000, stok=50, kategori=minuman)
Produk(id=2, nama=Es Teh Manis, harga=8000, stok=100, kategori=minuman)
=== findByNamaContaining('goreng') ===
Produk(id=3, nama=Nasi Goreng, harga=25000, stok=20, kategori=makanan)
Produk(id=4, nama=Mie Goreng, harga=22000, stok=15, kategori=makanan)
=== findByHargaBetween(10000, 22000) ===
Produk(id=1, nama=Kopi Susu, harga=18000, stok=50, kategori=minuman)
Produk(id=4, nama=Mie Goreng, harga=22000, stok=15, kategori=makanan)
Produk(id=5, nama=Keripik Singkong, harga=12000, stok=5, kategori=snack)
=== findByStokLessThan(20) ===
Produk(id=4, nama=Mie Goreng, harga=22000, stok=15, kategori=makanan)
Produk(id=5, nama=Keripik Singkong, harga=12000, stok=5, kategori=snack)
=== findByKategoriOrderByHargaAsc('makanan') ===
Produk(id=4, nama=Mie Goreng, harga=22000, stok=15, kategori=makanan)
Produk(id=3, nama=Nasi Goreng, harga=25000, stok=20, kategori=makanan)
=== findById(2) ===
Produk(id=2, nama=Es Teh Manis, harga=8000, stok=100, kategori=minuman)
=== @Query: stok > 20, urut stok desc ===
Produk(id=2, nama=Es Teh Manis, harga=8000, stok=100, kategori=minuman)
Produk(id=1, nama=Kopi Susu, harga=18000, stok=50, kategori=minuman)Mari kita bedah bagian penting kode di atas:
interface JpaRepository<T, ID>: kontrak dengan methodsave,findById,findAll,delete,count. Pada produksi, Spring menghasilkan implementasi otomatis dari interface ini.abstract class InMemoryJpaRepository<T, ID>(idSelector): template class yang menyimpan data dimutableListOf<T>(). Constructor menerimaidSelector: (T) -> ID— function yang mengekstrak ID dari entitas, sehingga methodfindByIddapat bekerja generik.override fun save(entity: T): bila entitas dengan ID sama sudah ada, ganti (update). Bila belum, tambahkan. Pola ini disebut “upsert” — Spring JPA menyimpan dengan semantics sama.fun findByKategori(kategori): derived query yang memfilterstoreberdasarkan fieldkategori. Pada produksi, Spring menerjemahkan nama method menjadi JPQLWHERE kategori = :kategori.fun findProdukStokLebihDari(threshold): query kustom yang diannotasi@Querypada produksi. Di sini, diimplementasi manual dengan filter dan sort — semantik tetap sama.
Paging dan Sorting #
JpaRepository mewarisi PagingAndSortingRepository yang menyediakan findAll(Pageable) dan findAll(Sort). Pageable dibuat via PageRequest.of(page, size) — halaman dimulai dari 0. Sort dibuat via Sort.by("harga").ascending(). Kita akan membahas pagination secara mendalam pada chapter API Pagination dan Filtering.
Referensi Lanjutan #
- Spring Data JPA reference
— rujukan resmi Spring Data JPA untuk
JpaRepository, query derivation, dan@Query
Selanjutnya #
Setelah persistensi berjalan, langkah berikutnya adalah mengamankan akses. Chapter Spring Security akan membahas filter chain, JWT auth filter, dan anotasi @PreAuthorize untuk otorisasi method-level.