Laravelで複数データベース接続を運用する:接続定義・読み書き分離・テナント分割まで

Laravelで複数データベース接続を運用する:接続定義・読み書き分離・テナント分割まで

Laravelは標準で複数DB接続を扱える。単に「接続を増やす」だけでなく、用途(参照専用DB、分析DB、外部システムDB、テナント別DB)ごとに責務を分離し、マイグレーション・トランザクション・キュー・テスト・監視まで含めた運用設計に落とし込むと事故が減る。ここでは config/database.php と Eloquent / Query Builder を軸に、複数接続を“壊れない形”で管理する手順をまとめる。

複数DBが必要になる代表パターンを整理する

・業務DB(書き込み)+参照専用レプリカ(読み取り)
・アプリDB(トランザクション重視)+分析DB(集計/BI)
・自社DB+外部プロダクトのDB(別スキーマ/別認証)
・マルチテナント(テナントごとにDB/スキーマ分割)
・段階移行(旧DBと新DBを並行稼働して切り替え)
このどれなのかで、接続の切り方と運用コストが変わる。

接続定義の基本:config/database.php に connections を追加する

Laravelの接続は connections 配下に増やせる。キー名が接続名になる(例:mysql、analytics、legacy)。

// config/database.php(抜粋イメージ)
'connections' => [

    'mysql' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', '3306'),
        'database' => env('DB_DATABASE', 'laravel'),
        'username' => env('DB_USERNAME', 'root'),
        'password' => env('DB_PASSWORD', ''),
        // ...
    ],

    'analytics' => [
        'driver' => 'pgsql',
        'host' => env('ANALYTICS_DB_HOST'),
        'port' => env('ANALYTICS_DB_PORT', '5432'),
        'database' => env('ANALYTICS_DB_DATABASE'),
        'username' => env('ANALYTICS_DB_USERNAME'),
        'password' => env('ANALYTICS_DB_PASSWORD'),
        // ...
    ],

    'legacy' => [
        'driver' => 'mysql',
        'host' => env('LEGACY_DB_HOST'),
        'port' => env('LEGACY_DB_PORT', '3306'),
        'database' => env('LEGACY_DB_DATABASE'),
        'username' => env('LEGACY_DB_USERNAME'),
        'password' => env('LEGACY_DB_PASSWORD'),
        // ...
    ],
],

.envの分離:接続ごとに環境変数プレフィックスを揃える

接続が増えるほど、変数名の統一が効いてくる。例として analytics/legacy を追加する。

# .env(例)
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=app
DB_USERNAME=app
DB_PASSWORD=secret

ANALYTICS_DB_HOST=analytics-db
ANALYTICS_DB_PORT=5432
ANALYTICS_DB_DATABASE=analytics
ANALYTICS_DB_USERNAME=analytics
ANALYTICS_DB_PASSWORD=secret

LEGACY_DB_HOST=legacy-db
LEGACY_DB_PORT=3306
LEGACY_DB_DATABASE=legacy
LEGACY_DB_USERNAME=legacy
LEGACY_DB_PASSWORD=secret

発生しやすい問題:
・config:cache 後に .env を更新しても反映されない(デプロイ手順に組み込む)
・接続名と環境変数がズレて、意図しないDBに接続する

クエリ単位で接続を指定する:DB::connection()

Query Builderを使う場合、接続を明示すればそのDBに対してSQLが発行される。

use Illuminate\Support\Facades\DB;

$rows = DB::connection('analytics')
    ->table('daily_metrics')
    ->whereDate('date', now()->toDateString())
    ->get();

複数接続運用では「どの接続で実行しているか」を必ず見える化する(メソッドチェーンの先頭で固定)。

Eloquentで接続を固定する:Modelの $connection を使う

モデル単位で接続を固定すると、コードが散らかりにくい。

namespace App\Models\Analytics;

use Illuminate\Database\Eloquent\Model;

class DailyMetric extends Model
{
    protected $connection = 'analytics';
    protected $table = 'daily_metrics';
    public $timestamps = false;
}

use App\Models\Analytics\DailyMetric;

$metrics = DailyMetric::whereDate('date', today())->get();

発生しやすい問題:
・リレーション先のモデルが別接続のままで、意図しないDBへ飛ぶ
・同名テーブルが複数DBにあり、間違って更新してしまう(命名/接続固定を徹底)

動的に接続を切り替える:on() / setConnection() を使う

同じモデルクラスを、処理の文脈でDB切替したい場合に使う。

$user = (new \App\Models\User)->setConnection('legacy')
    ->newQuery()
    ->where('email', $email)
    ->first();

$users = \App\Models\User::on('legacy')->where('status', 'active')->get();

ただし、乱用すると「どこでどのDBか」が追えなくなる。基本はモデルで固定、例外として動的切替。

