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.
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:
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
)@Entitymenandai class sebagai entity JPA. Harus punya no-arg constructor (Hibernate butuh ini untuk instantiate via reflection; di Kotlin pakai pluginkotlin-jpaatauall-open).@Idmenandai primary key.@GeneratedValuemengatur strategi generate ID (IDENTITY,SEQUENCE,TABLE,UUID).@Columnmengatur nama kolom, nullable, length, unique.@OneToMany/@ManyToOnemendefinisikan relasi.mappedBymenandai sisi inverse (tanpa FK).fetch = FetchType.LAZYmemuat relasi saat diakses;EAGERsaat entity induk dimuat.
Lifecycle Session #
Instance entity di JPA punya empat state:
| State | Kondisi |
|---|---|
| transient | Baru dibuat new, belum di-associate dengan session |
| persistent | Sedang dikelola session; perubahan ter- track dan akan disinkronkan saat flush |
| detached | Pernah persistent, session sudah ditutup; masih bisa dimodifikasi tapi perubahan tidak otomatis tersimpan |
| removed | Di- 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.
// 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:
=== 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):
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 #
- Hibernate ORM — dokumentasi resmi Hibernate untuk pemetaan JPA
- Hibernate User Guide — panduan pengguna lengkap Hibernate ORM
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.