Hibernate

Hibernate #

Menulis SQL yang benar untuk setiap operasi CRUD di aplikasi enterprise adalah pekerjaan yang repetitif, rawan error, dan membuat kode bisnis terkubur di antara string SQL dan boilerplate JDBC. Hibernate adalah solusi ORM (Object-Relational Mapping) paling matang di ekosistem Java — ia memetakan objek Java ke tabel database secara transparan, mengelola koneksi, menangani transaksi, dan menghasilkan SQL yang dioptimalkan. Developer bisa fokus menulis logika bisnis dalam bahasa objek yang familiar tanpa harus memikirkan detail SQL untuk setiap operasi. Tapi Hibernate bukan magic — ia punya model mental yang perlu dipahami dengan baik. Konsep seperti persistence context, lazy loading, dirty checking, dan N+1 query adalah jebakan yang sering menyebabkan bug halus dan masalah performa di production. Memahami cara kerja Hibernate di bawah kapak adalah kunci untuk menggunakannya secara efektif, bukan hanya mengetahui cara memakai anotasinya.

Arsitektur Hibernate #

Sebelum menulis kode, pahami dua konsep inti yang mengontrol segalanya di Hibernate: SessionFactory dan Session.

SessionFactory dan Session #

SessionFactory adalah objek berat yang dibuat sekali saat aplikasi startup. Ia menyimpan semua metadata mapping, konfigurasi connection pool, dan cache level kedua. Satu aplikasi biasanya punya satu SessionFactory.

Session adalah objek ringan yang merepresentasikan satu unit of work — satu percakapan antara aplikasi dan database. Ia mengelola persistence context: kumpulan entity yang sedang “diawasi” oleh Hibernate dalam satu sesi.

flowchart TD
    APP[Aplikasi Java]

    subgraph SF[SessionFactory — dibuat sekali saat startup]
        META[Metadata Mapping\nentity → tabel]
        POOL[Connection Pool\nHikariCP]
        L2[Second-Level Cache\nEhCache / Redis]
    end

    subgraph S[Session — per unit of work]
        PC[Persistence Context\nidentity map]
        L1[First-Level Cache\nper session]
    end

    APP -->|buka session| S
    SF -->|menyediakan| S
    S -->|query / flush| DB[(Database)]

Persistence Context dan Entity States #

Ini adalah konsep paling penting di Hibernate. Setiap entity yang kamu bekerja dengannya berada dalam salah satu dari empat state:

stateDiagram-v2
    [*] --> Transient: new Produk()

    Transient --> Persistent: session.persist() / session.save()
    Persistent --> Detached: session.close() / session.evict()
    Persistent --> Removed: session.remove() / session.delete()
    Detached --> Persistent: session.merge()
    Removed --> [*]: flush() + commit()

    note right of Persistent: Hibernate mengawasi perubahan\n(dirty checking)\nSQL otomatis saat flush
    note right of Detached: Tidak diawasi\nperubahan tidak otomatis tersimpan
    note right of Transient: Tidak terhubung\nke database sama sekali
// Transient — objek baru, tidak dikenal Hibernate
Produk produk = new Produk();
produk.setNama("Laptop");

// Persistent — dikelola oleh persistence context
session.persist(produk);
produk.setHarga(new BigDecimal("15000000")); // perubahan ini OTOMATIS tersimpan saat flush!

// session.flush() + session.getTransaction().commit()
// Hibernate mendeteksi perubahan harga dan mengeksekusi UPDATE secara otomatis

// Detached — setelah session ditutup
session.close();
produk.setStok(10); // perubahan ini TIDAK tersimpan — tidak ada yang mengawasi

// Persistent lagi — merge() mengembalikan ke managed state
Session sesiBaru = sessionFactory.openSession();
Produk managed = sesiBaru.merge(produk); // SQL UPDATE dieksekusi

Setup Dependencies #

