LaravelのEloquentで、二つのテーブルを一列の外部キーだけでは結べないことがあります。
会計システムから受け取るデータやテナント別の既存データでは、会社コードと伝票番号のような複数列をそろえて初めて行を特定できるためです。
Laravel Newsが紹介したComposhipsは、このような複数列の照合をEloquentのリレーションとして定義できるLaravel向けパッケージです。
ただし、Laravelが複合主キーを全面的に扱えるようになるわけではありません。
採用前には、リレーションの照合、モデルの識別、更新処理を分けて考える必要があります。
Composhipsが補うEloquentの制約
複数列リレーションとは、親子の対応を一組の列ではなく、二つ以上の列の組み合わせで判定する関係です。
たとえば、顧客番号がテナントごとに採番されるデータでは、tenant_idとcustomer_noの両方が一致して初めて顧客と注文を結べます。
通常のhasMany()に親モデルの値を使うwhere()を足す方法は、Eager Loading(事前読み込み)で正しく働かない場合があります。
Laravelが事前読み込み用のリレーションを空のモデルから組み立てる段階では、条件に使った親属性を取得できないためです。
Composhipsはキー名の代わりに列名の配列を受け取り、親の集合に対して複数列の組み合わせを照合します。
ここで「複数列でリレーションを結ぶこと」と「複数列をモデルの主キーとして扱うこと」は別の問題です。
Laravel 13のEloquent公式ドキュメントは、モデルの複合主キーを標準ではサポートしないと明記し、モデルを一意に識別する主キーとは別に複数列の一意インデックスを設ける方法を示しています。
複数列リレーションの実装例
Composhipsの現行composer.jsonでは、PHP 8.2以降とLaravel 12または13に対応するilluminate/databaseが依存条件です。
プロジェクトのPHPとLaravelのバージョンを確認してからComposerで追加します。
composer require awobaz/compoships
次の例では、顧客と注文をtenant_idとcustomer_noで結びます。
リレーションの両側のモデルでComposhipsトレイトを使い、外部キーとローカルキーを同じ順序の配列で渡します。
<?php
namespace App\Models;
use Awobaz\Compoships\Compoships;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Customer extends Model
{
use Compoships;
public function orders(): HasMany
{
return $this->hasMany(
Order::class,
['tenant_id', 'customer_no'],
['tenant_id', 'customer_no']
);
}
}
class Order extends Model
{
use Compoships;
public function customer(): BelongsTo
{
return $this->belongsTo(
Customer::class,
['tenant_id', 'customer_no'],
['tenant_id', 'customer_no']
);
}
}
Composhipsの公式リポジトリによると、hasOne、hasMany、belongsTo、belongsToManyが複数列の照合に対応します。
列の並びが左右でずれると別の値を比較してしまうため、配列の位置を仕様として固定し、両方向のリレーションをテストする必要があります。
新規設計と既存データで判断を分ける
| 状況 | 第一候補 | 確認する点 |
|---|---|---|
| 新規システムでスキーマを変更できる | 単一の主キーと複数列の一意制約を組み合わせる | 標準Eloquentの検索、ルートモデルバインディング、キューを保ちやすいか |
| 外部システムや既存DBを変更できない | Composhipsで複数列リレーションを定義する | 列の順序、NULLの意味、複合インデックス、参照整合性 |
| 複合主キーの行をモデル経由で更新する | $compositeKeyによる書き込み範囲の補助を検証する |
find()やルートモデルバインディングなど、単一キーのまま残る経路 |
スキーマを管理できる新規開発であれば、単一の代理主キーを置き、業務上の組み合わせには一意制約を設定する構成が扱いやすい場合が多いです。
PHPフレームワークの選定条件と同様に、パッケージの機能だけでなく、保守するチームと既存資産を基準に決めます。
一方、外部製品のDBや長年運用してきた基幹データをそのまま参照するなら、スキーマ変更の影響が大きくなります。
その場合はComposhipsを境界層に置き、アプリケーション全体へ複数列の条件式を散らさない設計に利点があります。
書き込み処理で残る注意点
現在の公式リポジトリでは、モデルに$compositeKeyを宣言すると、取得済みモデルのsave()、update()、delete()、refresh()、fresh()が複数列を使って対象行を絞る機能も説明されています。
これは、同じ伝票番号が別テナントにも存在するとき、更新先を一列だけで決めてしまう危険を減らすための補助です。
しかし、Model::find()と通常のルートモデルバインディングは単一の主キーを使い続けます。
firstOrCreate()やupdateOrCreate()も渡した条件から独自に検索条件を作るため、$compositeKeyだけに任せることはできません。
パッケージの公式説明も、Eloquentの主キー処理を全面的に置き換える機能ではないと範囲を限定しています。
キューに一件の複合キー付きモデルを渡す経路と、モデルのコレクションを渡す経路にも違いがあります。
公式リポジトリでは、コレクションには専用のQueueableCompositeCollectionが必要とされているため、ジョブの直列化とワーカーでの復元まで結合テストに含めます。
導入前に確認する五つの項目
- データの意味を確定する:どの列の組み合わせが一意なのか、NULLを未設定とみなすのか、別の状態として扱うのかを決めます。
- DB制約を点検する:アプリケーションのリレーション定義とは別に、必要な一意制約と外部キー制約がDBにあるかを確認します。
- 実行計画を測る:複数列のインデックスは列順で利用条件が変わるため、代表的な検索を
EXPLAINで確認します。 - 読み書きを分けて試す:通常取得、事前読み込み、作成、更新、削除、ソフトデリート、キュー復元をそれぞれテストします。
- 更新手順を決める:Composerの固定範囲、パッケージ更新時の回帰テスト、標準Eloquentへ戻す条件を記録します。
テストでは、同じ顧客番号を持つ別テナントの行を用意し、誤った行が混ざらないことまで検証します。
正常系の一件だけでは、複数列を使う目的そのものを確認できません。
LaravelのTDDを組み立てる方法も、モデルとDBの回帰テストを設計する際の参考になります。
よくある質問
Composhipsを入れれば複合主キーを標準機能と同じように使えますか
いいえ。
複数列リレーションと一部の書き込み経路は補えますが、find()、ルートモデルバインディング、条件を独自に組み立てるヘルパーには単一キーを前提とする経路が残ります。
事前読み込みを使えばN+1問題は必ず解消しますか
事前読み込みはクエリ回数を減らす手段ですが、呼び出し位置やネストしたリレーションによっては追加クエリが残ります。
Laravelのクエリログや監視ツールで、実際のSQLと件数を確認します。
複合インデックスを追加すれば十分ですか
インデックスは検索を速くする仕組みであり、データの一意性や参照整合性を自動では保証しません。
業務上重複を許さない組み合わせには一意制約を検討し、親子関係をDBでも守る必要がある場合は外部キー制約を設計します。
採用判断の結論
Composhipsは、Laravelの標準的な単一キー設計を捨てるための道具ではなく、変更しにくいスキーマとEloquentの間をつなぐ選択肢です。
既存DBや外部データの複数列照合が避けられないときに範囲を限定して使い、単一キーのまま残る処理をテストで明示できるなら、条件式の重複と誤更新のリスクを抑えやすくなります。
参考情報
- Laravel News「Compoships: Eloquent Relationships on Multiple Columns」
- Compoships公式GitHubリポジトリ
- Composhipsのcomposer.json
- Laravel 13公式ドキュメント「Eloquent: Getting Started」
- 株式会社greeden公式サイト
この記事に関連する株式会社greedenの取り組み
複数列リレーションは、既存データを生かす設計判断と移行後の検証が成否を分けます。株式会社greedenは、要件定義から設計、実装、テスト、保守まで一貫したWebシステム開発を支援しています。
