Alur pendaftaran pesanan menyimpan tiga hal sekaligus: keranjang, mutasi stok, dan baris audit. Bila ketiga simpan() tersebut dijalankan satu per satu dan yang kedua gagal, database kita berada di keadaan setengah jadi — keranjang tersimpan tanpa catatan stok. Martin Fowler mendefinisikan unit of work sebagai objek yang “menjaga daftar objek yang terpengaruh oleh satu transaksi bisnis dan mengoordinasikan penulisan perubahan serta penanganan masalah konkurensi”. Sederhananya: kumpulkan seluruh perubahan, eksekusi sebagai satu paket, dan pastikan paket itu menempel utuh atau batal utuh. Chapter ini membangun versi mininya yang bisa dijalankan, lalu menghubungkannya dengan repository dan transaksi JDBC.

Semua atau Tidak Sama Sekali #

Jaminan “semua atau tidak sama sekali” adalah inti transaksi database — properti atomicity dalam istilah ACID. Tanpa unit kerja, kode domain memanggil simpan() berulang kali dan setiap panggilan langsung menulis ke media simpan; tidak ada momen untuk berkata “batal semuanya” ketika baris ketiga ditolak. Pola ini memindahkan momen eksekusi ke belakang: pemanggil hanya menjadwalkan, dan satu method commit() yang mengeksekusi seluruh jadwal terurut dalam satu wadah transaksi.

Ketika satu aksi gagal di tengah eksekusi, commit() membatalkan efek aksi-aksi sebelumnya — rollback — lalu melaporkan penyebab kegagalannya. Pemanggil tidak perlu tahu aksi mana yang tersimpan atau belum; keadaan media simpan hanya punya dua kemungkinan yang bersih: sebelum paket, atau sesudah paket menempel utuh.

Unit Kerja Mini dalam Kode #

Program berikut memodelkan unit kerja sebagai daftar aksi terjadwal. Skenario pertama berhasil; skenario kedua sengaja menempatkan aksi yang gagal di tengah untuk memperlihatkan pembatalan:

java UnitKerja.java
import java.util.ArrayList;
import java.util.List;

public class UnitKerja {
    public static void main(String[] args) {
        // Skenario sukses: tiga aksi dieksekusi berurutan dalam satu commit
        UnitKerjaDb unit = new UnitKerjaDb();
        unit.jadwalInsert("Buku Java", 85_000);
        unit.jadwalUpdate("Pena", 12_500);
        unit.jadwalHapus("Penghapus");
        unit.commit();

        // Skenario gagal: aksi kedua melempar galat, aksi pertama dibatalkan
        UnitKerjaDb unitGagal = new UnitKerjaDb();
        unitGagal.jadwalInsert("Tas", 250_000);
        unitGagal.jadwalHapus("ProdukRusak"); // sengaja gagal saat dieksekusi
        unitGagal.commit();
    }
}

// Satu aksi perubahan data, dieksekusi belakangan oleh commit()
interface AksiDb {
    String jalankan();
}

class Insert implements AksiDb {
    private final String nama;
    private final long harga;

    Insert(String nama, long harga) {
        this.nama = nama;
        this.harga = harga;
    }

    @Override
    public String jalankan() {
        return "INSERT produk (" + nama + ", Rp" + harga + ")";
    }
}

class Update implements AksiDb {
    private final String nama;
    private final long hargaBaru;

    Update(String nama, long hargaBaru) {
        this.nama = nama;
        this.hargaBaru = hargaBaru;
    }

    @Override
    public String jalankan() {
        return "UPDATE produk SET harga=" + hargaBaru + " WHERE nama=" + nama;
    }
}

class Hapus implements AksiDb {
    private final String nama;

    Hapus(String nama) {
        this.nama = nama;
    }

    @Override
    public String jalankan() {
        if (nama.startsWith("ProdukRusak")) {
            throw new IllegalStateException("produk tidak ditemukan: " + nama);
        }
        return "DELETE produk WHERE nama=" + nama;
    }
}

