P
imvdmolen.nl
Blog

Laravel Herd's database switching gebruiken voor multi-tenant projecten

Multi-tenant projecten waarbij datasets per bedrijfsonderdeel gescheiden moeten blijven komen in mijn werk regelmatig voor. Normaal gesproken betekent dit meerdere databases opzetten, configureren en constant switchen tussen verbindingen tijdens development. Met Laravel Herd bleek dit eenvoudiger dan verwacht door de ingebouwde database switching die ik een tijdlang over het hoofd had gezien.

Herd's database switching instellen

Het opzetten van database switching begint met het creëren van meerdere databases via Herd's interface. Ik navigeer naar de Herd applicatie en klik op de database tab waar ik nieuwe databases kan aanmaken. Voor mijn multi-tenant project maakte ik drie databases aan: tenant_company_a, tenant_company_b en tenant_shared voor gedeelde data zoals gebruikersrollen en systeem configuraties.

// config/database.php - aangepaste configuratie
'connections' => [
    'mysql' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '3306'),
        'database' => env('DB_DATABASE', 'tenant_company_a'),
        'username' => env('DB_USERNAME', 'root'),
        'password' => env('DB_PASSWORD', ''),
        // ... rest van de configuratie
    ],
    
    'tenant_a' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '3306'),
        'database' => 'tenant_company_a',
        'username' => env('DB_USERNAME', 'root'),
        'password' => env('DB_PASSWORD', ''),
    ],
    
    'tenant_b' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '3306'),
        'database' => 'tenant_company_b',
        'username' => env('DB_USERNAME', 'root'),
        'password' => env('DB_PASSWORD', ''),
    ],
],

Na het instellen van de database verbindingen gebruik ik Herd's CLI functionaliteit om te switchen tussen databases. Het commando herd database switch tenant_company_a verandert automatisch de DATABASE_URL environment variable en herstart de benodigde services. Dit gebeurt zonder dat ik handmatig bestanden hoef aan te passen of mijn development server opnieuw hoef op te starten.

Tijdens het testen van verschillende tenant scenarios kan ik nu binnen seconden switchen tussen databases. Als ik bijvoorbeeld wijzigingen wil testen in de dataset van company A, voer ik herd database switch tenant_company_a uit en al mijn Eloquent queries gebruiken automatisch de juiste database. Voor het testen van shared functionaliteit switch ik naar de tenant_shared database waar alle algemene data staat.

Dynamische database verbindingen in code

Naast Herd's switching functionaliteit bouwde ik ook dynamische database switching direct in mijn Laravel code. Dit is essentieel voor een echte multi-tenant applicatie waar de database verbinding bepaald wordt door de inkomende request of gebruikerscontext. Ik creëerde een middleware die de juiste tenant database selecteert op basis van de subdomain of een parameter in de URL.

// app/Http/Middleware/TenantDatabaseSwitcher.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;

class TenantDatabaseSwitcher
{
    public function handle(Request $request, Closure $next)
    {
        $subdomain = explode('.', $request->getHost())[0];
        
        if ($subdomain === 'company-a') {
            Config::set('database.default', 'tenant_a');
        } elseif ($subdomain === 'company-b') {
            Config::set('database.default', 'tenant_b');
        }
        
        // Herverbind alle database connecties
        DB::purge(Config::get('database.default'));
        
        return $next($request);
    }
}

Voor complexere scenario's waarbij ik data uit meerdere tenant databases tegelijk nodig heb, gebruik ik Laravel's multiple database connections. In mijn models kan ik specificeren welke verbinding gebruikt moet worden, of ik kan deze dynamisch instellen tijdens runtime. Dit is vooral handig voor rapportage functionaliteiten waarbij ik data van meerdere tenants moet combineren.

// app/Models/Order.php
class Order extends Model
{
    protected $connection = 'tenant_a'; // Vaste verbinding
    
    // Of dynamisch per model instance
    public function setTenantConnection($tenant)
    {
        $this->connection = "tenant_{$tenant}";
        return $this;
    }
}

// Gebruik in controller
$orders = (new Order())->setTenantConnection('a')->where('status', 'pending')->get();
$otherOrders = (new Order())->setTenantConnection('b')->where('status', 'pending')->get();

Het combineren van Herd's database switching met programmatische database switching geeft mij de flexibiliteit om zowel tijdens development snel te kunnen testen als in productie robuuste multi-tenant functionaliteit te bieden. Voor unit tests gebruik ik Herd's switching om test databases in te stellen, terwijl de applicatie zelf de tenant database bepaalt op basis van de request context.

Migration management per tenant