<!-- Maven — Hibernate ORM standalone (tanpa Spring) -->
<dependencies>
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>6.5.2.Final</version>
    </dependency>

    <!-- Driver database PostgreSQL -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.3</version>
    </dependency>

    <!-- Connection pool HikariCP -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.1.0</version>
    </dependency>
</dependencies>

File konfigurasi src/main/resources/META-INF/persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0"
    xmlns="https://jakarta.ee/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <persistence-unit name="tokoonline" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <!-- Daftarkan semua entity class -->
        <class>com.contoh.model.Produk</class>
        <class>com.contoh.model.Kategori</class>
        <class>com.contoh.model.Pesanan</class>

        <properties>
            <!-- Koneksi database -->
            <property name="jakarta.persistence.jdbc.driver"
                      value="org.postgresql.Driver"/>
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:postgresql://localhost:5432/tokoonline"/>
            <property name="jakarta.persistence.jdbc.user" value="postgres"/>
            <property name="jakarta.persistence.jdbc.password" value="rahasia"/>

            <!-- Hibernate settings -->
            <property name="hibernate.dialect"
                      value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="validate"/>
            <!-- none | validate | update | create | create-drop -->

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>

            <!-- Connection pool via HikariCP -->
            <property name="hibernate.hikari.maximumPoolSize" value="10"/>
            <property name="hibernate.hikari.minimumIdle" value="2"/>
            <property name="hibernate.hikari.connectionTimeout" value="30000"/>

            <!-- Batch operations -->
            <property name="hibernate.jdbc.batch_size" value="25"/>
            <property name="hibernate.order_inserts" value="true"/>
            <property name="hibernate.order_updates" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Entity Mapping #

Entity Dasar #

package com.contoh.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "produk",
       indexes = {
           @Index(name = "idx_produk_nama", columnList = "nama"),
           @Index(name = "idx_produk_kategori", columnList = "kategori_id")
       })
public class Produk {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = "produk_seq")
    @SequenceGenerator(name = "produk_seq",
                       sequenceName = "produk_id_seq",
                       allocationSize = 50) // ambil 50 ID sekaligus — lebih efisien dari IDENTITY
    private Long id;

    @NotBlank
    @Size(max = 100)
    @Column(nullable = false, length = 100)
    private String nama;

    @Lob // untuk teks panjang
    @Column(columnDefinition = "TEXT")
    private String deskripsi;

    @NotNull
    @DecimalMin("0.01")
    @Column(nullable = false, precision = 15, scale = 2)
    private BigDecimal harga;

    @Min(0)
    @Column(nullable = false)
    private int stok;

    @Column(nullable = false)
    private boolean aktif = true;

    // Enum mapping
    @Enumerated(EnumType.STRING) // simpan sebagai string, bukan angka
    @Column(nullable = false, length = 20)
    private StatusProduk status = StatusProduk.DRAFT;

    // Timestamp otomatis
    @Column(name = "dibuat_pada", nullable = false, updatable = false)
    private LocalDateTime dibuatPada;

    @Column(name = "diubah_pada")
    private LocalDateTime diubahPada;

    @PrePersist
    protected void onCreate() {
        dibuatPada = LocalDateTime.now();
        diubahPada = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        diubahPada = LocalDateTime.now();
    }

    // Getter dan setter
    public Long getId() { return id; }
    public String getNama() { return nama; }
    public void setNama(String nama) { this.nama = nama; }
    public BigDecimal getHarga() { return harga; }
    public void setHarga(BigDecimal harga) { this.harga = harga; }
    public int getStok() { return stok; }
    public void setStok(int stok) { this.stok = stok; }
    public boolean isAktif() { return aktif; }
    public void setAktif(boolean aktif) { this.aktif = aktif; }
    public StatusProduk getStatus() { return status; }
    public void setStatus(StatusProduk status) { this.status = status; }
    public LocalDateTime getDibuatPada() { return dibuatPada; }
    public LocalDateTime getDiubahPada() { return diubahPada; }
}