// Unit of Work: mengumpulkan perubahan lalu mengeksekusinya sebagai satu kesatuan
class UnitKerjaDb {
    private final List<AksiDb> aksiTerjadwal = new ArrayList<>();

    void jadwalInsert(String nama, long harga) {
        aksiTerjadwal.add(new Insert(nama, harga));
    }

    void jadwalUpdate(String nama, long hargaBaru) {
        aksiTerjadwal.add(new Update(nama, hargaBaru));
    }

    void jadwalHapus(String nama) {
        aksiTerjadwal.add(new Hapus(nama));
    }

    void commit() {
        List<String> sudahJalan = new ArrayList<>();
        System.out.println("== commit: " + aksiTerjadwal.size() + " aksi terjadwal ==");
        try {
            for (AksiDb aksi : aksiTerjadwal) {
                String hasil = aksi.jalankan();
                sudahJalan.add(hasil);
                System.out.println("  sukses: " + hasil);
            }
            aksiTerjadwal.clear();
            System.out.println("  commit selesai: " + sudahJalan.size() + " aksi menempel");
        } catch (RuntimeException e) {
            aksiTerjadwal.clear();
            System.out.println("  gagal pada aksi ke-" + (sudahJalan.size() + 1)
                    + ", batalkan " + sudahJalan.size() + " aksi sebelumnya");
            System.out.println("  penyebab: " + e.getMessage());
        }
    }
}

Output yang Dihasilkan:

text
== commit: 3 aksi terjadwal ==
  sukses: INSERT produk (Buku Java, Rp85000)
  sukses: UPDATE produk SET harga=12500 WHERE nama=Pena
  sukses: DELETE produk WHERE nama=Penghapus
  commit selesai: 3 aksi menempel
== commit: 2 aksi terjadwal ==
  sukses: INSERT produk (Tas, Rp250000)
  gagal pada aksi ke-2, batalkan 1 aksi sebelumnya
  penyebab: produk tidak ditemukan: ProdukRusak

Mari kita bedah mekanismenya:

  1. Baris 22-24 (interface AksiDb): setiap perubahan dikemas sebagai objek aksi dengan satu method jalankan(). Pengemasan ini berkerabat dengan command pattern dan membuat unit kerja tidak perlu tahu isi aksinya — cukup cara mengeksekusinya.
  2. Baris 73-86 (class UnitKerjaDb, tiga method jadwal...): pemanggil hanya menjadwalkan; tidak ada satu pun penulisan pada tahap ini. Urutan jadwal adalah urutan eksekusi, sehingga perilaku program dapat diprediksi dan diuji.
  3. Baris 88-107 (commit): satu-satunya titik eksekusi. Seluruh aksi dijalankan terurut; begitu satu aksi melempar RuntimeException, blok catch melaporkan posisi kegagalan, jumlah aksi yang harus dibatalkan, dan penyebabnya — lalu jadwal dikosongkan agar paket yang sama tidak dieksekusi dua kali.
  4. Baris 56-71 (class Hapus): aksi uji yang sengaja melempar IllegalStateException untuk nama tertentu. Pada implementasi JDBC sungguhan, kegagalan serupa muncul dari pelanggaran kendala atau koneksi terputus, dan blok catch tadi adalah tempat rollback() dipanggil.

Bersama Repository dan JDBC #

Unit kerja dan repository saling melengkapi: repository menyediakan kosakata per aggregate, sedangkan unit kerja memastikan perubahan pada beberapa aggregate menempel dalam satu transaksi. Pola yang umum adalah repository menerima unit kerja dan menyerahkan perubahan ke sana, alih-alih langsung menulis; pemanggil menutup alur dengan satu commit().

