(jp) =
言い換えれば、複雑なデータベース関係と Laravel モデルを扱うことです。
最近、大規模な Laravel プロジェクトの 1 つで、複雑なパフォーマンスの問題に対処する必要がありました。 はやく現場を整えてくれ。
管理者ユーザーに、システム内のすべての人の概要をテーブルで表示してもらい、そのテーブルの列に、各人についてその時点でアクティブな契約を一覧表示する必要があります。
間の関係 Contract
と Person
以下のとおりであります:
Contract > HabitantContract > Habitant > Person
どのようにしてこの関係階層に到達したかについて詳しく説明するのに時間をかけたくありません。 はい、この階層がユースケースにとって重要であることを知っておくことが重要です。 Contract
いくつか持つことができます Habitants
、ピボットモデルを介してリンクされています HabitantContract
; そしてそれぞれ Habitant
と関係がある Person
.
すべての人の概要を表示しているので、コントローラーで次のようなことをしたいと思います。
class PeopleController
public function index()
$people = PersonResource::collection(Person::paginate());
return view('people.index', compact('people'));
要点を理解していただければ幸いですが、これは単純化しすぎた例であることを明確にしましょう。 理想的には、リソース クラスは次のようになります。
class PersonResource extends JsonResource
public function toArray($request): array
return [
'name' => $this->name,
'active_contracts' => $this->activeContracts
->map(function (Contract $contract)
return $contract->contract_number;
)
->implode(', '),
];
特に注意してください Person::activeContracts
関係。 どうすればこれを機能させることができるでしょうか?
最初の考えは、 HasManyThrough
しかし、関係階層の深さは 4 レベルであることを思い出してください。 それに加えて、私は見つけます HasManyThrough
非常に混乱する。
その場で契約を 1 人 1 つずつ照会できます。 問題は、追加のクエリがあるため、n+1 の問題が発生することです。 あたり 人。 少数のモデルを扱っている場合のパフォーマンスへの影響を想像してみてください。
頭に浮かんだ最後の解決策は、すべての人、すべての契約を読み込み、それらを手動でマッピングすることでした。 最終的には、これがまさに私がやったことですが、カスタム リレーションを使用するという最もクリーンな方法で行いました。
飛び込みましょう。
# Person モデルの設定
私たちが欲しいので $person->activeContracts
他のリレーションとまったく同じように機能するために、ここで行う作業はほとんどありません。他のリレーションと同様に、モデルにリレーション メソッドを追加しましょう。
class Person extends Model
public function activeContracts(): ActiveContractsRelation
return new ActiveContractsRelation($this);
ここで行うことはこれ以上ありません。 もちろん、実際には実装していないので、まだ始まったばかりです。 ActiveContractsRelation
!
# カスタム関係クラス
残念ながら、独自のリレーション クラスの作成に関するドキュメントはありません。 幸いなことに、それらについて学ぶのにそれほど多くは必要ありません。いくつかのコード ダイビング スキルと少しの時間で、かなりのことを習得できます。 ああ、IDEも役立ちます。
Laravel が提供する既存のリレーション クラスを見ると、それらすべてを支配する 1 つの基本リレーションがあることがわかります。 Illuminate\Database\Eloquent\Relations\Relation
. それを拡張するということは、いくつかの抽象メソッドを実装する必要があることを意味します。
class ActiveContractsRelation extends Relation
public function addConstraints()
public function addEagerConstraints(array $models)
public function initRelation(array $models, $relation)
public function match(array $models, Collection $results, $relation)
public function getResults()
何が起こる必要があるかは常に完全に明確であるとは限りませんが、doc ブロックは私たちを道へと導きます。 ここでも幸運なことに、Laravel には参照できる既存のリレーション クラスがまだいくつかあります。
カスタム リレーション クラスを段階的に構築していきましょう。 コンストラクターをオーバーライドし、既存のプロパティにいくつかの型ヒントを追加することから始めます。 念のために言っておきますが、型システムは私たちが愚かな間違いを犯すのを防いでくれます。
アブストラクト Relation
コンストラクターは、特に雄弁な人のために両方を必要とします Builder
クラス、および関係が属する親モデル。 の Builder
関連するモデルのベース クエリ オブジェクトとなることを意図しており、 Contract
、 私たちの場合には。
ユースケース専用の関係クラスを構築しているため、ビルダーを構成可能にする必要はありません。 コンストラクタは次のようになります。
class ActiveContractsRelation extends Relation
protected $query;
protected $parent;
public function __construct(Person $parent)
parent::__construct(Contract::query(), $parent);
ヒントを入力することに注意してください $query
両方とも Contract
モデルだけでなく、 Builder
クラス。 これにより、IDE は、モデル クラスで定義されたカスタム スコープなど、より優れたオートコンプリートを提供できます。
リレーションが構築されました: クエリが実行されます Contract
モデルを作成し、 Person
モデルを親として。 クエリの作成に進みます。
これは、 addConstraints
メソッドが入ります。これは、基本クエリを構成するために使用されます。 これにより、ニーズに合わせてリレーション クエリが設定されます。 これは、ほとんどのビジネス ルールが含まれる場所です。
- 有効な契約のみを表示したい
- 特定の個人に属する有効な契約のみをロードしたい (
$parent
私たちの関係の) - 他のリレーションを熱心にロードしたいかもしれませんが、それについては後で詳しく説明します。
これは何ですか addConstraints
今のところ、次のようになります。
class ActiveContractsRelation extends Relation
public function addConstraints()
$this->query
->whereActive()
->join(
'contract_habitants',
'contract_habitants.contract_id',
'=',
'contracts.id'
)
->join(
'habitants',
'habitants.id',
'=',
'contract_habitants.habitant_id'
);
ここで、基本的な結合がどのように機能するかを知っていることを前提としています。 ここで何が起こっているかを要約しますが、すべてをロードするクエリを作成しています contracts
そして彼らの habitants
、 contract_habitants
ピボット テーブル、したがって 2 つの結合。
もう 1 つの制約は、アクティブなコントラクトのみが表示されるようにすることです。 これには、 Contract
モデル。
基本クエリを配置したら、本当の魔法を追加します。熱心な読み込みをサポートします。 ここでパフォーマンスが向上します。1 人あたり 1 つのクエリを実行して契約を読み込むのではなく、1 つのクエリを実行してすべての契約を読み込み、後でこれらの契約を正しい人物にリンクします。
これは何 addEagerConstraints
、 initRelation
と match
に使用されます。 それらを1つずつ見てみましょう。
まず、 addEagerConstraints
方法。 これにより、クエリを変更して、一連の人々に関連するすべての契約を読み込むことができます。 必要なクエリは 2 つだけであり、後で結果をリンクすることを忘れないでください。
class ActiveContractsRelation extends Relation
public function addEagerConstraints(array $people)
$this->query->whereIn(
'habitants.contact_id',
collect($people)->pluck('id')
);
に加入して以来、 habitants
前の表にあるように、この方法はかなり簡単です。提供された人々のセットに属する契約のみをロードします。
次は initRelation
. 繰り返しますが、これはかなり簡単です。その目標は、空を初期化することです activeContract
関係 Person
後で入力できるようにします。
class ActiveContractsRelation extends Relation
public function initRelation(array $people, $relation)
foreach ($people as $person)
$person->setRelation(
$relation,
$this->related->newCollection()
);
return $people;
注意してください $this->related
プロパティは親によって設定されます Relation
クラスであり、これはベース クエリのクリーンなモデル インスタンスです。つまり、空の Contract
モデル:
abstract class Relation
public function __construct(Builder $query, Model $parent)
$this->related = $query->getModel();
最後に、問題を解決するコア機能にたどり着きます。それは、すべての人と契約を結びつけることです。
class ActiveContractsRelation extends Relation
public function match(array $people, Collection $contracts, $relation)
if ($contracts->isEmpty())
return $people;
foreach ($people as $person)
$person->setRelation(
$relation,
$contracts->filter(function (Contract $contract) use ($person)
return $contract->habitants->pluck('person_id')->contains($person->id);
)
);
return $people;
ここで何が起こっているのか見ていきましょう。一方では、親モデルである人々の配列があります。 一方、リレーション クラスによって実行されたクエリの結果であるコントラクトのコレクションがあります。 の目標 match
機能はそれらを結合することです。
これを行う方法? それほど難しいことではありません。すべての人をループし、その契約に関連付けられている居住者に基づいて、それぞれの人に属するすべての契約を検索します。
ほぼ完了しました? うーん…もう1つ問題があります。 を使用しているため、 $contract->habitants
そうしないと、問題を解決する代わりに n+1 の問題を移動しただけです。 それで、元に戻ります addEagerConstraints
方法はちょっと。
class ActiveContractsRelation extends Relation
public function addEagerConstraints(array $people)
$this->query
->whereIn(
'habitants.contact_id',
collect($people)->pluck('id')
)
->with('habitants')
->select('contracts.*');
を追加しています with
すべての居住者を熱心にロードするように呼び出しますが、特定の select
声明。 Laravel のクエリ ビルダーに、 contracts
そうしないと、関連する居住者データがテーブルにマージされます。 Contract
モデルが間違った ID を持っている原因となります。
最後に、実装する必要があります getResults
単純にクエリを実行するメソッド:
class ActiveContractsRelation extends Relation
public function getResults()
return $this->query->get();
以上です! カスタムリレーションは、他の Laravel リレーションと同じように使用できるようになりました。 これは、複雑な問題を Laravel の方法で解決するエレガントなソリューションです。