ACID Transactions MongoDB di Laravel
TutorialLaravelPHP#laravel#database#php#backend

ACID Transactions MongoDB di Laravel

A
Abd. Asis
Bagikan:

Bayangkan sebuah aplikasi manajemen proyek freelance: saat klien menyetujui invoice, sistem perlu melakukan tiga hal sekaligus — menandai invoice sebagai lunas, mengurangi saldo kredit klien, dan mencatat entri di buku kas. Jika proses ini terhenti di tengah jalan karena server crash atau timeout, kita berpotensi punya invoice yang sudah "lunas" tapi saldo klien belum berkurang.

Di sinilah multi-document ACID transactions menjadi kritis. Tidak semua database NoSQL mendukungnya, tapi MongoDB mulai versi 4.0 sudah menyediakan jaminan ini — dan dengan paket laravel-mongodb, kita bisa memanfaatkannya lewat API yang familiar.

Mengapa Single-Document Tidak Selalu Cukup

MongoDB sudah lama menjamin atomisitas untuk operasi pada satu dokumen. Kalau kita memperbarui dua field dalam satu dokumen yang sama, tidak ada risiko partial update. Tapi masalah muncul ketika operasi menyentuh beberapa koleksi berbeda secara bersamaan.

Tanpa transaksi, urutan operasi ini rentan terhadap partial failure:

  1. Invoice diperbarui menjadi paid
  2. Proses crash atau network timeout
  3. Saldo klien tidak sempat dikurangi
  4. Buku kas tidak mencatat transaksi

Hasilnya: data inkonsisten yang sulit dideteksi dan lebih sulit lagi diperbaiki secara manual. Untuk operasi bisnis yang melibatkan lebih dari satu koleksi, kita butuh jaminan "semua berhasil atau semua batal" — itulah yang diberikan oleh ACID transactions.

Memahami ACID Transactions di MongoDB

ACID adalah singkatan dari empat properti yang menjamin keandalan transaksi database:

  • Atomicity — semua operasi dalam satu transaksi berhasil bersama-sama, atau tidak ada yang dieksekusi
  • Consistency — database selalu berada dalam state yang valid sebelum dan sesudah transaksi
  • Isolation — transaksi yang berjalan bersamaan tidak saling mempengaruhi hingga salah satunya selesai
  • Durability — perubahan yang sudah di-commit bersifat permanen, bahkan jika server restart

MongoDB mengimplementasikan ACID transactions menggunakan snapshot isolation. Artinya, setiap transaksi bekerja pada snapshot data yang konsisten sejak transaksi dimulai — perubahan dari transaksi lain tidak terlihat sampai transaksi kita selesai.

MongoDB multi-document transactions membutuhkan replica set (minimal satu node). Instalasi standalone tanpa replica set tidak mendukung fitur ini. Untuk pengembangan lokal, gunakan mongod --replSet rs0 atau MongoDB Atlas.

Menyiapkan Laravel dengan MongoDB

Instalasi dan Konfigurasi Koneksi

Instal paket mongodb/laravel-mongodb menggunakan Composer:

BashBASH
composer require mongodb/laravel-mongodb

Tambahkan konfigurasi koneksi MongoDB di config/database.php:

PHPPHP
// config/database.php

'connections' => [

    'mongodb' => [
        'driver'   => 'mongodb',
        'dsn'      => env('MONGODB_URI', 'mongodb://127.0.0.1:27017'),
        'database' => env('MONGODB_DATABASE', 'freelance_app'),
    ],

    // ... koneksi lain

],

Tambahkan variabel di .env:

CodeINI
MONGODB_URI=mongodb://127.0.0.1:27017
MONGODB_DATABASE=freelance_app

Kemudian buat model yang menggunakan koneksi MongoDB dengan cara meng-extend MongoDB\Laravel\Eloquent\Model:

PHPPHP
// app/Models/Invoice.php

namespace App\Models;

use MongoDB\Laravel\Eloquent\Model;

class Invoice extends Model
{
    protected $connection = 'mongodb';
    protected $collection = 'invoices';

    protected $fillable = [
        'client_id',
        'amount',
        'status',
        'paid_at',
    ];
}

Lakukan hal yang sama untuk model ClientCredit dan CashEntry.

Operasi Multi-Dokumen yang Aman dengan DB::transaction

Skenario: Penerbitan Invoice dan Pembaruan Saldo Klien

Setelah model siap, kita bisa membungkus operasi lintas koleksi dalam satu DB::transaction. Semua operasi di dalamnya bersifat atomik — jika satu gagal, semua dibatalkan.

PHPPHP
// app/Actions/MarkInvoiceAsPaidAction.php

namespace App\Actions;

use App\Models\CashEntry;
use App\Models\ClientCredit;
use App\Models\Invoice;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class MarkInvoiceAsPaidAction
{
    public function execute(Invoice $invoice): void
    {
        DB::connection('mongodb')->transaction(function () use ($invoice) {
            // 1. Tandai invoice sebagai lunas
            $invoice->update([
                'status'  => 'paid',
                'paid_at' => now(),
            ]);

            // 2. Kurangi saldo kredit klien
            ClientCredit::where('client_id', $invoice->client_id)
                ->decrement('balance', $invoice->amount);

            // 3. Catat entri di buku kas
            CashEntry::create([
                'source'     => 'invoice',
                'reference'  => $invoice->id,
                'amount'     => $invoice->amount,
                'recorded_at' => now(),
            ]);
        });
    }
}

Jika CashEntry::create() melempar exception — misalnya karena validasi gagal atau koneksi terputus — Laravel otomatis melakukan rollback pada perubahan invoice dan ClientCredit yang sudah dieksekusi sebelumnya.