Di lapisan JDBC, primitifnya sudah tersedia: connection.setAutoCommit(false) mematikan penulisan otomatis per statement, connection.commit() mengeksekusi seluruh batch, dan connection.rollback() membatalkannya — pembahasan lengkapnya ada di chapter Transaksi dengan JDBC dan Batch Processing pada Section Database Programming. Di dunia ORM seperti JPA, EntityManager adalah unit kerja yang siap pakai: method persist dan merge hanya melacak, dan getTransaction().commit() yang menuliskan seluruh perubahan sekaligus.

Satu pola penyimpanan tersisa yang bukan pola teknis, melainkan pola berpikir: sebelum arsitektur dipilih, bagaimana cara memilihnya? Chapter penutup section ini merangkum seluruh perjalanan kita menjadi kerangka keputusan.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

UnitKerja.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class UnitKerja {
    public static void main(String[] args) {
        // Skenario sukses: tiga aksi dieksekusi berurutan dalam satu commit
        UnitKerjaDb unit = new UnitKerjaDb();
        unit.jadwalInsert("Buku Java", 85_000);
        unit.jadwalUpdate("Pena", 12_500);
        unit.jadwalHapus("Penghapus");
        unit.commit();

        // Skenario gagal: aksi kedua melempar galat, aksi pertama dibatalkan
        UnitKerjaDb unitGagal = new UnitKerjaDb();
        unitGagal.jadwalInsert("Tas", 250_000);
        unitGagal.jadwalHapus("ProdukRusak"); // sengaja gagal saat dieksekusi
        unitGagal.commit();
    }
}

// Satu aksi perubahan data, dieksekusi belakangan oleh commit()
interface AksiDb {
    String jalankan();
}

class Insert implements AksiDb {
    private final String nama;
    private final long harga;

    Insert(String nama, long harga) {
        this.nama = nama;
        this.harga = harga;
    }

    @Override
    public String jalankan() {
        return "INSERT produk (" + nama + ", Rp" + harga + ")";
    }
}

class Update implements AksiDb {
    private final String nama;
    private final long hargaBaru;

    Update(String nama, long hargaBaru) {
        this.nama = nama;
        this.hargaBaru = hargaBaru;
    }

    @Override
    public String jalankan() {
        return "UPDATE produk SET harga=" + hargaBaru + " WHERE nama=" + nama;
    }
}

class Hapus implements AksiDb {
    private final String nama;

    Hapus(String nama) {
        this.nama = nama;
    }

    @Override
    public String jalankan() {
        if (nama.startsWith("ProdukRusak")) {
            throw new IllegalStateException("produk tidak ditemukan: " + nama);
        }
        return "DELETE produk WHERE nama=" + nama;
    }
}

// Unit of Work: mengumpulkan perubahan lalu mengeksekusinya sebagai satu kesatuan
class UnitKerjaDb {
    private final List<AksiDb> aksiTerjadwal = new ArrayList<>();

    void jadwalInsert(String nama, long harga) {
        aksiTerjadwal.add(new Insert(nama, harga));
    }

    void jadwalUpdate(String nama, long hargaBaru) {
        aksiTerjadwal.add(new Update(nama, hargaBaru));
    }

    void jadwalHapus(String nama) {
        aksiTerjadwal.add(new Hapus(nama));
    }

    void commit() {
        List<String> sudahJalan = new ArrayList<>();
        System.out.println("== commit: " + aksiTerjadwal.size() + " aksi terjadwal ==");
        try {
            for (AksiDb aksi : aksiTerjadwal) {
                String hasil = aksi.jalankan();
                sudahJalan.add(hasil);
                System.out.println("  sukses: " + hasil);
            }
            aksiTerjadwal.clear();
            System.out.println("  commit selesai: " + sudahJalan.size() + " aksi menempel");
        } catch (RuntimeException e) {
            aksiTerjadwal.clear();
            System.out.println("  gagal pada aksi ke-" + (sudahJalan.size() + 1)
                    + ", batalkan " + sudahJalan.size() + " aksi sebelumnya");
            System.out.println("  penyebab: " + e.getMessage());
        }
    }
}