Aplikasi di dunia nyata sering tidak cukup dengan satu database. Data transaksi bisa berada di database utama, sementara data statistik disalin ke database analitik yang lebih ringan untuk dibaca agar query laporan tidak membebani database utama. Laravel mendukung pola seperti ini lewat array connections yang boleh berisi sebanyak apapun koneksi. Pada chapter ini kita menambah satu koneksi kedua pada proyek latihan, memakainya dari query builder, schema builder, dan model Eloquent, lalu menutup dengan pola pemisahan baca-tulis (read/write splitting).

Koneksi Default dan Cara Resolusinya #

Kita mulai dari mekanisme yang sudah dikenal dari chapter sebelumnya. Kunci default di config/database.php berisi nama koneksi bawaan — sqlite pada proyek kita — dan seluruh operasi database yang tidak menyebut koneksi secara eksplisit akan memakai koneksi tersebut. Kita bisa membuktikannya dengan memanggil DB::connection() tanpa argumen, lalu membandingkannya dengan pemanggilan bernama:

php
use Illuminate\Support\Facades\DB;

echo DB::connection()->getName().PHP_EOL;
echo DB::connection('sqlite_analytics')->getName().PHP_EOL;

Output yang Dihasilkan:

text
sqlite
sqlite_analytics

Method getName() mengembalikan nama koneksi yang sedang dipakai. Pemanggilan pertama tanpa argumen mengikuti kunci default, sedangkan pemanggilan kedua meminta koneksi bernama sqlite_analytics — koneksi yang akan kita definisikan sekarang.

Menambah Koneksi Kedua #

Koneksi baru didefinisikan dengan menambahkan entri baru pada array connections di config/database.php. Kita tambahkan koneksi SQLite kedua yang menunjuk berkas terpisah, diletakkan tepat setelah entri sqlite:

php
'sqlite_analytics' => [
    'driver' => 'sqlite',
    'database' => database_path('analytics.sqlite'),
    'prefix' => '',
    'foreign_key_constraints' => true,
],

Nama entri — sqlite_analytics — bebas kita pilih; nama itulah yang dipakai saat meminta koneksi. Karena berkas database/analytics.sqlite belum ada, kita siapkan berkas kosongnya terlebih dahulu dari direktori proyek:

bash
touch database/analytics.sqlite

Definisi ini sengaja memakai database_path() secara langsung alih-alih env(). Nilai konfigurasi yang bersifat internal — tidak berubah antar lingkungan — memang lazim ditulis langsung di berkas config, sementara nilai yang berbeda antara mesin developer dan server produksi tetap diteruskan lewat env().

Memakai Koneksi Tertentu pada Query dan Skema #

Sekarang kita isi koneksi kedua tersebut dengan satu tabel. Schema builder mengenal method connection() untuk memilih koneksi sebelum operasi skema dijalankan:

php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::connection('sqlite_analytics')->create('page_views', function (Blueprint $table) {
    $table->id();
    $table->string('path');
    $table->timestamps();
});

Tiga hal terjadi pada kode di atas: facade Schema meminta koneksi sqlite_analytics, membuka berkas analytics.sqlite, lalu membuat tabel page_views di dalamnya — tabel posts di database utama tidak tersentuh sama sekali. Query builder mengikuti pola yang sama:

php
use Illuminate\Support\Facades\DB;

DB::connection('sqlite_analytics')->table('page_views')->insert([
    'path' => '/artikel/laravel',
    'created_at' => now(),
    'updated_at' => now(),
]);

Query insert di atas mengenali tabel page_views karena berjalan pada koneksi sqlite_analytics. Method connection() inilah pintu masuk satu-satunya untuk memilih koneksi pada query builder maupun schema builder — tanpanya Laravel selalu memakai koneksi default.

Properti $connection pada Model Eloquent #

Model Eloquent punya dua cara berpindah koneksi. Cara pertama adalah properti $connection yang mengikat model secara permanen:

php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class PageView extends Model
{
    protected $connection = 'sqlite_analytics';

    protected $fillable = ['path'];
}

Cara kedua adalah method on() yang memilih koneksi per query. Untuk menguji keduanya sekaligus, kita jalankan tiga query verifikasi melalui skrip PHP kecil pada proyek latihan (potongan fokus, tanpa baris use):