Menangani Retry untuk Transient Error

Tidak semua error dalam transaksi MongoDB berarti ada bug di kode kita. Transient errors seperti write conflict antar transaksi yang bersamaan bisa terjadi dalam sistem dengan concurrency tinggi. Untuk kasus ini, retry adalah pendekatan yang tepat.

Laravel mendukung retry langsung lewat parameter kedua DB::transaction():

PHPPHP
// app/Actions/MarkInvoiceAsPaidAction.php

DB::connection('mongodb')->transaction(function () use ($invoice) {
    $invoice->update([
        'status'  => 'paid',
        'paid_at' => now(),
    ]);

    ClientCredit::where('client_id', $invoice->client_id)
        ->decrement('balance', $invoice->amount);

    CashEntry::create([
        'source'     => 'invoice',
        'reference'  => $invoice->id,
        'amount'     => $invoice->amount,
        'recorded_at' => now(),
    ]);
}, 3); // coba ulang hingga 3 kali jika terjadi transient error

Parameter ketiga ini memberi tahu Laravel untuk mencoba ulang closure sebanyak tiga kali sebelum akhirnya melempar exception ke lapisan yang lebih atas. Gunakan nilai yang wajar — tiga hingga lima kali biasanya sudah cukup untuk menangani spike concurrency sementara.

Retry hanya tepat untuk transient errors (write conflict, deadlock). Jika error disebabkan oleh logika bisnis — misalnya saldo klien tidak mencukupi — retry tidak akan membantu dan justru membuang waktu. Validasi kondisi bisnis sebelum membuka transaksi.

Memisahkan Logika Bisnis dari Transaksi Database

Salah satu kesalahan umum adalah memasukkan terlalu banyak hal ke dalam closure transaksi — termasuk pemanggilan API eksternal, pengiriman email, atau operasi yang bukan bagian dari perubahan database.

Transaksi yang ideal hanya berisi operasi database. Semua side effect (notifikasi, webhook, event) sebaiknya dijalankan setelah transaksi berhasil di-commit.

PHPPHP
// app/Actions/MarkInvoiceAsPaidAction.php

namespace App\Actions;

use App\Events\InvoicePaid;
use App\Models\CashEntry;
use App\Models\ClientCredit;
use App\Models\Invoice;
use Illuminate\Support\Facades\DB;

class MarkInvoiceAsPaidAction
{
    public function execute(Invoice $invoice): void
    {
        // Validasi kondisi bisnis di luar transaksi
        $credit = ClientCredit::where('client_id', $invoice->client_id)->firstOrFail();

        if ($credit->balance < $invoice->amount) {
            throw new \DomainException('Saldo klien tidak mencukupi untuk melunasi invoice ini.');
        }

        // Transaksi hanya berisi operasi database
        DB::connection('mongodb')->transaction(function () use ($invoice, $credit) {
            $invoice->update([
                'status'  => 'paid',
                'paid_at' => now(),
            ]);

            $credit->decrement('balance', $invoice->amount);

            CashEntry::create([
                'source'      => 'invoice',
                'reference'   => $invoice->id,
                'amount'      => $invoice->amount,
                'recorded_at' => now(),
            ]);
        }, 3);

        // Side effect dijalankan setelah transaksi sukses
        event(new InvoicePaid($invoice));
    }
}

Pola ini memiliki dua keuntungan utama. Pertama, transaksi database berjalan lebih singkat — semakin pendek durasi transaksi, semakin kecil risiko timeout dan write conflict. Kedua, kita tidak perlu khawatir email atau webhook terkirim dua kali jika transaksi di-retry.

Hal yang Perlu Diperhatikan

Beberapa hal yang sering menjadi jebakan saat bekerja dengan MongoDB transactions:

  • Replica set adalah prasyarat — transaksi tidak bisa berjalan di instance MongoDB standalone. Pastikan environment lokal juga dikonfigurasi dengan replica set untuk menghindari perbedaan perilaku antara development dan production.
  • Durasi transaksi dibatasi 60 detik secara default di MongoDB. Transaksi yang memakan waktu lebih dari itu akan di-abort otomatis. Ini alasan lain mengapa operasi berat seperti pemrosesan file tidak boleh ada di dalam closure transaksi.
  • Hindari transaksi untuk operasi single-document — MongoDB sudah atomik di level dokumen tunggal. Membungkus operasi single-document dalam transaksi hanya menambah overhead tanpa manfaat.
  • Index tetap penting — transaksi tidak menghilangkan kebutuhan index yang tepat. Query tanpa index di dalam transaksi yang berjalan bersamaan bisa menyebabkan lock contention yang memperparah write conflict.

Kesimpulan

Multi-document ACID transactions di MongoDB bukan sekadar fitur tambahan — ini adalah jaring pengaman untuk operasi bisnis yang tidak boleh gagal di tengah jalan. Dengan DB::transaction() dari laravel-mongodb, kita bisa mendapatkan jaminan konsistensi data yang setara dengan database relasional, tanpa meninggalkan ekosistem MongoDB. Kunci utamanya: jaga agar closure transaksi tetap ringkas, validasi kondisi bisnis di luar transaksi, dan pindahkan semua side effect ke luar agar sistem tetap dapat diprediksi.

Referensi

  1. 1 MongoDB Manual — Transactions
  2. 2 Laravel Documentation — Database Transactions
  3. 3 mongodb/laravel-mongodb — GitHub Repository
Abd. Asis
Ditulis oleh
Abd. Asis

Software Developer dari Madura. Menulis tentang PHP, Laravel, dan pengembangan web modern dalam Bahasa Indonesia.