Come usare più di un database in Laravel

Laravel supporta nativamente l’uso di più database nello stesso progetto: puoi avere più connessioni (anche di driver diversi), instradare query e modelli verso una connessione specifica, eseguire migrazioni su database separati, gestire transazioni e persino cambiare connessione a runtime. Questa guida mostra una configurazione solida e i pattern più comuni per applicazioni reali.

1. Concetti chiave

  • Connessione: un insieme di parametri (driver, host, database, credenziali) definito in config/database.php.
  • Connessione predefinita: quella usata da query builder ed Eloquent quando non specifichi altro.
  • Selezione della connessione: puoi indicarla per singola query (DB::connection(...)), per modello ($connection), per migrazione (--database) o a runtime (Model::on(...)).
  • Replica / read-write splitting: una connessione può avere nodi di lettura e scrittura separati.

2. Configurare più connessioni

In config/database.php trovi l’array connections. Aggiungi una o più connessioni, ad esempio una principale mysql e una secondaria mysql_reporting.

<?php

return [
    'default' => env('DB_CONNECTION', 'mysql'),

    'connections' => [

        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'app'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
        ],

        'mysql_reporting' => [
            'driver' => 'mysql',
            'host' => env('DB_REPORTING_HOST', '127.0.0.1'),
            'port' => env('DB_REPORTING_PORT', '3306'),
            'database' => env('DB_REPORTING_DATABASE', 'reporting'),
            'username' => env('DB_REPORTING_USERNAME', 'root'),
            'password' => env('DB_REPORTING_PASSWORD', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
        ],
    ],
];

Nel file .env aggiungi le variabili corrispondenti:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=app
DB_USERNAME=root
DB_PASSWORD=

DB_REPORTING_HOST=127.0.0.1
DB_REPORTING_PORT=3306
DB_REPORTING_DATABASE=reporting
DB_REPORTING_USERNAME=root
DB_REPORTING_PASSWORD=

Se usi config:cache, ricordati di rigenerare la cache dopo modifiche a .env o config:

php artisan config:clear
php artisan config:cache

3. Eseguire query su una connessione specifica

Il modo più diretto è indicare la connessione sul facade DB:

use Illuminate\Support\Facades\DB;

$users = DB::connection('mysql')
    ->table('users')
    ->where('active', 1)
    ->get();

$daily = DB::connection('mysql_reporting')
    ->table('daily_stats')
    ->whereDate('day', now()->toDateString())
    ->first();

Puoi anche “tenere” un’istanza di connection e riusarla:

$reporting = DB::connection('mysql_reporting');

$topPages = $reporting->table('page_views')
    ->select('path', DB::raw('count(*) as views'))
    ->groupBy('path')
    ->orderByDesc('views')
    ->limit(10)
    ->get();

4. Modelli Eloquent su database diversi

Se un modello vive stabilmente su un database “secondario”, imposta la proprietà $connection (e opzionalmente $table).

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class DailyStat extends Model
{
    protected $connection = 'mysql_reporting';
    protected $table = 'daily_stats';

    protected $fillable = ['day', 'signups', 'revenue'];
}

In questo modo, qualsiasi chiamata a DailyStat::query() userà automaticamente mysql_reporting.

4.1 Cambiare connessione al volo con on()

Quando lo stesso modello può vivere su più database (scenario multi-tenant o data-sharding), usa:

use App\Models\User;

$users = User::on('mysql')->where('active', 1)->get();
$usersArchive = User::on('mysql_archive')->where('active', 1)->get();

Oppure da un’istanza:

$user = new User();
$user->setConnection('mysql_archive');
$user->save();

4.2 Relazioni Eloquent tra database diversi

Laravel consente di definire relazioni anche se i modelli usano connessioni differenti. Tuttavia:

  • Le join tra database diversi non sono portabili tra driver e spesso non sono possibili (dipende dal DBMS e dai permessi).
  • Le relazioni funzionano bene quando si risolvono con query separate (tipicamente hasMany, belongsTo), ma evita di affidarti a join cross-database.
  • Per performance, usa eager loading con criterio, o materializza dati di lettura in un database di reporting.
