Exposed adalah ORM ringan yang menyusun DSL type-safe di atas JDBC. Untuk skenario yang membutuhkan abstraksi penuh — caching level dua, lazy loading otomatis, dan portabilitas antar implementation — komunitas Java memakai JPA (Jakarta Persistence API). JPA adalah spec, dan Hibernate adalah implementasi paling populer. Pada chapter ini kita akan memahami hubungan JPA-Hibernate, anotasi @Entity, @Id, @OneToMany, serta lifecycle session yang menjadi inti kerja ORM penuh.

JPA: Spec, Bukan Implementasi #

JPA adalah standar persistence di ekosistem Java/Kotlin, didefinisikan melalui Jakarta EE (dulu Java EE). JPA sendiri hanya spesifikasi — API berupa interface dan anotasi. Aplikasi memakai implementasi yang menjalankan JPA: Hibernate (paling populer), EclipseLink, atau OpenJPA. Karena kode memakai interface JPA, mengganti implementasi relatif mudah tanpa mengubah logika bisnis.

hocon build.gradle.kts
dependencies {
    implementation("jakarta.persistence:jakarta.persistence-api:3.1.0")
    implementation("org.hibernate.orm:hibernate-core:6.5.2.Final")
    runtimeOnly("org.postgresql:postgresql:42.7.2")
}

Hibernate: Implementasi Populer #

Hibernate adalah implementasi JPA paling matang dan paling banyak dipakai. Hibernate menyediakan tambahan di luar JPA spec: HQL (Hibernate Query Language), second-level cache dengan EhCache/Infinispan, dan integrasi dengan Spring Data JPA. API utama Hibernate adalah SessionFactory yang membuat Session. Session di JPA dipetakan ke EntityManager.

Anotasi Inti JPA #

Anotasi JPA menandai class sebagai entity dan mendeskripsikan pemetaan ke tabel:

kotlin Entity dengan anotasi JPA
import jakarta.persistence.*

@Entity
@Table(name = "pelanggan")
data class Pelanggan(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(name = "nama", nullable = false, length = 100)
    val nama: String,

    @Column(name = "email", nullable = false, unique = true)
    val email: String,

    @OneToMany(mappedBy = "pelanggan", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
    val pesanan: List<Pesanan> = emptyList()
)

@Entity
@Table(name = "pesanan")
data class Pesanan(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "pelanggan_id")
    val pelanggan: Pelanggan? = null,

    @Column(name = "total") val total: Int
)
  • @Entity menandai class sebagai entity JPA. Harus punya no-arg constructor (Hibernate butuh ini untuk instantiate via reflection; di Kotlin pakai plugin kotlin-jpa atau all-open).
  • @Id menandai primary key. @GeneratedValue mengatur strategi generate ID (IDENTITY, SEQUENCE, TABLE, UUID).
  • @Column mengatur nama kolom, nullable, length, unique.
  • @OneToMany / @ManyToOne mendefinisikan relasi. mappedBy menandai sisi inverse (tanpa FK).
  • fetch = FetchType.LAZY memuat relasi saat diakses; EAGER saat entity induk dimuat.

Lifecycle Session #

Instance entity di JPA punya empat state:

StateKondisi
transientBaru dibuat new, belum di-associate dengan session
persistentSedang dikelola session; perubahan ter- track dan akan disinkronkan saat flush
detachedPernah persistent, session sudah ditutup; masih bisa dimodifikasi tapi perubahan tidak otomatis tersimpan
removedDi- mark untuk delete saat commit

Method utama EntityManager: persist(entity) (transient → persistent), find(Klass, id), merge(entity) (detached → persistent), remove(entity) (persistent → removed), createQuery(jpql).

Kode Lengkap: JPA-Style API #

Kode berikut memodelkan JPA-like API di atas SQLite agar bisa dijalankan tanpa dependency Hibernate penuh. Kode mendefinisikan anotasi mock, EntityManager sederhana, dan demonstrasi persist, find, serta query JPQL-like.

