โ† Semua pembelajaran / Laravel Nol โ†’ Enterprise
Fase 2 ยท Database & Eloquent

Koneksi database & transaksi

Sebelum menulis satu query pun, ada tiga keputusan yang menempel sampai produksi: driver mana, bagaimana koneksi dikelola, dan di mana batas transaksinya.

Sumber asli laravel.com Resmi Rangkuman ~7 menit baca

Intisari

  • Konfigurasi ada di config/database.php, nilainya dari .env. Boleh punya banyak koneksi sekaligus.
  • DB::transaction(fn () => ...) otomatis rollback saat ada exception, dan bisa mengulang saat deadlock.
  • Laravel mendukung koneksi read/write terpisah โ€” penting begitu ada read replica di Fase 9.
  • Replication lag itu nyata: menulis lalu langsung membaca bisa mengembalikan data lama.
  • DB::listen() memperlihatkan setiap query beserta durasinya โ€” alat utama berburu N+1.

Konfigurasi koneksi

// config/database.php
'mysql' => [
    'driver'   => 'mysql',
    'host'     => env('DB_HOST', '127.0.0.1'),
    'database' => env('DB_DATABASE'),
    'username' => env('DB_USERNAME'),
    'password' => env('DB_PASSWORD'),
    'charset'  => 'utf8mb4',
    'collation'=> 'utf8mb4_unicode_ci',
    'strict'   => true,          // biarkan true: MySQL akan menolak data meragukan
],

utf8mb4, bukan utf8. Di MySQL, utf8 hanyalah UTF-8 tiga byte dan tidak sanggup menyimpan emoji atau sebagian aksara. Menyimpan "๐Ÿ‘" ke kolom utf8 akan memotong data atau melempar error. Default Laravel sudah benar โ€” jangan diubah.

Read/write terpisah

'mysql' => [
    'read'  => ['host' => [env('DB_HOST_READ')]],    // reader endpoint Aurora
    'write' => ['host' => [env('DB_HOST')]],         // writer endpoint
    'sticky' => true,
    // ... sisa konfigurasi
],
SetelanEfek
Tanpa read/writeSemua query ke satu server
Dengan keduanyaSELECT ke replica, sisanya ke writer
'sticky' => trueSetelah menulis, sisa request itu membaca dari writer

sticky menyelesaikan gejala, bukan penyebabnya. Ia hanya berlaku dalam satu siklus permintaan. Kalau kamu menyimpan data lalu mengirim job antrean yang langsung membacanya, worker itu adalah proses lain โ€” ia bisa membaca replica yang belum ter-update dan menemukan barisnya "tidak ada". Solusinya: kirim data, bukan cuma ID; atau tunda job-nya; atau paksa baca dari koneksi writer.

Transaksi

use Illuminate\Support\Facades\DB;

// Bentuk closure โ€” otomatis commit di akhir, rollback saat exception
$pesanan = DB::transaction(function () use ($keranjang, $user) {
    $pesanan = Pesanan::create([
        'user_id' => $user->id,
        'total'   => $keranjang->total(),
    ]);

    foreach ($keranjang->items as $item) {
        // lockForUpdate: kunci barisnya sampai transaksi selesai
        $produk = Produk::whereKey($item->produk_id)->lockForUpdate()->first();

        if ($produk->stok < $item->jumlah) {
            throw new StokTidakCukup($produk, $item->jumlah);   // seluruhnya batal
        }

        $produk->decrement('stok', $item->jumlah);
        $pesanan->items()->create($item->toArray());
    }

    return $pesanan;
}, attempts: 3);        // ulangi sampai 3x kalau kena deadlock

Jangan pernah memanggil layanan luar di dalam transaksi. Memanggil gerbang pembayaran atau mengirim email di dalam DB::transaction() berarti kunci baris ditahan selama menunggu jaringan, dan kalau transaksinya rollback, email tetap terkirim. Pola yang benar: DB::afterCommit(), atau job antrean yang memang menunggu commit (Fase 3).

DB::afterCommit(function () use ($pesanan) {
    KirimKonfirmasiPesanan::dispatch($pesanan);   // hanya jalan kalau commit sukses
});

Melihat query yang benar-benar berjalan

// AppServiceProvider::boot() โ€” hanya untuk lingkungan lokal
if (app()->environment('local')) {
    DB::listen(function ($query) {
        Log::debug($query->sql, [
            'binding' => $query->bindings,
            'ms'      => $query->time,
        ]);
    });
}

Dua pengaman yang layak dinyalakan sejak awal โ€” keduanya membuat masalah muncul di laptop, bukan di produksi:

// Gagalkan kalau ada relasi yang diakses tanpa eager loading (penyebab N+1)
Model::preventLazyLoading(! app()->isProduction());

// Peringatkan kalau satu permintaan menghabiskan terlalu banyak waktu di database
DB::whenQueryingForLongerThan(500, function ($connection) {
    Log::warning('query lambat', ['koneksi' => $connection->getName()]);
});

Query mentah, kalau memang perlu

// AMAN โ€” parameter di-bind, bukan disambung
$hasil = DB::select('SELECT * FROM produk WHERE harga > ?', [$minimum]);

// BERBAHAYA โ€” SQL injection
$hasil = DB::select("SELECT * FROM produk WHERE nama = '{$request->nama}'");

Query builder dan Eloquent selalu memakai prepared statement, jadi keduanya kebal SQL injection selama kamu tidak menyambung string sendiri. Satu-satunya celah adalah tempat yang menerima ekspresi mentah: DB::raw(), whereRaw(), orderByRaw(). Jangan pernah menaruh input pengguna di sana tanpa binding.

Latihan: tulis satu DB::transaction() yang membuat pesanan lalu mengurangi stok, dan paksa throw di tengahnya. Setelah itu cek tabel โ€” pesanannya harus tidak ada. Lalu nyalakan DB::listen() dan hitung berapa query yang sebenarnya dijalankan satu halaman daftar produkmu.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.