M
Mr Sugiarto
Developer
18 Sep 2026 4 min read

Bab lalu kita sudah menyalakan PostgreSQL lewat Docker Compose dan memahami kenapa ddl-auto diset validate. Sekarang saatnya melihat sisi Kotlin-nya: bagaimana sebuah data class biasa berubah jadi representasi tabel database lewat JPA, dan bagaimana JpaRepository memberi kita operasi CRUD tanpa satu baris implementasi pun.

Entity Order

Ini isi Order.kt:

@Entity
@Table(name = "orders")
data class Order(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    val customerName: String,
    val itemName: String,
    val quantity: Int,
    val unitPrice: BigDecimal,
    val totalPrice: BigDecimal,
    @Enumerated(EnumType.STRING)
    val status: OrderStatus
)
  • @Entity — menandai class ini sebagai representasi sebuah tabel database. Setiap instance Order yang disimpan lewat JPA akan menjadi satu baris.
  • @Table(name = "orders") — eksplisit menyebutkan nama tabelnya orders. Tanpa anotasi ini, Hibernate akan menebak nama tabel dari nama class (biasanya jadi order, yang bahkan bentrok dengan keyword SQL ORDER BY — jadi eksplisit di sini juga menghindari masalah tersembunyi).
  • @Id @GeneratedValue(strategy = GenerationType.IDENTITY)@Id menandai id sebagai primary key, dan GenerationType.IDENTITY bilang ke Hibernate: "biarkan database sendiri yang generate nilai ini" (di PostgreSQL, ini artinya kolom GENERATED ALWAYS AS IDENTITY, yang akan kita lihat lagi di migration bab depan).

Kenapa EnumType.STRING, bukan default (ORDINAL)?

Kolom status diberi anotasi @Enumerated(EnumType.STRING). Ini keputusan kecil yang dampaknya besar kalau diabaikan. JPA punya dua cara menyimpan enum:

  • EnumType.ORDINAL (default kalau anotasi @Enumerated tidak ditulis sama sekali) — menyimpan posisi index enum constant sebagai integer. Untuk OrderStatus, CONFIRMED (index 0) disimpan sebagai 0, NEEDS_REVIEW (index 1) disimpan sebagai 1.
  • EnumType.STRING — menyimpan nama enum constant apa adanya: "CONFIRMED" atau "NEEDS_REVIEW".

Masalah ORDINAL muncul begitu enum-nya berubah. Bayangkan kelak ada kebutuhan menambah status baru di tengah, misalnya:

enum class OrderStatus { CONFIRMED, CANCELLED, NEEDS_REVIEW }

Semua baris lama yang tadinya 1 (artinya NEEDS_REVIEW) sekarang diam-diam terbaca sebagai CANCELLED — bukan karena datanya berubah, tapi karena maknanya bergeser mengikuti urutan enum yang baru. Ini korupsi data yang sangat halus: tidak ada error, tidak ada crash, aplikasi tetap jalan, tapi status order lama jadi salah.

EnumType.STRING kebal dari masalah ini karena yang disimpan adalah nama constant-nya, bukan posisinya — menambah atau mengurutkan ulang enum constant tidak memengaruhi data yang sudah tersimpan. Konsekuensinya cuma sedikit lebih banyak ruang penyimpanan (string vs integer), harga yang sangat wajar dibanding risiko korupsi data. OrderStatus.kt sendiri isinya sederhana:

enum class OrderStatus { CONFIRMED, NEEDS_REVIEW }

kotlin("plugin.jpa") — kenapa data class bisa jadi entity?

Ingat plugin Gradle yang sempat disinggung di Fase 1: kotlin("plugin.jpa"). Ini bukan detail build tool yang bisa diabaikan — tanpanya, Order sebagai data class sebenarnya tidak legal dipakai sebagai JPA entity. Ada dua alasan teknis:

  1. JPA/Hibernate butuh no-arg constructor untuk membuat instance entity lewat reflection sebelum field-nya diisi. data class Kotlin secara default tidak punya constructor kosong kalau semua propertinya tidak punya default value.
  2. Hibernate sering perlu membuat proxy dari sebuah entity (subclass buatan runtime, dipakai misalnya untuk lazy loading). Class Kotlin secara default bersifat final — tidak bisa di-subclass — sehingga Hibernate tidak bisa membuat proxy-nya.

Plugin kotlin("plugin.jpa") menangani keduanya secara otomatis: class yang diberi anotasi @Entity akan digenerate ulang oleh compiler supaya punya no-arg constructor dan tidak lagi final (open), tanpa kita perlu menulis boilerplate itu manual. Ini kenapa Order bisa tetap ditulis sebagai data class biasa dan tetap berfungsi sebagai entity JPA.

OrderRepository — CRUD tanpa implementasi

interface OrderRepository : JpaRepository<Order, Long> {
    // ...
}

Cukup dengan mewarisi JpaRepository<Order, Long> — parameter pertama tipe entity-nya, kedua tipe primary key-nya — interface ini langsung mendapat implementasi lengkap dari Spring Data JPA saat aplikasi start, tanpa kita menulis satu baris kode SQL atau implementasi apa pun. Beberapa method yang otomatis tersedia:

  • save(entity) — insert kalau id belum ada, update kalau sudah ada.
  • findById(id) — mengembalikan Optional<Order>.
  • findAll() — semua baris di tabel orders.
  • deleteById(id) — hapus berdasarkan primary key.
  • count() — jumlah total baris.
  • existsById(id) — cek keberadaan tanpa perlu load seluruh entity.

OrderService.getOrderById di project ini sudah memakai salah satunya:

fun getOrderById(id: Long): Order =
    orderRepository.findById(id).orElseThrow { OrderNotFoundException(id) }

Perhatikan tidak ada implementasi OrderRepositoryImpl di mana pun di project ini — Spring Data JPA yang membuatkan proxy implementasinya di belakang layar saat context aplikasi dibangun. Selain method bawaan ini, OrderRepository juga punya tiga method query custom (findByCustomerNameContainingIgnoreCase, findAllByStatus, findSummariesWithTotalPriceAbove) yang akan kita bedah lengkap di bab query custom nanti — untuk sekarang, cukup pahami dulu bahwa method-method itu ditumpuk di atas fondasi CRUD gratis yang sama.

Sebelum masuk ke query custom, ada satu pertanyaan mendasar yang belum terjawab: dari mana tabel orders itu sendiri berasal, kalau ddl-auto diset validate dan tidak boleh membuat tabel otomatis? Jawabannya ada di bab berikutnya — Flyway.

M
Mr Sugiarto

Developer

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
Artikel Terkait