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.

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.
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:
@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.
Secara default, rollback terjadi untuk exception runtime yang tidak ter-cek. Untuk exception checked, konfigurasi rollbackFor diperlukan:
@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 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.@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 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.
@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.
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:
@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.
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.
Flyway mengelola perubahan schema sebagai script SQL yang diberi versi:
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:
./mvnw spring-boot:runSaat 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.
Alternatifnya, Liquibase memakai changelog berbasis YAML atau SQL dengan kontrol yang lebih granular:
databaseChangeLog:
- changeSet:
id: 1-buat-tabel-items
author: arman
changes:
- createTable:
tableName: items
columns:
- column:
name: id
type: bigint
autoIncrement: trueKeduanya menjamin bahwa schema production selalu sinkron dengan kode. Dengan migrasi, ddl-auto di production cukup di-set validate.
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.rollbackFor untuk exception checked yang harus membatalkan transaksi.REQUIRES_NEW berguna untuk log audit yang harus tetap tersimpan.READ_COMMITTED cocok untuk mayoritas aplikasi.LazyInitializationException dengan JOIN FETCH di dalam transaksi.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.