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.
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
],
| Setelan | Efek |
|---|---|
Tanpa read/write | Semua query ke satu server |
| Dengan keduanya | SELECT ke replica, sisanya ke writer |
'sticky' => true | Setelah 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.