Tampilan
Kenapa penting
Di M07 data katalog disimpan di Map di memori — hilang setiap kali aplikasi di-restart. Aplikasi backend nyata menyimpan pesanan, produk, dan pembayaran di database relasional seperti PostgreSQL. Menulis SQL dan memindahkan setiap kolom ResultSet ke field object secara manual itu panjang dan rawan salah. JPA memetakan class Java ke tabel sehingga kamu bekerja dengan object, sementara SQL dibuat oleh framework. Hampir semua codebase Spring Boot di tempat kerja memakai JPA, dan bug paling mahal di sana — update yang tak terduga, data enum rusak, query yang membengkak — berasal dari tidak paham cara JPA mengelola entity. Bab ini membangun fondasi itu.
Konsep inti
ORM, JPA, Hibernate, dan Spring Data JPA
ORM (Object-Relational Mapping) adalah teknik memetakan object ke baris tabel. Tiga nama yang sering tertukar:
| Lapisan | Apa itu |
|---|---|
| Jakarta Persistence | Spesifikasi (API standar): anotasi @Entity, interface EntityManager. Paket jakarta.persistence |
| Hibernate 7 | Implementasi spesifikasi itu — yang benar-benar membuat SQL. Default di Spring Boot 4 |
| Spring Data JPA | Lapisan di atas JPA: repository otomatis, query method, paginasi (bab Spring Data) |
Nama lama "JPA" (Java Persistence API) masih dipakai sehari-hari; sejak pindah ke Jakarta EE paketnya jakarta.*, bukan javax.*. Kode di bab ini memakai API standar jakarta.persistence, jadi berlaku untuk implementasi mana pun.
Menyiapkan project
Tambahkan starter dan driver PostgreSQL. Versinya dikelola oleh Spring Boot, jadi tidak perlu ditulis:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>Starter ini membawa Hibernate, Spring Data JPA, dan connection pool HikariCP. Jalankan PostgreSQL lokal dengan Docker, lalu konfigurasi datasource:
bash
docker run -d --name shop-db -p 5432:5432 \
-e POSTGRES_DB=shop -e POSTGRES_USER=shop -e POSTGRES_PASSWORD=rahasia \
postgres:17yaml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/shop
username: shop
password: ${DB_PASSWORD:rahasia} # di luar laptop: dari environment variable
jpa:
hibernate:
ddl-auto: update # HANYA untuk belajar — lihat bagian ddl-auto
show-sql: true
properties:
hibernate:
format_sql: trueDialect PostgreSQL dideteksi otomatis dari koneksi; tidak perlu menyetel hibernate.dialect.
Entity pertama
Entity adalah class yang setiap instance-nya mewakili satu baris tabel:
java
// Potongan — package id.jalurjava.catalog
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String sku;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal price;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ProductStatus status;
@Column(nullable = false, updatable = false)
private Instant createdAt;
protected Product() {} // wajib untuk JPA; protected agar tidak dipakai kode aplikasi
public Product(String sku, String name, BigDecimal price) {
this.sku = sku;
this.name = name;
this.price = price;
this.status = ProductStatus.ACTIVE;
this.createdAt = Instant.now();
}
public void changePrice(BigDecimal newPrice) {
if (newPrice.signum() <= 0) {
throw new IllegalArgumentException("Harga harus positif");
}
this.price = newPrice;
}
public Long getId() { return id; }
public String getSku() { return sku; }
public BigDecimal getPrice() { return price; }
public ProductStatus getStatus() { return status; }
}@Entitymenandai class sebagai entity;@Tablemenentukan nama tabel (default: nama class).@Idmenandai primary key. Pakai tipe wrapperLong, bukanlong, agar "belum punya ID" bisa diwakilinull.@Columnmengatur nama,nullable,length,unique,precision/scale. Atribut ini terutama memengaruhi DDL yang dibuat Hibernate; aturan yang sebenarnya tetap constraint di database. Tanpa@Column, field tetap dipetakan. Spring Boot memakai naming strategy snake_case, jadicreatedAtmenjadi kolomcreated_at.- Tipe data: uang memakai
BigDecimal→numeric(12,2)(ingat M01: jangandouble). Waktu kejadian memakaiInstant→timestamp with time zonedi PostgreSQL. Enum selalu@Enumerated(EnumType.STRING); default-nyaORDINALyang menyimpan posisi konstanta — menambah konstanta di tengah enum diam-diam merusak data lama.
Tidak ada setter untuk semua field: perubahan lewat method bermakna seperti changePrice yang sekaligus memvalidasi. Hibernate mengisi field langsung (karena @Id diletakkan di field), jadi setter tidak dibutuhkan.
Strategi ID: IDENTITY vs SEQUENCE
| Strategi | Cara kerja | Catatan |
|---|---|---|
IDENTITY | kolom auto-increment (generated by default as identity) | ID baru diketahui setelah INSERT → Hibernate tidak bisa batch insert |
SEQUENCE | ID diambil dari sequence database sebelum INSERT | bisa pre-alokasi blok ID (allocationSize, default 50) dan batch insert |
java
// Potongan — alternatif SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq")
@SequenceGenerator(name = "product_seq", sequenceName = "product_seq", allocationSize = 50)
private Long id;IDENTITY paling sederhana dan cukup untuk kebanyakan tabel. SEQUENCE lebih efisien untuk insert massal, tetapi INCREMENT BY sequence di database harus sama dengan allocationSize — penting saat skema dikelola Flyway.
Kenapa constructor no-arg dan bukan record
Saat membaca baris dari database, Hibernate membuat object kosong lewat constructor tanpa argumen, lalu mengisi field-nya. Untuk relasi lazy (bab berikutnya) Hibernate membuat proxy — subclass dari entity — sehingga class entity tidak boleh final. Dirty checking juga butuh field yang bisa berubah.
Record kebalikan dari semua itu: final, field-nya final, dan hanya punya constructor kanonik. Karena itu record tidak bisa jadi entity. Record tetap sangat berguna di sekitar JPA: sebagai DTO request/response (M07), hasil projection query, atau @Embeddable (value object seperti alamat) yang didukung Jakarta Persistence 3.2.
Persistence context dan dirty checking
EntityManager (di Hibernate: Session) menyimpan persistence context — semacam Map dari ID ke instance entity selama satu transaksi. Konsekuensinya:
- Satu baris = satu object.
finddua kali dengan ID yang sama di transaksi yang sama mengembalikan instance yang sama, dan query hanya dijalankan sekali. - Dirty checking. Hibernate menyimpan snapshot state saat entity dimuat. Saat flush (otomatis menjelang commit, atau sebelum query yang terdampak), setiap entity managed yang berubah dibuatkan
UPDATE. Kamu tidak perlu memanggilsave.
Lifecycle entity
| Status | Artinya | Masuk lewat |
|---|---|---|
| transient | object Java biasa, belum dikenal persistence context | new Product(...) |
| managed | dipantau persistence context; perubahan otomatis disimpan | persist, find, hasil query |
| detached | pernah managed, tetapi persistence context sudah ditutup | transaksi selesai, detach, clear |
| removed | dijadwalkan dihapus saat flush | remove |
Mengubah entity detached tidak berefek apa pun ke database. merge(detached) menyalin state-nya ke instance managed dan mengembalikan instance managed itu — object yang kamu kirim tetap detached.
equals dan hashCode entity
Di M03 kamu belajar HashSet memakai hashCode untuk memilih bucket. Entity punya jebakan khusus: ID baru terisi setelah persist. Bila hashCode dihitung dari id, nilainya berubah setelah object masuk ke HashSet:
java
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class EntityHashDemo {
// Versi keliru: hashCode berubah saat id diisi
static class BadProduct {
Long id; // null sampai "disimpan"
final String sku;
BadProduct(String sku) { this.sku = sku; }
@Override public boolean equals(Object o) {
return o instanceof BadProduct other && Objects.equals(id, other.id);
}
@Override public int hashCode() { return Objects.hashCode(id); }
}
// Versi aman: equals berbasis id (bila sudah ada), hashCode konstan
static class Product {
Long id;
final String sku;
Product(String sku) { this.sku = sku; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Product other)) return false;
return id != null && id.equals(other.id);
}
@Override public int hashCode() { return Product.class.hashCode(); }
}
public static void main(String[] args) {
Set<BadProduct> bad = new HashSet<>();
BadProduct kopi = new BadProduct("KOPI-250");
bad.add(kopi);
kopi.id = 1L; // seolah-olah database memberi id setelah persist
System.out.println("bad.contains = " + bad.contains(kopi));
Set<Product> good = new HashSet<>();
Product teh = new Product("TEH-100");
good.add(teh);
teh.id = 2L;
System.out.println("good.contains = " + good.contains(teh));
System.out.println(new Product("A").equals(new Product("B"))); // dua entity baru: tidak sama
}
}Output:
text
bad.contains = false
good.contains = true
falsePola aman: equals membandingkan ID hanya bila sudah terisi, hashCode konstan per class. Pakai instanceof, bukan getClass() ==, karena proxy Hibernate adalah subclass. Di entity sungguhan, bandingkan lewat getter (other.getId()), sebab field milik proxy tidak terisi. Alternatif lain: natural key yang unik dan tidak pernah berubah, misalnya sku. Jangan memakai @Data Lombok atau equals berbasis semua field pada entity.
ddl-auto hanya untuk belajar
spring.jpa.hibernate.ddl-auto membuat Hibernate mengelola skema: create, create-drop, update, validate, atau none. update enak untuk eksperimen, tetapi tidak pernah menghapus kolom, tidak bisa mengganti nama kolom, dan tidak meninggalkan riwayat perubahan. Di aplikasi sungguhan skema dikelola dengan migrasi (Flyway, bab 5) dan ddl-auto disetel validate atau none.
Contoh di dunia backend
Service katalog memakai EntityManager langsung agar lifecycle terlihat jelas. (Di bab Spring Data kamu akan memakai repository yang lebih ringkas; @Transactional dibahas tuntas di bab transaksi — untuk sekarang anggap ia membuka transaksi di awal method dan commit di akhir.)
java
// Potongan — package id.jalurjava.catalog
import jakarta.persistence.EntityManager;
import java.math.BigDecimal;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductService {
private final EntityManager em; // constructor injection seperti di M07
public ProductService(EntityManager em) {
this.em = em;
}
@Transactional
public Long create(String sku, String name, BigDecimal price) {
Product product = new Product(sku, name, price); // transient
em.persist(product); // managed; IDENTITY → INSERT langsung, id terisi
return product.getId();
}
@Transactional
public void changePrice(Long id, BigDecimal newPrice) {
Product product = em.find(Product.class, id); // managed
if (product == null) {
throw new ProductNotFoundException(id); // dipetakan ke 404 oleh @RestControllerAdvice (M07)
}
product.changePrice(newPrice); // tidak ada save/update
} // commit → flush → UPDATE products ...
}Dengan show-sql: true, changePrice menghasilkan kira-kira (disederhanakan):
sql
select p.id, p.created_at, p.name, p.price, p.sku, p.status from products p where p.id=?
update products set name=?, price=?, sku=?, status=? where id=?Secara default Hibernate meng-update semua kolom yang boleh di-update, bukan hanya yang berubah — created_at tidak ikut karena updatable = false. Perhatikan juga bahwa controller sebaiknya menerima dan mengembalikan DTO record, bukan entity: entity adalah detail persistence, dan mengembalikannya langsung membocorkan kolom internal ke klien.
Pertanyaan interview
1. Apa beda JPA, Hibernate, dan Spring Data JPA?
JPA (Jakarta Persistence) adalah spesifikasi: anotasi dan API EntityManager. Hibernate adalah implementasi yang membuat SQL dan mengelola persistence context. Spring Data JPA berada di atasnya dan menyediakan repository, query method, dan paginasi tanpa menulis implementasi. Spring Boot memakai Hibernate sebagai implementasi default.
2. Apa itu persistence context dan dirty checking?
Persistence context adalah kumpulan entity managed dalam satu EntityManager/transaksi, diindeks berdasarkan ID. Ia menjamin satu baris diwakili satu instance. Saat flush, Hibernate membandingkan state entity managed dengan snapshot saat dimuat dan menghasilkan UPDATE untuk yang berubah — karena itu entity managed tidak perlu "disimpan ulang".
3. Kenapa @Enumerated(EnumType.STRING) lebih disarankan daripada default-nya?
Default-nya ORDINAL menyimpan posisi konstanta. Menyisipkan atau mengurutkan ulang konstanta mengubah arti data lama tanpa error apa pun. STRING menyimpan nama konstanta: sedikit lebih boros ruang, tetapi aman terhadap perubahan urutan dan mudah dibaca saat query manual. Mengganti nama konstanta tetap butuh migrasi data.
Jebakan umum
@EnumeratedtanpaEnumType.STRING. Data enum rusak diam-diam saat enum diubah urutannya.- Uang memakai
double. PakaiBigDecimaldenganprecision/scaleyang jelas. hashCodeberbasis ID yang dibuat database atauequalsberbasis semua field — entity "hilang" dariHashSet.- Mengubah entity detached lalu heran tidak tersimpan, atau sebaliknya kaget entity managed ter-update tanpa
save. Selalu tahu status entity yang sedang kamu pegang. - Membuat entity
finalatau tanpa constructor no-arg — proxy dan pembacaan data gagal. ddl-auto: updatedi production. Skema tanpa riwayat dan perubahan berisiko; pakai migrasi.
Ringkasan
- Jakarta Persistence adalah spesifikasi, Hibernate 7 implementasinya, Spring Data JPA lapisan repository di atasnya.
- Entity:
@Entity,@IdbertipeLong, constructor no-argprotected, tidakfinal; record untuk DTO, bukan entity. - Pakai
BigDecimaluntuk uang,Instantuntuk waktu, dan@Enumerated(EnumType.STRING)untuk enum. IDENTITYsederhana;SEQUENCEmendukung pre-alokasi ID dan batch insert.- Persistence context menjamin satu instance per baris; dirty checking menyimpan perubahan entity managed saat flush.
equalsberbasis ID yang sudah terisi +hashCodekonstan;ddl-autohanya untuk belajar.