php
echo 'Query lewat properti $connection : '.PageView::count().' baris'.PHP_EOL;
echo 'Query lewat method on()         : '
    .PageView::on('sqlite_analytics')->count().' baris'.PHP_EOL;

try {
    PageView::on('sqlite')->count();
} catch (Throwable $e) {
    echo 'Query ke koneksi sqlite         : '.$e->getMessage().PHP_EOL;
}

Output yang Dihasilkan:

text
Query lewat properti $connection : 1 baris
Query lewat method on()         : 1 baris
Query ke koneksi sqlite         : SQLSTATE[HY000]: General error: 1 no such table: page_views (Connection: sqlite, Database: database/database.sqlite, SQL: select count(*) as "aggregate" from "page_views")

Dua query pertama berhasil membaca satu baris data dari analytics.sqlite — satu lewat properti, satu lewat method on(). Query ketiga sengaja memaksa model yang sama ke koneksi sqlite dan gagal dengan pesan no such table, karena database utama memang tidak memiliki tabel page_views. Kegagalan ini justru bukti bahwa pemilihan koneksi benar-benar mengarahkan query ke database yang berbeda.

Koneksi Read/Write #

Untuk aplikasi berskala besar, Laravel mendukung pemisahan baca-tulis pada satu koneksi — pola yang dijelaskan lebih lanjut di dokumentasi database . Kunci read dan write menggantikan detail koneksi langsung, seperti contoh template MySQL berikut:

php
'mysql' => [
    'read' => [
        'host' => env('DB_REPLICA_HOST', '127.0.0.1'),
    ],
    'write' => [
        'host' => env('DB_HOST', '127.0.0.1'),
    ],
    'driver' => 'mysql',
    'database' => env('DB_DATABASE', 'laravel'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
    // ...
],

Cara kerjanya: array read menimpa nilai host untuk seluruh statement SELECT, array write menimpa host untuk INSERT, UPDATE, dan DELETE, sedangkan kunci-kunci di luar keduanya dipakai bersama. Pola ini biasa dipakai bersama replikasi database — tulisan masuk ke server utama, pembacaan disebar ke server replika.

Mekanismenya bisa kita buktikan tanpa server MySQL, cukup dengan dua berkas SQLite yang berperan sebagai “sisi baca” dan “sisi tulis”:

php
'sqlite_rw' => [
    'read' => [
        'database' => database_path('reading.sqlite'),
    ],
    'write' => [
        'database' => database_path('writing.sqlite'),
    ],
    'driver' => 'sqlite',
    'prefix' => '',
],

Setelah menyiapkan tabel visits yang sama di kedua berkas, kita menyisipkan satu baris lewat koneksi sqlite_rw, membacanya kembali, lalu memeriksa isi kedua berkas secara fisik lewat PDO:

php
DB::connection('sqlite_rw')->table('visits')->insert(['path' => '/beranda']);

echo 'Hasil SELECT lewat sqlite_rw: '
    .DB::connection('sqlite_rw')->table('visits')->count().' baris'.PHP_EOL;

foreach (['reading', 'writing'] as $berkas) {
    $pdo = new PDO('sqlite:'.database_path($berkas.'.sqlite'));
    $jumlah = $pdo->query('select count(*) from visits')->fetchColumn();
    echo $berkas.'.sqlite berisi '.$jumlah.' baris'.PHP_EOL;
}

Output yang Dihasilkan:

text
Hasil SELECT lewat sqlite_rw: 0 baris
reading.sqlite berisi 0 baris
writing.sqlite berisi 1 baris

Hasilnya mencolok: baris yang baru disisipkan tidak terbaca. Statement INSERT diarahkan ke berkas writing.sqlite (terbukti berisi satu baris), sedangkan statement SELECT membaca dari reading.sqlite yang kosong. Pada produksi, server replika tersinkronisasi lewat replikasi MySQL sehingga data tulisan akhirnya ikut terbaca — percobaan dua berkas statis kita sengaja memutus sinkronisasi itu agar arah pemilihan koneksi terlihat jelas.

Kini kita menguasai cara berbicara ke banyak database dari satu aplikasi. Namun struktur tabel itu sendiri — bagaimana tabel dibuat, diubah, dan dibatalkan — belum kita sentuh; itulah peran migration, konsep yang kita bahas pada chapter berikutnya.