Een van de grootste uitdagingen bij multi-tenant applicaties is het beheren van database migrations. Elke tenant database moet dezelfde structuur hebben, maar soms wil je ook tenant-specifieke aanpassingen kunnen maken. Met Herd's database switching kan ik migrations uitvoeren op specifieke databases zonder complexe scripts of handmatige interventies.

# Switch naar tenant A database via Herd
herd database switch tenant_company_a

# Voer migrations uit voor deze tenant
php artisan migrate

# Switch naar tenant B
herd database switch tenant_company_b
php artisan migrate

# Voor alle tenants tegelijk (custom artisan command)
php artisan tenants:migrate

Ik bouwde een custom Artisan command die automatisch door alle tenant databases loopt en migrations uitvoert. Deze command gebruikt Herd's API om database verbindingen te wijzigen en vervolgens de standaard migration commands aan te roepen. Dit bespaart enorm veel tijd tijdens deployment en zorgt ervoor dat alle tenant databases consistent blijven.

// app/Console/Commands/MigrateTenants.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;

class MigrateTenants extends Command
{
    protected $signature = 'tenants:migrate {--fresh : Drop all tables and re-run all migrations}';
    protected $description = 'Run migrations on all tenant databases';

    public function handle()
    {
        $tenantConnections = ['tenant_a', 'tenant_b'];
        
        foreach ($tenantConnections as $connection) {
            $this->info("Migrating database: {$connection}");
            
            Config::set('database.default', $connection);
            DB::purge($connection);
            
            if ($this->option('fresh')) {
                Artisan::call('migrate:fresh', ['--force' => true]);
            } else {
                Artisan::call('migrate', ['--force' => true]);
            }
            
            $this->info("Completed migrations for {$connection}");
        }
        
        $this->info('All tenant databases have been migrated successfully!');
    }
}

Seeding van tenant-specifieke data werkt volgens hetzelfde principe. Ik creëer verschillende seeders voor elke tenant en gebruik Herd's database switching om deze uit te voeren op de juiste databases. Voor development data maak ik gebruik van factories die realistische maar fictieve data genereren voor elke tenant database.

Voor backup en restore operaties heeft Herd's database switching ook voordelen. Ik kan per tenant database backups maken en deze testen door te switchen naar een test database, de backup te restoren en vervolgens mijn applicatie te testen met de gerestore data. Dit proces dat voorheen veel handmatig werk vergde, is nu geautomatiseerd en betrouwbaar geworden.

Performance optimalisaties en monitoring

Database switching brengt ook performance overwegingen met zich mee. Ik merkte al snel dat connection pooling belangrijk wordt wanneer je applicatie regelmatig tussen databases moet switchen. Laravel's database connection pooling helpt hierbij, maar ik moest ook custom caching implementeren voor database metadata en connection informatie.

// app/Services/TenantDatabaseManager.php
class TenantDatabaseManager
{
    protected $connectionCache = [];
    
    public function switchToTenant($tenant)
    {
        $connectionName = "tenant_{$tenant}";
        
        // Cache connection configuratie
        if (!isset($this->connectionCache[$connectionName])) {
            $this->connectionCache[$connectionName] = [
                'driver' => 'mysql',
                'host' => config('database.connections.mysql.host'),
                'database' => "tenant_company_{$tenant}",
                'username' => config('database.connections.mysql.username'),
                'password' => config('database.connections.mysql.password'),
            ];
        }
        
        Config::set("database.connections.{$connectionName}", 
                   $this->connectionCache[$connectionName]);
        Config::set('database.default', $connectionName);
        
        return $connectionName;
    }
}

Monitoring van database performance per tenant is cruciaal geworden. Ik gebruik Laravel's query logging in combinatie met custom metrics om te zien welke tenants de meeste database load veroorzaken. Herd's ingebouwde monitoring tools geven mij inzicht in connection counts en query performance per database, wat helpt bij het identificeren van bottlenecks.

Query caching per tenant vereist extra aandacht omdat cached results niet mogen worden gedeeld tussen verschillende tenants. Ik implementeerde cache keys die de tenant identifier bevatten om ervoor te zorgen dat data isolatie behouden blijft. Dit voorkomt situaties waarbij tenant A data van tenant B te zien krijgt door verkeerd gecachte queries.

Het mooie aan Laravel Herd's database switching is dat het development en productie dichter bij elkaar brengt. Waar ik vroeger vaak problemen had met database configuratie verschillen tussen mijn lokale setup en productie, geeft Herd mij de mogelijkheid om lokaal dezelfde database switching patronen te testen die ik in productie gebruik. Dit heeft geleid tot veel minder deployment issues en meer vertrouwen in mijn multi-tenant implementaties.