Belajar Spring Boot - Transaction & Persistence Advanced
Episode 10 of 24

Belajar Spring Boot - Transaction & Persistence Advanced

Episode ini memperdalam persistence: manajemen transaksi dengan @Transactional, propagation dan isolation level, lazy loading serta strategi fetch untuk performance JPA, dan migrasi schema dengan Flyway atau Liquibase.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Menyimpan data bukan sekadar memanggil method repository. Data yang saling bergantung harus berubah bersama-sama dalam satu transaksi, query harus efisien agar tidak memperlambat aplikasi, dan schema database harus berevolusi dengan aman. Episode 10 membahas ketiganya.

Kalian akan menguasai @Transactional, memahami propagation dan isolation level, menghindari jebakan lazy loading, serta memakai Flyway dan Liquibase untuk migrasi schema yang terkendali.

Transaction Management dengan @Transactional

Mengapa Transaksi Penting

Transaksi menjamin bahwa rangkaian operasi database berjalan atomic: semua berhasil atau semuanya batal. Contoh klasik adalah transfer uang — pengurangan saldo dan penambahan saldo harus terjadi bersama. Spring mengelola transaksi dengan annotation @Transactional:

Transaksi dengan @Transactional
@Service
public class TransferService {
 
    private final AccountRepository repository;
 
    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = repository.findById(fromId).orElseThrow();
        Account to = repository.findById(toId).orElseThrow();
        from.debit(amount);
        to.credit(amount);
        // jika salah satu gagal, keduanya di-rollback
    }
}

Jika method melempar exception, seluruh perubahan di dalam transaksi di-rollback secara otomatis. Perhatian: @Transactional bekerja lewat proxy, jadi panggilan method antar bean (self-invocation) tidak akan memicu transaksi.

Rollback Behavior

Secara default, rollback terjadi untuk exception runtime yang tidak ter-cek. Untuk exception checked, konfigurasi rollbackFor diperlukan:

Atur rollback untuk exception tertentu
@Transactional(rollbackFor = InvoiceException.class)
public void generateInvoice(Order order) throws InvoiceException {
    // operasi yang bisa melempar InvoiceException
}

Menentukan rollbackFor dengan tepat mencegah transaksi di-commit ketika seharusnya batal — salah satu bug transaction paling umum di aplikasi nyata.

Propagation dan Isolation Level

Propagation

Propagation menentukan bagaimana method berinteraksi dengan transaksi yang sedang berjalan. Nilai yang paling sering dipakai:

  • REQUIRED (default) — ikut transaksi yang ada, atau buat baru jika belum ada.
  • REQUIRES_NEW — selalu buat transaksi baru, menangguhkan yang sedang berjalan.
  • SUPPORTS — ikut jika ada, berjalan tanpa transaksi jika tidak.
Transaksi terpisah
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAudit(AuditEntry entry) {
    auditRepository.save(entry);
}

Contoh di atas memakai REQUIRES_NEW agar catatan audit tetap tersimpan meski transaksi utama di-rollback — pola audit yang umum.

Isolation Level

Isolation mengontrol bagaimana transaksi saling melihat perubahan yang belum di-commit. Empat level standar: READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, dan SERIALIZABLE. Semakin tinggi level, semakin kuat konsistensi namun semakin mahal performa.

Atur isolation per transaksi
@Transactional(isolation = Isolation.READ_COMMITTED)

PostgreSQL dan kebanyakan database modern memakai READ_COMMITTED sebagai default — pilihan yang seimbang. Gunakan level lebih tinggi hanya untuk kasus yang benar-benar membutuhkannya.

Lazy Loading dan Fetch Strategy

LazyLoading dan LazyInitializationException

Relasi antar entity seperti @OneToMany secara default bersifat lazy: data anak baru dimuat saat diakses. Masalah muncul ketika akses dilakukan di luar transaksi — error LazyInitializationException adalah momok developer JPA.

Solusi yang benar: muat data yang dibutuhkan di dalam transaksi dengan fetch = FetchType.EAGER, atau gunakan join query:

Query join untuk menghindari lazy problem
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
 
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);
}

Query JOIN FETCH memuat order beserta item-nya dalam satu perjalanan ke database. Hindari FetchType.EAGER secara global — mudah memicu N+1 query.

Mengatasi Masalah N+1 Query

N+1 problem terjadi ketika satu query untuk daftar, lalu satu query tambahan per baris untuk relasi. Indikasinya: lambat tiba-tiba saat data bertambah. Gunakan JOIN FETCH, batch fetching, atau EntityGraph untuk memperbaiki.

Migrasi Schema dengan Flyway dan Liquibase

Flyway

Flyway mengelola perubahan schema sebagai script SQL yang diberi versi:

Migrasi V1__buat_tabel_items.sql
CREATE TABLE items (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    price NUMERIC(12,2) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

Tambahkan dependency dan letakkan script di src/main/resources/db/migration:

Aktifkan Flyway
./mvnw spring-boot:run

Saat aplikasi start, Flyway menjalankan migrasi yang belum dieksekusi dan mencatatnya di tabel flyway_schema_history. Script tidak boleh diubah setelah diterapkan — buat versi baru untuk perubahan berikutnya. Kapan saja, perintah ./mvnw flyway:info menampilkan versi migrasi mana yang sudah diterapkan dan mana yang belum.

Liquibase

Alternatifnya, Liquibase memakai changelog berbasis YAML atau SQL dengan kontrol yang lebih granular:

Changelog Liquibase
databaseChangeLog:
  - changeSet:
      id: 1-buat-tabel-items
      author: arman
      changes:
        - createTable:
            tableName: items
            columns:
              - column:
                  name: id
                  type: bigint
                  autoIncrement: true

Keduanya menjamin bahwa schema production selalu sinkron dengan kode. Dengan migrasi, ddl-auto di production cukup di-set validate.

Penutup

Episode 10 membekali kalian dengan persistence tingkat lanjut: transaksi dengan @Transactional beserta propagation dan isolation level, strategi fetch untuk menghindari lazy loading dan N+1 query, serta migrasi schema terkendali dengan Flyway atau Liquibase.

Inti yang harus dibawa pulang:

  • @Transactional membuat rangkaian operasi database atomic dengan rollback otomatis.
  • Tentukan rollbackFor untuk exception checked yang harus membatalkan transaksi.
  • Propagation REQUIRES_NEW berguna untuk log audit yang harus tetap tersimpan.
  • Isolation default READ_COMMITTED cocok untuk mayoritas aplikasi.
  • Hindari LazyInitializationException dengan JOIN FETCH di dalam transaksi.
  • Gunakan Flyway atau Liquibase untuk migrasi schema yang aman dan berversi.

Di episode 11 selanjutnya kita akan membahas batch processing dan scheduling — Spring Batch dengan job, step, reader, processor, dan writer; scheduled task dengan @Scheduled dan cron expression; serta eksekusi task asynchronous dengan task executor.