public enum StatusProduk {
    DRAFT, AKTIF, NONAKTIF, DIHAPUS
}

Embedded dan Embeddable #

Untuk mengelompokkan field yang secara logis bersama tanpa membuat tabel terpisah:

// @Embeddable — objek yang di-embed ke dalam entity lain
@Embeddable
public class Alamat {

    @Column(nullable = false, length = 200)
    private String jalan;

    @Column(nullable = false, length = 50)
    private String kota;

    @Column(nullable = false, length = 10)
    private String kodePos;

    @Column(nullable = false, length = 50)
    private String provinsi;

    // getter setter...
}

@Entity
@Table(name = "pelanggan")
public class Pelanggan {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String nama;

    // @Embedded — gunakan @AttributeOverrides jika nama kolom perlu diubah
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "jalan", column = @Column(name = "alamat_jalan")),
        @AttributeOverride(name = "kota", column = @Column(name = "alamat_kota")),
        @AttributeOverride(name = "kodePos", column = @Column(name = "alamat_kode_pos")),
        @AttributeOverride(name = "provinsi", column = @Column(name = "alamat_provinsi"))
    })
    private Alamat alamatPengiriman;

    // getter setter...
}

Relasi Antar Entity #

OneToMany dan ManyToOne #

Relasi paling umum: satu Kategori punya banyak Produk, setiap Produk milik satu Kategori.

@Entity
@Table(name = "kategori")
public class Kategori {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String nama;

    // mappedBy mengacu pada field di sisi ManyToOne
    // cascade: operasi pada Kategori diteruskan ke Produk-nya
    // orphanRemoval: Produk yang dilepas dari Kategori otomatis dihapus dari DB
    @OneToMany(mappedBy = "kategori",
               cascade = CascadeType.ALL,
               orphanRemoval = true,
               fetch = FetchType.LAZY) // LAZY = default untuk koleksi, TIDAK load saat query Kategori
    private java.util.List<Produk> produk = new java.util.ArrayList<>();

    // Helper method untuk menjaga konsistensi dua sisi relasi
    public void tambahProduk(Produk produk) {
        this.produk.add(produk);
        produk.setKategori(this); // juga set sisi ManyToOne
    }

    public void hapusProduk(Produk produk) {
        this.produk.remove(produk);
        produk.setKategori(null);
    }

    public Long getId() { return id; }
    public String getNama() { return nama; }
    public void setNama(String nama) { this.nama = nama; }
    public java.util.List<Produk> getProduk() { return produk; }
}

// Tambahkan ke entity Produk:
@ManyToOne(fetch = FetchType.LAZY) // LAZY = default untuk single entity
@JoinColumn(name = "kategori_id", nullable = false)
private Kategori kategori;

public Kategori getKategori() { return kategori; }
public void setKategori(Kategori kategori) { this.kategori = kategori; }

ManyToMany #

Relasi banyak-ke-banyak: satu Pesanan bisa punya banyak Produk, satu Produk bisa ada di banyak Pesanan.

@Entity
@Table(name = "pesanan")
public class Pesanan {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private java.time.LocalDateTime tanggalPesan;

    @Column(nullable = false, precision = 15, scale = 2)
    private BigDecimal total;

    // Pemilik relasi (yang punya join table)
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "pesanan_produk",               // nama tabel join
        joinColumns = @JoinColumn(name = "pesanan_id"),
        inverseJoinColumns = @JoinColumn(name = "produk_id")
    )
    private java.util.Set<Produk> produk = new java.util.HashSet<>();
    // Gunakan Set, bukan List, untuk ManyToMany — hindari duplikat dan masalah performa

    public void tambahProduk(Produk p) { produk.add(p); }
    public void hapusProduk(Produk p) { produk.remove(p); }

    // getter setter...
}
Untuk ManyToMany yang butuh menyimpan data tambahan di join table (seperti jumlah item atau harga saat dibeli), jangan gunakan @ManyToMany langsung. Buat entity perantara ItemPesanan dengan relasi @ManyToOne ke Pesanan dan Produk. Ini jauh lebih fleksibel dan mudah di-query.

