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 ajoincross-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=trueaiuta 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:
- Recuperare i parametri del tenant (host, database, username, password) da una fonte affidabile (es. DB principale o config service).
- Impostare la connessione in
config(). - 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()eDB::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.