kotlin
// jpa_dan_hibernate.kt
// Mendemonstrasikan konsep JPA (Jakarta Persistence API) dan implementasi
// Hibernate: anotasi @Entity, @Id, @OneToMany, dan session lifecycle. Pada
// kode produksi, Hibernate memproses anotasi via reflection. Di sini kita
// memodelkan JPA-like API sederhana di atas SQLite agar bisa dijalankan
// tanpa dependency Hibernate penuh.

import java.sql.Connection
import java.sql.DriverManager

// Mock anotasi JPA (di produksi: jakarta.persistence.*)
@Target(AnnotationTarget.CLASS) annotation class Entity(val table: String = "")
@Target(AnnotationTarget.PROPERTY) annotation class Id(val generated: Boolean = true)
@Target(AnnotationTarget.PROPERTY) annotation class Column(val name: String = "")
@Target(AnnotationTarget.PROPERTY) annotation class ManyToOne
@Target(AnnotationTarget.PROPERTY) annotation class OneToMany
@Target(AnnotationTarget.PROPERTY) annotation class Transient

// Metadata entity: hasil scan anotasi (di produksi: Hibernate EntityManagerFactory)
data class FieldMeta(
    val propertyName: String,
    val columnName: String,
    val sqlType: String,
    val isId: Boolean = false,
    val isGenerated: Boolean = false,
    val isTransient: Boolean = false
)
data class EntityMeta(
    val klassName: String,
    val table: String,
    val fields: List<FieldMeta>
) {
    fun idField(): FieldMeta = fields.first { it.isId }
    fun persistableFields(): List<FieldMeta> = fields.filterNot { it.isTransient || it.isId && it.isGenerated }
}

// EntityManager mirip Hibernate Session
class EntityManager(private val conn: Connection) {
    private val metas = mutableMapOf<String, EntityMeta>()

    fun register(meta: EntityMeta) {
        metas[meta.klassName] = meta
    }

    fun createTable(klassName: String): String {
        val meta = metas[klassName] ?: error("Entity $klassName belum diregistrasi")
        val cols = meta.fields.filterNot { it.isTransient }.joinToString(",\n  ") { f ->
            val parts = mutableListOf(f.columnName, f.sqlType)
            if (f.isId) {
                parts.add("PRIMARY KEY")
                if (f.isGenerated) parts.add("AUTOINCREMENT")
            }
            parts.joinToString(" ")
        }
        val ddl = "CREATE TABLE IF NOT EXISTS ${meta.table} (\n  $cols\n)"
        conn.createStatement().use { it.execute(ddl) }
        return ddl
    }

    fun <T : Any> persist(klassName: String, entity: T, extractField: (T, String) -> Any?) {
        val meta = metas[klassName] ?: error("Entity $klassName belum diregistrasi")
        val cols = meta.persistableFields()
        val colNames = cols.joinToString(", ") { it.columnName }
        val placeholders = cols.joinToString(", ") { "?" }
        val sql = "INSERT INTO ${meta.table} ($colNames) VALUES ($placeholders)"
        conn.prepareStatement(sql).use { ps ->
            cols.forEachIndexed { i, f ->
                val v = extractField(entity, f.propertyName)
                when (v) {
                    is String -> ps.setString(i + 1, v)
                    is Int -> ps.setInt(i + 1, v)
                    is Long -> ps.setLong(i + 1, v)
                    is Boolean -> ps.setBoolean(i + 1, v)
                    null -> ps.setNull(i + 1, java.sql.Types.NULL)
                    else -> ps.setObject(i + 1, v)
                }
            }
            ps.executeUpdate()
        }
    }