読み書き分離(Read/Write Splitting):read / write を設定して自動振り分けする

MySQL/PGのレプリカ構成では、読み取りはreadへ、書き込みはwriteへ振る設計がある。Laravelは接続定義でread/writeを持てる。

// config/database.php(概念例:mysql)
'mysql' => [
    'driver' => 'mysql',
    'read' => [
        'host' => [env('DB_READ_HOST', '127.0.0.1')],
    ],
    'write' => [
        'host' => [env('DB_WRITE_HOST', '127.0.0.1')],
    ],
    'sticky' => true,
    'database' => env('DB_DATABASE', 'laravel'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
    // ...
],

・sticky=true:書き込み直後の読み取りを同一接続(write側)に寄せて整合性問題を減らす
発生しやすい問題:
・レプリカ遅延で「更新したのに見えない」が起きる(sticky/設計で吸収)
・トランザクション中にreadへ飛んで不整合(トランザクション境界を厳密に)

トランザクション:必ず同一接続で完結させる

異なる接続を跨いで DB::transaction() しても、分散トランザクションにはならない。基本は「接続ごとにトランザクション」を徹底する。

use Illuminate\Support\Facades\DB;

DB::connection('mysql')->transaction(function () {
    // mysql側の更新はここで完結
});

DB::connection('analytics')->transaction(function () {
    // analytics側の更新はここで完結
});

複数DBで整合が必要なら、アプリ側の冪等設計・補償トランザクション(後追い整合)を前提にする。

マイグレーションの分離:接続別・パス別に管理する

分析DBや外部DBにマイグレーションを当てる場合は、migrationファイルの置き場を分けると運用が楽。

# analytics用マイグレーションを別パスで実行する例
php artisan migrate --database=analytics --path=database/migrations/analytics

発生しやすい問題:
・デフォルト接続に当たってしまい、別DBにテーブルが作られる
・同名マイグレーションや依存順序が崩れる(パス分離で回避)

キューとジョブ:実行環境で接続がズレないように固定する

ジョブは非同期で別プロセス/別コンテナで走る。ジョブ内で接続を明示しないと、環境変数やデフォルト接続の差分で事故が起きる。

public function handle(): void
{
    $row = \DB::connection('legacy')->table('users')->where('id', $this->legacyUserId)->first();

    // 取り込んだ結果を mysql に保存
    \DB::connection('mysql')->table('users')->updateOrInsert(
        ['external_id' => $row->id],
        ['name' => $row->name]
    );
}

テナント分割:テナントごとにDBを切り替える実務パターン

代表的な方式:
・DB分割(tenant_001, tenant_002 のようにDBが別)
・スキーマ分割(PostgreSQLのschema)
・テーブルに tenant_id を持たせる(単一DB)
DB分割を採る場合は、リクエスト単位で接続を切り替える仕組みが必要になる。

use Illuminate\Support\Facades\DB;

public function handle($request, \Closure $next)
{
    $tenant = $request->header('X-Tenant');

    config([
        'database.connections.tenant.database' => "tenant_{$tenant}",
        'database.default' => 'tenant',
    ]);

    DB::purge('tenant');     // 既存接続を破棄
    DB::reconnect('tenant'); // 新設定で再接続

    return $next($request);
}

注意点:
・接続プール/再接続のコストが増えるため、乱用しない
・テナント識別子をログに必ず残し、事故時の追跡性を確保
・接続名(tenant)を固定し、databaseだけ差し替える運用が管理しやすい

接続のヘルスチェック:起動時ではなく“必要時に検知”する

外部DB/分析DBは落ちる前提で、例外処理とリトライ方針を決める。接続確認を毎リクエストでやると遅くなる。

try {
    DB::connection('analytics')->select('select 1');
} catch (\Throwable $e) {
    // フォールバック(キャッシュ値、別経路、機能停止)
}

ログと監視:どの接続で遅いのかを切り分ける

複数DBで遅いとき、「どの接続」「どのクエリ」かが分からないと詰む。
・DB接続名をログに出す(テナントIDも同様)
・遅いクエリ監視(スロークエリログ / APM)
・レプリカ遅延の可視化(read/write構成の場合)
・キュー実行時の接続先ログ(非同期は特に)

まとめ:Laravelで複数DBを安全に管理する要点

・複数DBの目的(レプリカ/分析/外部/テナント/移行)を先に固定
・connectionsを増やし、.envはプレフィックス統一で管理
・クエリ/モデルで接続を明示し、意図しない接続を排除
・read/write分離はstickyと整合性設計までセットで考える
・トランザクションは接続を跨がない(跨ぐなら補償設計)
・マイグレーション/ジョブ/テストは“接続指定漏れ”が事故要因なので仕組みで防ぐ