OneToOne #

@Entity
@Table(name = "pengguna")
public class Pengguna {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    // OneToOne — profil dimuat terpisah (LAZY)
    @OneToOne(mappedBy = "pengguna",
              cascade = CascadeType.ALL,
              fetch = FetchType.LAZY,
              optional = true)
    private ProfilPengguna profil;

    // getter setter...
}

@Entity
@Table(name = "profil_pengguna")
public class ProfilPengguna {

    @Id
    private Long id; // ID sama dengan Pengguna

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId // gunakan ID dari Pengguna sebagai ID ProfilPengguna
    @JoinColumn(name = "pengguna_id")
    private Pengguna pengguna;

    private String bio;
    private String fotoUrl;

    // getter setter...
}

Querying — JPQL dan Criteria API #

JPQL (Jakarta Persistence Query Language) #

JPQL mirip SQL tapi beroperasi pada entity Java, bukan tabel database.

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;

public class ProdukRepository {

    private final EntityManager em;

    public ProdukRepository(EntityManager em) {
        this.em = em;
    }

    // Query dasar
    public java.util.List<Produk> semuaProdukAktif() {
        return em.createQuery(
            "SELECT p FROM Produk p WHERE p.aktif = true ORDER BY p.nama",
            Produk.class
        ).getResultList();
    }

    // Query dengan parameter — SELALU gunakan named parameter, jangan string concat
    public java.util.Optional<Produk> cariByNama(String nama) {
        // ✗ ANTI-PATTERN: string concatenation — rentan SQL injection
        // em.createQuery("SELECT p FROM Produk p WHERE p.nama = '" + nama + "'");

        // ✓ BENAR: named parameter
        return em.createQuery(
                "SELECT p FROM Produk p WHERE lower(p.nama) = lower(:nama)",
                Produk.class)
            .setParameter("nama", nama)
            .getResultStream()
            .findFirst();
    }

    // Query dengan JOIN FETCH — solusi untuk N+1 problem
    public java.util.List<Produk> semuaDenganKategori() {
        return em.createQuery(
            // JOIN FETCH memuat Kategori sekaligus dalam satu query
            "SELECT p FROM Produk p JOIN FETCH p.kategori k ORDER BY k.nama, p.nama",
            Produk.class
        ).getResultList();
    }

    // Paginasi
    public java.util.List<Produk> denganPaginasi(int halaman, int ukuranHalaman) {
        return em.createQuery("SELECT p FROM Produk p ORDER BY p.id", Produk.class)
            .setFirstResult(halaman * ukuranHalaman)
            .setMaxResults(ukuranHalaman)
            .getResultList();
    }

    // Agregasi
    public long hitungProdukAktif() {
        return em.createQuery(
            "SELECT COUNT(p) FROM Produk p WHERE p.aktif = true",
            Long.class
        ).getSingleResult();
    }

    public java.math.BigDecimal hargaRata() {
        return em.createQuery(
            "SELECT AVG(p.harga) FROM Produk p WHERE p.aktif = true",
            java.math.BigDecimal.class
        ).getSingleResult();
    }

    // Named Query — didefinisikan di entity, bisa di-cache
    // (tambahkan di class Produk: @NamedQuery(name = "Produk.aktif", query = "..."))
    public java.util.List<Produk> semuaAktifViaNamedQuery() {
        return em.createNamedQuery("Produk.aktif", Produk.class).getResultList();
    }

    // Update/Delete bulk — lebih efisien dari load entity satu per satu
    public int nonaktifkanStokHabis() {
        return em.createQuery(
            "UPDATE Produk p SET p.aktif = false WHERE p.stok = 0"
        ).executeUpdate();
    }
}

Criteria API #