    fun find(klassName: String, id: Int, hydrate: (Map<String, Any?>) -> Unit) {
        val meta = metas[klassName] ?: error("Entity $klassName belum diregistrasi")
        val sql = "SELECT * FROM ${meta.table} WHERE ${meta.idField().columnName} = ?"
        conn.prepareStatement(sql).use { ps ->
            ps.setInt(1, id)
            ps.executeQuery().use { rs ->
                if (rs.next()) {
                    val md = rs.metaData
                    val row = (1..md.columnCount).associate { i -> md.getColumnName(i) to rs.getObject(i) }
                    hydrate(row)
                }
            }
        }
    }
}

// Contoh entity Pelanggan (di produksi: Hibernate memproses anotasi ini)
@Entity(table = "pelanggan")
data class Pelanggan(
    @Id(generated = true) val id: Long = 0,
    @Column(name = "nama") val nama: String,
    @Column(name = "email") val email: String
)

// Contoh entity Pesanan dengan relasi ManyToOne ke Pelanggan
@Entity(table = "pesanan")
data class Pesanan(
    @Id(generated = true) val id: Long = 0,
    @Column(name = "pelanggan_id") val pelangganId: Long,
    @Column(name = "total") val total: Int,
    @Transient val pelanggan: Pelanggan? = null
)

fun main() {
    Class.forName("org.sqlite.JDBC")
    val url = "jdbc:sqlite:file::memory:?cache=shared"
    val keeper: Connection = DriverManager.getConnection(url)

    DriverManager.getConnection(url).use { conn ->
        val em = EntityManager(conn)
        // Registrasi metadata (di produksi: Hibernate scan anotasi otomatis)
        em.register(EntityMeta(
            klassName = "Pelanggan", table = "pelanggan",
            fields = listOf(
                FieldMeta("id", "id", "INTEGER", isId = true, isGenerated = true),
                FieldMeta("nama", "nama", "TEXT"),
                FieldMeta("email", "email", "TEXT")
            )
        ))
        em.register(EntityMeta(
            klassName = "Pesanan", table = "pesanan",
            fields = listOf(
                FieldMeta("id", "id", "INTEGER", isId = true, isGenerated = true),
                FieldMeta("pelangganId", "pelanggan_id", "INTEGER"),
                FieldMeta("total", "total", "INTEGER"),
                FieldMeta("pelanggan", "pelanggan", "INTEGER", isTransient = true)
            )
        ))

        println("=== JPA-style DDL generation ===")
        println(em.createTable("Pelanggan"))
        println()
        println(em.createTable("Pesanan"))

        println("\n=== persist() = Hibernate Session.save() ===")
        em.persist("Pelanggan", Pelanggan(0, "Budi Santoso", "[email protected]")) { e, prop ->
            when (prop) {
                "nama" -> e.nama; "email" -> e.email; else -> null
            }
        }
        em.persist("Pelanggan", Pelanggan(0, "Siti Aminah", "[email protected]")) { e, prop ->
            when (prop) {
                "nama" -> e.nama; "email" -> e.email; else -> null
            }
        }
        println("2 pelanggan dipersist")

        em.persist("Pesanan", Pesanan(0, 1, 150000)) { e, prop ->
            when (prop) { "pelangganId" -> e.pelangganId; "total" -> e.total; else -> null }
        }
        em.persist("Pesanan", Pesanan(0, 1, 75000)) { e, prop ->
            when (prop) { "pelangganId" -> e.pelangganId; "total" -> e.total; else -> null }
        }
        em.persist("Pesanan", Pesanan(0, 2, 200000)) { e, prop ->
            when (prop) { "pelangganId" -> e.pelangganId; "total" -> e.total; else -> null }
        }
        println("3 pesanan dipersist")

        println("\n=== find() = Session.get() ===")
        em.find("Pelanggan", 1) { row ->
            println("Pelanggan(id=${row["id"]}, nama=${row["nama"]}, email=${row["email"]})")
        }

        println("\n=== JPQL-like query: SELECT p FROM Pesanan p WHERE p.pelangganId = 1 ===")
        conn.prepareStatement("SELECT * FROM pesanan WHERE pelanggan_id = 1 ORDER BY id").use { ps ->
            ps.executeQuery().use { rs ->
                while (rs.next()) {
                    println("  Pesanan(id=${rs.getInt("id")}, pelanggan_id=${rs.getInt("pelanggan_id")}, total=${rs.getInt("total")})")
                }
            }
        }

        println("\n=== Hibernate lifecycle ===")
        println("1. transient    : objek baru, belum di-associate ke session")
        println("2. persistent   : sedang dikelola session, perubahan ter-track")
        println("3. detached     : pernah persistent, session sudah close")
        println("4. removed      : di-mark untuk delete pada commit")

        println("\n=== Perbandingan JPA vs Hibernate ===")
        println("| Aspek        | JPA (spec)         | Hibernate (impl)              |")
        println("|--------------|--------------------|-------------------------------|")
        println("| Anotasi      | jakarta.persistence| jakarta.persistence           |")
        println("| API          | EntityManager      | Session (extends EM)         |")
        println("| Query lang   | JPQL               | HQL (superset JPQL)          |")
        println("| Cache L2     | via provider       | EhCache/Infinispan           |")
    }

    keeper.close()
}