class Order extends Model
{
    protected $connection = 'mysql';

    public function stats()
    {
        return $this->hasOne(OrderStat::class, 'order_id');
    }
}

class OrderStat extends Model
{
    protected $connection = 'mysql_reporting';
}

5. Migrazioni su database multipli

Puoi mantenere migrazioni separate per ciascuna connessione. Un approccio pratico è creare directory dedicate, ad esempio:

  • database/migrations (default)
  • database/migrations_reporting

Eseguire migrazioni su una connessione specifica:

php artisan migrate --database=mysql_reporting --path=database/migrations_reporting

Un esempio di migrazione per il database di reporting:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::connection('mysql_reporting')->create('daily_stats', function (Blueprint $table) {
            $table->id();
            $table->date('day')->unique();
            $table->unsignedInteger('signups')->default(0);
            $table->decimal('revenue', 12, 2)->default(0);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::connection('mysql_reporting')->dropIfExists('daily_stats');
    }
};

5.1 Migrazioni automatiche in deployment

In pipeline o deploy script, esegui una migrate per connessione nell’ordine corretto. Se hai vincoli applicativi tra DB, considera di versionare anche job di sincronizzazione o viste materializzate.

6. Seeding su database diversi

I seeder possono scrivere su connessioni specifiche tramite query builder o modelli con $connection. Ad esempio:

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class ReportingSeeder extends Seeder
{
    public function run(): void
    {
        DB::connection('mysql_reporting')
            ->table('daily_stats')
            ->insert([
                'day' => now()->toDateString(),
                'signups' => 0,
                'revenue' => 0,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
    }
}

7. Transazioni con più database

Una transazione è sempre legata a una singola connessione. Se devi scrivere su due database in modo atomico, Laravel (come molti framework) non può garantire atomicità “globale” senza un coordinatore di transazioni distribuite (2PC), che raramente è consigliabile per applicazioni web tradizionali.

Approcci pratici:

  • Outbox pattern: scrivi sul database principale e pubblica un evento (o una riga “outbox”) processata da un job che sincronizza l’altro database.
  • Compensazione: se fallisce il secondo passo, esegui una operazione di rollback logico (annulla l’effetto della prima scrittura).
  • Idempotenza: rendi i job di sincronizzazione ripetibili senza creare duplicati.

Esempio: transazione sul database principale e inserimento in outbox nella stessa transazione:

use Illuminate\Support\Facades\DB;

DB::connection('mysql')->transaction(function () use ($payload) {
    $orderId = DB::table('orders')->insertGetId([
        'status' => 'created',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    DB::table('outbox_events')->insert([
        'type' => 'order_created',
        'payload' => json_encode(['order_id' => $orderId] + $payload),
        'created_at' => now(),
    ]);
});

Un job (queue) leggerà l’outbox e scriverà su mysql_reporting in modo asincrono e idempotente.

8. Read/Write splitting (repliche)

Se usi replica, Laravel supporta configurazioni con nodi read e write. Un esempio (semplificato) su MySQL:

'mysql' => [
    'driver' => 'mysql',
    'database' => env('DB_DATABASE', 'app'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
    'read' => [
        'host' => [
            env('DB_READ_HOST_1', '10.0.0.11'),
            env('DB_READ_HOST_2', '10.0.0.12'),
        ],
    ],
    'write' => [
        'host' => [
            env('DB_WRITE_HOST', '10.0.0.10'),
        ],
    ],
    'sticky' => true,
]
  • sticky=true aiuta a leggere dalla primaria dopo una scrittura, riducendo inconsistenze dovute a lag di replica.
  • Se usi più connessioni, puoi adottare lo splitting solo sulla connessione principale e lasciare la seconda per reporting/ETL.

9. Cambiare connessione a runtime (tenant, sharding, “database per cliente”)

Per multi-tenancy con database separati per cliente, spesso si configura una connessione “template” e si sovrascrivono dinamicamente i parametri per richiesta. Un approccio comune:

  1. Recuperare i parametri del tenant (host, database, username, password) da una fonte affidabile (es. DB principale o config service).
  2. Impostare la connessione in config().
  3. Chiedere a Laravel di “dimenticare” la connessione per forzare un nuovo connect.
use Illuminate\Support\Facades\DB;

function useTenantConnection(array $tenantDb): void
{
    config()->set('database.connections.tenant', [
        'driver' => 'mysql',
        'host' => $tenantDb['host'],
        'port' => $tenantDb['port'] ?? 3306,
        'database' => $tenantDb['database'],
        'username' => $tenantDb['username'],
        'password' => $tenantDb['password'],
        'charset' => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
        'prefix' => '',
        'strict' => true,
    ]);

    DB::purge('tenant');      // elimina l'istanza di connessione esistente
    DB::reconnect('tenant');  // apre una nuova connessione con i nuovi parametri
}

Da qui puoi usare DB::connection('tenant') o modelli con $connection = 'tenant'.

9.1 Scoping dei modelli per tenant

Per evitare errori, crea un base model tenant-aware:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

abstract class TenantModel extends Model
{
    protected $connection = 'tenant';
}

e poi:

class Invoice extends TenantModel
{
    protected $table = 'invoices';
}

10. Job in coda e connessioni multiple

Le queue possono eseguire job fuori dal contesto della request: devi assicurarti che il job sappia quale connessione usare. Pattern tipici:

  • Passare l’identificativo del tenant e ricalcolare la connessione all’inizio del job.
  • Per reporting: far leggere dal DB principale e scrivere sul DB reporting con una connessione esplicita.
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;

class SyncDailyStats implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle(): void
    {
        $count = DB::connection('mysql')
            ->table('users')
            ->whereDate('created_at', now()->toDateString())
            ->count();

        DB::connection('mysql_reporting')
            ->table('daily_stats')
            ->updateOrInsert(
                ['day' => now()->toDateString()],
                ['signups' => $count, 'updated_at' => now(), 'created_at' => now()]
            );
    }
}

11. Testing: database multipli e isolamenti

Quando testi:

  • Assicurati che entrambe le connessioni puntino a database di test.
  • Esegui migrazioni per entrambe le connessioni nel setup, oppure usa suite separate.
  • Se usi SQLite in-memory per test rapidi, ricorda che ogni connessione in-memory è un “mondo” a parte: non condivide tabelle con altre connessioni.

Esempio di test che migra due connessioni:

use Illuminate\Support\Facades\Artisan;
use Tests\TestCase;

class MultiDbTestCase extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        Artisan::call('migrate', ['--database' => 'mysql', '--path' => 'database/migrations']);
        Artisan::call('migrate', ['--database' => 'mysql_reporting', '--path' => 'database/migrations_reporting']);
    }
}

12. Errori comuni e come evitarli

  • Connessione cacheata: dopo cambi a config o env, rigenera cache config; altrimenti Laravel continua con i vecchi valori.
  • Relazioni che fanno join implicite: alcune operazioni (es. whereHas) possono generare query più complesse. Se i modelli sono su DB diversi, verifica il SQL risultante.
  • Transazioni “multi-DB”: non assumere atomicità su più connessioni; usa outbox/compensazione.
  • Connessione dinamica senza purge: se cambi parametri a runtime, usa DB::purge() e DB::reconnect().
  • Sessioni / cache su DB diverso: se configuri session/cache su database, assicurati che puntino alla connessione corretta e che le tabelle siano migrate su quel DB.

13. Checklist di progetto

  • Definisci chiaramente lo scopo di ciascun database (OLTP, reporting, archiviazione, tenant, integrazioni).
  • Centralizza la scelta della connessione (base model, repository, service) per ridurre errori.
  • Automatizza migrazioni e seed per tutte le connessioni in CI/CD.
  • Gestisci consistenza e sincronizzazione con pattern robusti (outbox, job idempotenti).
  • Monitora: errori di connessione, timeouts e lag di replica, soprattutto con read/write splitting.

Con questi strumenti puoi scalare il tuo progetto Laravel oltre il singolo database, mantenendo codice pulito, performance prevedibili e un’architettura più adatta a reporting, multi-tenancy o separazione dei domini.

Torna su