Criteria API memungkinkan query type-safe yang dibangun secara programatis — berguna untuk query dinamis di mana kondisi WHERE bervariasi tergantung input.

import jakarta.persistence.criteria.*;

public class ProdukCriteriaRepository {

    private final EntityManager em;

    public ProdukCriteriaRepository(EntityManager em) {
        this.em = em;
    }

    // Query dinamis berdasarkan filter yang tersedia
    public java.util.List<Produk> cariDinamis(String nama, BigDecimal hargaMin,
                                               BigDecimal hargaMaks, Boolean aktif) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Produk> cq = cb.createQuery(Produk.class);
        Root<Produk> root = cq.from(Produk.class);

        java.util.List<Predicate> predicates = new java.util.ArrayList<>();

        // Tambahkan kondisi hanya jika parameter tidak null
        if (nama != null && !nama.isBlank()) {
            predicates.add(cb.like(cb.lower(root.get("nama")),
                "%" + nama.toLowerCase() + "%"));
        }
        if (hargaMin != null) {
            predicates.add(cb.greaterThanOrEqualTo(root.get("harga"), hargaMin));
        }
        if (hargaMaks != null) {
            predicates.add(cb.lessThanOrEqualTo(root.get("harga"), hargaMaks));
        }
        if (aktif != null) {
            predicates.add(cb.equal(root.get("aktif"), aktif));
        }

        cq.where(predicates.toArray(new Predicate[0]));
        cq.orderBy(cb.asc(root.get("nama")));

        return em.createQuery(cq).getResultList();
    }
}

Masalah N+1 Query #

N+1 adalah masalah performa paling umum di Hibernate. Ia terjadi ketika kamu memuat N entitas, lalu untuk setiap entity melakukan 1 query tambahan untuk memuat relasi — total N+1 query ke database.

public void contohN1Problem(EntityManager em) {

    // ✗ ANTI-PATTERN: N+1 problem
    // Query 1: SELECT * FROM kategori → hasilkan 50 kategori
    java.util.List<Kategori> semuaKategori = em.createQuery(
        "SELECT k FROM Kategori k", Kategori.class
    ).getResultList();

    for (Kategori k : semuaKategori) {
        // Query 2..51: SELECT * FROM produk WHERE kategori_id = ?
        // (lazy loading memicu query baru untuk SETIAP kategori)
        System.out.println(k.getNama() + ": " + k.getProduk().size() + " produk");
    }
    // Total: 1 + 50 = 51 query!
}

public void solusiN1JoinFetch(EntityManager em) {

    // ✓ SOLUSI 1: JOIN FETCH — muat semua sekaligus dalam 1 query
    java.util.List<Kategori> kategori = em.createQuery(
        "SELECT DISTINCT k FROM Kategori k LEFT JOIN FETCH k.produk",
        Kategori.class
    ).getResultList();
    // Total: 1 query dengan JOIN

    for (Kategori k : kategori) {
        // produk sudah dimuat — tidak ada query tambahan
        System.out.println(k.getNama() + ": " + k.getProduk().size() + " produk");
    }
}

public void solusiN1EntityGraph(EntityManager em) {

    // ✓ SOLUSI 2: @EntityGraph — lebih fleksibel dari JOIN FETCH
    jakarta.persistence.EntityGraph<Kategori> graph =
        em.createEntityGraph(Kategori.class);
    graph.addAttributeNodes("produk"); // muat atribut "produk" sekaligus

    java.util.List<Kategori> kategori = em.createQuery(
        "SELECT k FROM Kategori k", Kategori.class)
        .setHint("jakarta.persistence.loadgraph", graph)
        .getResultList();
}