Output yang Dihasilkan:

text
=== JPA-style DDL generation ===
CREATE TABLE IF NOT EXISTS pelanggan (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  nama TEXT,
  email TEXT
)

CREATE TABLE IF NOT EXISTS pesanan (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  pelanggan_id INTEGER,
  total INTEGER
)

=== persist() = Hibernate Session.save() ===
2 pelanggan dipersist
3 pesanan dipersist

=== find() = Session.get() ===
Pelanggan(id=1, nama=Budi Santoso, [email protected])

=== JPQL-like query: SELECT p FROM Pesanan p WHERE p.pelangganId = 1 ===
  Pesanan(id=1, pelanggan_id=1, total=150000)
  Pesanan(id=2, pelanggan_id=1, total=75000)

=== Hibernate lifecycle ===
1. transient    : objek baru, belum di-associate ke session
2. persistent   : sedang dikelola session, perubahan ter-track
3. detached     : pernah persistent, session sudah close
4. removed      : di-mark untuk delete pada commit

=== Perbandingan JPA vs Hibernate ===
| Aspek        | JPA (spec)         | Hibernate (impl)              |
|--------------|--------------------|-------------------------------|
| Anotasi      | jakarta.persistence| jakarta.persistence           |
| API          | EntityManager      | Session (extends EM)         |
| Query lang   | JPQL               | HQL (superset JPQL)          |
| Cache L2     | via provider       | EhCache/Infinispan           |

Kotlin Plugin yang Diperlukan #

Hibernate butuh no-arg constructor untuk instantiate entity via reflection. Kotlin secara default menghasilkan constructor dengan semua parameter, sehingga kita perlu plugin kotlin-jpa (untuk entity JPA) dan all-open (karena entity perlu class yang open agar bisa di- subclass oleh Hibernate proxy):

hocon build.gradle.kts (plugins)
plugins {
    kotlin("jvm") version "2.0.21"
    kotlin("plugin.jpa") version "2.0.21"
    kotlin("plugin.allopen") version "2.0.21"
}

allOpen {
    annotation("jakarta.persistence.Entity")
    annotation("jakarta.persistence.MappedSuperclass")
    annotation("jakarta.persistence.Embeddable")
}

Tanpa plugin ini, Hibernate melempar error saat runtime karena tidak bisa instantiate entity Kotlin.

Referensi Lanjutan #

Selanjutnya #

Setelah memahami JPA dan Hibernate, kita akan mendalami relasi antar entity: one-to-one, one-to-many, many-to-many, serta strategi fetch EAGER vs LAZY. Chapter Entities dan Relationships akan membedahnya dengan contoh konkret.