Mengakses basis data lewat JDBC murni berarti menulis pola yang sama berulang-ulang: buka koneksi, siapkan PreparedStatement, petakan ResultSet ke objek, tutup resource, dan tangani SQLException di setiap method. Spring Data JPA memotong seluruh pola itu dengan satu gagasan berani: kita cukup mendeklarasikan method di interface — implementasinya dihasilkan saat runtime. Pada Chapter ini kita kenal arsitektur berlapis di baliknya, keluarga Repository yang bisa dipilih, dan fitur paling terkenal: derived query dari nama method.
Arsitektur Berlapis #
Spring Data JPA bukan pengganti Hibernate — keduanya bekerja di lapisan berbeda. Hibernate adalah implementasi JPA (jakarta.persistence): ORM yang memetakan objek ke tabel dan sebaliknya. Spring Data JPA berdiri di atasnya sebagai lapisan abstraksi repository: menyerap pembuatan query, pagination, dan transaction management yang tadinya menjadi tugas kode kita. Peta lapisannya:
Controller -> Service -> Repository (Spring Data) -> EntityManager (JPA) -> Hibernate -> JDBC -> DatabaseFondasi abstraksinya ada di Spring Data Commons: interface Repository<T, ID> sebagai akar, CrudRepository untuk operasi CRUD dasar, PagingAndSortingRepository untuk akses berhalaman, dan JpaRepository yang menambahkan utilitas spesifik JPA seperti flush() dan saveAll(). Spring Data menyediakan keluarga serupa untuk teknologi lain — MongoRepository, RedisRepository, JdbcRepository — dengan semangat yang sama.
Interface Repository dan Derived Query #
Seluruh prosesnya dimulai dari interface kosong yang kita deklarasikan sendiri — ditampilkan apa adanya untuk Spring Boot 3.4/3.5:
public interface ProdukRepository extends JpaRepository<Produk, Long> {
List<Produk> findByKategori(String kategori);
List<Produk> findByHargaLessThan(BigDecimal batas);
Optional<Produk> findBySku(String sku);
long countByKategori(String kategori);
boolean existsBySku(String sku);
@Query("SELECT p FROM Produk p WHERE p.nama LIKE %:kataKunci%")
List<Produk> cariBerdasarkanNama(@Param("kataKunci") String kataKunci);
}Tidak ada satu baris implementasi. Saat startup, Spring Data membuat proxy untuk setiap interface repository; method tanpa @Query diurai dari namanya menjadi JPQL, method bertanda @Query memakai query yang tertulis eksplisit. Konvensi penamaan pengurai: prefiks (findBy, countBy, existsBy, deleteBy) menentukan operasi, potongan setelah By menjadi kriteria, kata And/Or menyambung kriteria, OrderBy...Desc mengatur urutan.
Mensimulasikan Pengurai Nama Method #
Alat penguraian nama method layak dibedah agar konvensinya tidak terasa seperti tebakan. Program Java murni berikut mengurai lima nama method repository menjadi operasi, kriteria, dan urutan:
import java.util.ArrayList;
import java.util.List;
public class TurunanQuery {
// Hasil penguraian nama method repository ala Spring Data.
record QueryTurunan(String operasi, List<String> kriteria, String penyambung,
String kolomUrut, boolean menurun) {
String keSql() {
StringBuilder sql = new StringBuilder();
sql.append(switch (operasi) {
case "find" -> "SELECT * FROM tabel";
case "count" -> "SELECT COUNT(*) FROM tabel";
case "exists" -> "SELECT EXISTS(SELECT 1 FROM tabel";
case "delete" -> "DELETE FROM tabel";
default -> throw new IllegalStateException();
});
if (!kriteria.isEmpty()) {
sql.append(" WHERE ");
for (int i = 0; i < kriteria.size(); i++) {
sql.append(i == 0 ? "" : " " + penyambung + " ");
sql.append(kriteria.get(i)).append(" = ?");
}
}
if (kolomUrut != null) {
sql.append(" ORDER BY ").append(kolomUrut)
.append(menurun ? " DESC" : " ASC");
}
if (operasi.equals("exists")) {
sql.append(")");
}
return sql.toString();
}
}
public static void main(String[] args) {
String[] namaMethod = {
"findByEmailAndStatus",
"countByStatus",
"existsByEmail",
"deleteByKategori",
"findByNamaOrderByTanggalDibuatDesc"
};
for (String nama : namaMethod) {
QueryTurunan q = uraikan(nama);
System.out.println(nama);
System.out.println(" operasi : " + q.operasi());
System.out.println(" kriteria : " + q.kriteria()
+ (q.kriteria().size() > 1 ? " (disambung " + q.penyambung() + ")" : ""));
System.out.println(" urutan : " + (q.kolomUrut() == null
? "-" : q.kolomUrut() + (q.menurun() ? " DESC" : " ASC")));
System.out.println(" kira-kira: " + q.keSql());
System.out.println();
}
}
static QueryTurunan uraikan(String nama) {
String operasi;
if (nama.startsWith("findBy")) {
operasi = "find";
} else if (nama.startsWith("countBy")) {
operasi = "count";
} else if (nama.startsWith("existsBy")) {
operasi = "exists";
} else if (nama.startsWith("deleteBy")) {
operasi = "delete";
} else {
throw new IllegalArgumentException("Operasi tidak dikenal: " + nama);
}
// Buang prefiks operasi + "By".
String sisa = nama.substring(operasi.length() + 2);
// Pisahkan klausa OrderBy bila ada.
String kolomUrut = null;
boolean menurun = false;
int idxOrder = sisa.indexOf("OrderBy");
if (idxOrder >= 0) {
String urutan = sisa.substring(idxOrder + "OrderBy".length());
menurun = urutan.endsWith("Desc");
kolomUrut = menurun
? urutan.substring(0, urutan.length() - "Desc".length())
: urutan;
sisa = sisa.substring(0, idxOrder);
}
// Pecah kriteria pada And / Or.
List<String> kriteria = new ArrayList<>();
String penyambung = "AND";
if (sisa.contains("And")) {
for (String k : sisa.split("And")) {
kriteria.add(kecilAwal(k));
}
} else if (sisa.contains("Or")) {
penyambung = "OR";
for (String k : sisa.split("Or")) {
kriteria.add(kecilAwal(k));
}
} else if (!sisa.isEmpty()) {
kriteria.add(kecilAwal(sisa));
}
return new QueryTurunan(operasi, kriteria, penyambung,
kolomUrut == null ? null : kecilAwal(kolomUrut), menurun);
}
// "EmailAndStatus" -> "email": huruf pertama kecil seperti nama property.
static String kecilAwal(String s) {
return Character.toLowerCase(s.charAt(0)) + s.substring(1);
}
}Output yang Dihasilkan:
findByEmailAndStatus
operasi : find
kriteria : [email, status] (disambung AND)
urutan : -
kira-kira: SELECT * FROM tabel WHERE email = ? AND status = ?
countByStatus
operasi : count
kriteria : [status]
urutan : -
kira-kira: SELECT COUNT(*) FROM tabel WHERE status = ?
existsByEmail
operasi : exists
kriteria : [email]
urutan : -
kira-kira: SELECT EXISTS(SELECT 1 FROM tabel WHERE email = ?)
deleteByKategori
operasi : delete
kriteria : [kategori]
urutan : -
kira-kira: DELETE FROM tabel WHERE kategori = ?
findByNamaOrderByTanggalDibuatDesc
operasi : find
kriteria : [nama]
urutan : tanggalDibuat DESC
kira-kira: SELECT * FROM tabel WHERE nama = ? ORDER BY tanggalDibuat DESCMari kita bedah pengurainya:
- Deretan
startsWithuntuk operasi: string diperiksa terhadap empat prefiks yang dikenal Spring Data. Sisa nama dipotong dengansubstring(operasi.length() + 2)— angka2menghapus kataByyang selalu mengikuti prefiks. indexOf("OrderBy")sebelum pemecahan kriteria: urutan pengerjaan ini penting. KlausaOrderByharus dipisahkan lebih dulu; jika tidak, pemecahan padaOrakan salah memotong kataOrderBymenjadi kriteria palsu — jebakan nyata pada pengurai sederhana.split("And")dansplit("Or"): kriteria dipecah pada kata sambung, lalukecilAwal()menurunkan huruf pertama (Email->email) sehingga hasilnya cocok dengan nama property entity. Dua kriteria pada contoh pertama tercetak[email, status]dengan penyambungAND.keSql()dengan switch expression: hasil penguraian diterjemahkan menjadi SQL perkiraan agar mudah dibaca. MethodfindByNamaOrderByTanggalDibuatDescmenjadiWHERE nama = ? ORDER BY tanggalDibuat DESC— persis bagaimana Spring Data menyusun JPQL-nya sebelum Hibernate mengubahnya menjadi SQL asli.
Konfigurasi Koneksi #
Dua starter menutup seluruh kebutuhan: spring-boot-starter-data-jpa membawa Hibernate dan Spring Data, sementara driver basis data ditambahkan terpisah. Konfigurasi koneksi berjalan lewat application.properties — ditampilkan apa adanya:
spring.datasource.url=jdbc:postgresql://db.example.com/produksi
spring.datasource.username=budi
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=falseProperti ddl-auto=validate menjaga Hibernate tidak mengubah skema di produksi — migrasi skema adalah tugas tool khusus seperti Flyway atau Liquibase. Properti open-in-view=false mematikan pola yang membiarkan lazy loading berjalan di lapisan web — pembahasan mendalamnya ada di Chapter Entities dan Repositories, tempat kita membedah pemetaan objek-tabel beserta jebakan relasinya.
Referensi #
- Spring Data: Project Home
- Spring Data JPA Reference: Query Methods
- Spring Guides: Accessing Data with JPA
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
TurunanQuery.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class TurunanQuery {
// Hasil penguraian nama method repository ala Spring Data.
record QueryTurunan(String operasi, List<String> kriteria, String penyambung,
String kolomUrut, boolean menurun) {
String keSql() {
StringBuilder sql = new StringBuilder();
sql.append(switch (operasi) {
case "find" -> "SELECT * FROM tabel";
case "count" -> "SELECT COUNT(*) FROM tabel";
case "exists" -> "SELECT EXISTS(SELECT 1 FROM tabel";
case "delete" -> "DELETE FROM tabel";
default -> throw new IllegalStateException();
});
if (!kriteria.isEmpty()) {
sql.append(" WHERE ");
for (int i = 0; i < kriteria.size(); i++) {
sql.append(i == 0 ? "" : " " + penyambung + " ");
sql.append(kriteria.get(i)).append(" = ?");
}
}
if (kolomUrut != null) {
sql.append(" ORDER BY ").append(kolomUrut)
.append(menurun ? " DESC" : " ASC");
}
if (operasi.equals("exists")) {
sql.append(")");
}
return sql.toString();
}
}
public static void main(String[] args) {
String[] namaMethod = {
"findByEmailAndStatus",
"countByStatus",
"existsByEmail",
"deleteByKategori",
"findByNamaOrderByTanggalDibuatDesc"
};
for (String nama : namaMethod) {
QueryTurunan q = uraikan(nama);
System.out.println(nama);
System.out.println(" operasi : " + q.operasi());
System.out.println(" kriteria : " + q.kriteria()
+ (q.kriteria().size() > 1 ? " (disambung " + q.penyambung() + ")" : ""));
System.out.println(" urutan : " + (q.kolomUrut() == null
? "-" : q.kolomUrut() + (q.menurun() ? " DESC" : " ASC")));
System.out.println(" kira-kira: " + q.keSql());
System.out.println();
}
}
static QueryTurunan uraikan(String nama) {
String operasi;
if (nama.startsWith("findBy")) {
operasi = "find";
} else if (nama.startsWith("countBy")) {
operasi = "count";
} else if (nama.startsWith("existsBy")) {
operasi = "exists";
} else if (nama.startsWith("deleteBy")) {
operasi = "delete";
} else {
throw new IllegalArgumentException("Operasi tidak dikenal: " + nama);
}
// Buang prefiks operasi + "By".
String sisa = nama.substring(operasi.length() + 2);
// Pisahkan klausa OrderBy bila ada.
String kolomUrut = null;
boolean menurun = false;
int idxOrder = sisa.indexOf("OrderBy");
if (idxOrder >= 0) {
String urutan = sisa.substring(idxOrder + "OrderBy".length());
menurun = urutan.endsWith("Desc");
kolomUrut = menurun
? urutan.substring(0, urutan.length() - "Desc".length())
: urutan;
sisa = sisa.substring(0, idxOrder);
}
// Pecah kriteria pada And / Or.
List<String> kriteria = new ArrayList<>();
String penyambung = "AND";
if (sisa.contains("And")) {
for (String k : sisa.split("And")) {
kriteria.add(kecilAwal(k));
}
} else if (sisa.contains("Or")) {
penyambung = "OR";
for (String k : sisa.split("Or")) {
kriteria.add(kecilAwal(k));
}
} else if (!sisa.isEmpty()) {
kriteria.add(kecilAwal(sisa));
}
return new QueryTurunan(operasi, kriteria, penyambung,
kolomUrut == null ? null : kecilAwal(kolomUrut), menurun);
}
// "EmailAndStatus" -> "email": huruf pertama kecil seperti nama property.
static String kecilAwal(String s) {
return Character.toLowerCase(s.charAt(0)) + s.substring(1);
}
}