public void solusiN1BatchSize(EntityManager em) {
    // ✓ SOLUSI 3: @BatchSize di entity — load dalam batch, bukan satu per satu
    // Tambahkan di entity: @BatchSize(size = 20) di atas koleksi
    // Hibernate akan load 20 kategori sekaligus saat lazy loading terpicu
    // Mengurangi dari N+1 menjadi N/20 + 1 query
}
flowchart TD
    subgraph N1[Masalah N+1]
        Q1[Query 1: SELECT kategori] --> R1[50 kategori]
        R1 --> Q2[Query 2: produk kategori-1]
        R1 --> Q3[Query 3: produk kategori-2]
        R1 --> QN[... Query 51: produk kategori-50]
        style Q2 fill:#fee2e2
        style Q3 fill:#fee2e2
        style QN fill:#fee2e2
    end

    subgraph SOLVED[Solusi JOIN FETCH]
        QJ[Query 1: SELECT k JOIN FETCH k.produk] --> RJ[50 kategori\n+ semua produk]
        style QJ fill:#dcfce7
    end

Transaksi dan Session Management #

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

public class TransaksiDemo {

    private final EntityManagerFactory emf;

    public TransaksiDemo() {
        this.emf = Persistence.createEntityManagerFactory("tokoonline");
    }

    // Pola standar untuk operasi write dengan transaksi
    public <T> T jalankanDalamTransaksi(java.util.function.Function<EntityManager, T> operasi) {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();
            T hasil = operasi.apply(em);
            tx.commit();
            return hasil;
        } catch (Exception e) {
            if (tx.isActive()) {
                tx.rollback();
            }
            throw new RuntimeException("Transaksi gagal: " + e.getMessage(), e);
        } finally {
            em.close(); // WAJIB — jangan bocorkan EntityManager
        }
    }

    // Contoh penggunaan
    public Produk simpanProduk(String nama, BigDecimal harga) {
        return jalankanDalamTransaksi(em -> {
            Produk produk = new Produk();
            produk.setNama(nama);
            produk.setHarga(harga);
            em.persist(produk);
            return produk;
        });
    }

    // Transfer stok antar produk — keduanya dalam satu transaksi
    public void transferStok(Long dariId, Long keId, int jumlah) {
        jalankanDalamTransaksi(em -> {
            Produk dari = em.find(Produk.class, dariId);
            Produk ke = em.find(Produk.class, keId);

            if (dari == null || ke == null) {
                throw new IllegalArgumentException("Produk tidak ditemukan");
            }
            if (dari.getStok() < jumlah) {
                throw new IllegalStateException("Stok tidak cukup");
            }

            dari.setStok(dari.getStok() - jumlah);
            ke.setStok(ke.getStok() + jumlah);
            // Tidak perlu em.merge() — entity sudah managed, dirty checking otomatis bekerja

            return null;
        });
    }
}

Second-Level Cache #

Hibernate mendukung dua level cache. First-level cache (per Session) selalu aktif. Second-level cache (lintas Session) membutuhkan provider seperti Ehcache atau Redis:

<!-- Tambahkan ke pom.xml -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jcache</artifactId>
    <version>6.5.2.Final</version>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
    <classifier>jakarta</classifier>
</dependency>
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

// Aktifkan second-level cache untuk entity ini
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
// READ_ONLY     → untuk data yang tidak pernah berubah (paling cepat)
// READ_WRITE    → untuk data yang sesekali berubah (aman, sedikit overhead)
// NONSTRICT_READ_WRITE → data yang jarang berubah, bisa stale sementara
@Table(name = "kategori")
public class Kategori {
    // ...

    @OneToMany(mappedBy = "kategori", fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // cache koleksi juga
    private java.util.List<Produk> produk;
}
# persistence.xml — aktifkan second-level cache
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.use_query_cache" value="true"/>
<property name="hibernate.cache.region.factory_class"
          value="org.hibernate.cache.jcache.JCacheRegionFactory"/>
<property name="hibernate.javax.cache.provider"
          value="org.ehcache.jsr107.EhcacheCachingProvider"/>

Operasi Batch — Insert dan Update Massal #

Untuk menyimpan atau memperbarui ribuan record, lakukan dalam batch agar tidak kehabisan memori:

public void simpanMassal(EntityManager em, java.util.List<Produk> produkBaru) {
    int batchSize = 25; // harus sama dengan hibernate.jdbc.batch_size di config

    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try {
        for (int i = 0; i < produkBaru.size(); i++) {
            em.persist(produkBaru.get(i));

            // Flush dan clear setiap batch — cegah persistence context membengkak
            if ((i + 1) % batchSize == 0) {
                em.flush();  // kirim SQL ke database
                em.clear();  // hapus semua entity dari cache — bebaskan memori
            }
        }
        em.flush(); // flush sisa batch terakhir
        tx.commit();

    } catch (Exception e) {
        if (tx.isActive()) tx.rollback();
        throw e;
    }

    System.out.printf("Berhasil simpan %d produk dalam batch%n", produkBaru.size());
}

Kapan Menggunakan Hibernate dan Kapan Tidak #

GUNAKAN HIBERNATE JIKA:
  ✓ Domain model kompleks dengan banyak relasi antar entity
  ✓ Ingin produktivitas CRUD tanpa menulis SQL manual untuk setiap operasi
  ✓ Database schema sering berevolusi — lazy loading dan dirty checking sangat membantu
  ✓ Butuh portabilitas antar database (PostgreSQL, MySQL, Oracle, dll)
  ✓ Sudah pakai Spring Boot atau Quarkus — Hibernate sudah terintegrasi
  ✓ Butuh caching query dan second-level cache tanpa implementasi manual

PERTIMBANGKAN ALTERNATIF JIKA:
  ✗ Query sangat kompleks dan spesifik database → jOOQ atau JDBC langsung lebih tepat
  ✗ Performa insert/update massal kritis → JDBC batch atau COPY PostgreSQL lebih cepat
  ✗ Schema sudah sangat terkontrol dan stabil → JDBC template lebih predictable
  ✗ Tim lebih nyaman dengan SQL daripada JPQL → jOOQ menawarkan SQL type-safe
  ✗ Microservice kecil dengan satu atau dua tabel → overhead Hibernate tidak sepadan

Ringkasan #

  • Persistence context adalah jantung Hibernate — entity yang dimuat dalam satu Session otomatis diawasi. Perubahan pada field entity yang managed akan otomatis menghasilkan SQL UPDATE saat flush, tanpa perlu memanggil save() atau update() secara eksplisit.
  • Entity states yang harus dipahami: Transient (baru, belum dikenal Hibernate), Persistent (dalam session, diawasi), Detached (session sudah tutup), dan Removed (ditandai untuk dihapus). Salah mengira state menyebabkan perubahan tidak tersimpan.
  • Lazy loading adalah default yang aman — koleksi (@OneToMany, @ManyToMany) tidak dimuat kecuali diakses. Gunakan JOIN FETCH atau @EntityGraph saat kamu tahu akan mengakses relasi untuk menghindari N+1 query.
  • N+1 problem adalah musuh utama performa Hibernate — selalu deteksi dengan mengaktifkan hibernate.show_sql=true di development dan periksa jumlah query yang dihasilkan per request.
  • Gunakan GenerationType.SEQUENCE dengan allocationSize yang besar (50-100) alih-alih IDENTITY untuk insert massal — IDENTITY memaksa flush per insert, mencegah batching.
  • Batch insert membutuhkan em.flush() + em.clear() setiap N record — tanpa clear, persistence context terus bertumbuh dan akhirnya menyebabkan OutOfMemoryError.
  • Second-level cache sangat efektif untuk data yang jarang berubah (tabel referensi, konfigurasi, master data) — entity yang sama dari request berbeda tidak perlu di-query ulang ke database.
  • Jangan gunakan @ManyToMany untuk join table yang punya atribut tambahan — buat entity perantara dengan dua @ManyToOne, jauh lebih fleksibel dan bisa menyimpan data seperti jumlah item atau harga saat transaksi.

← Sebelumnya: Quarkus   Berikutnya: Selenium →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact