te28

関係の問題-stitcher.io

(jp) =

言い換えれば、複雑なデータベース関係と Laravel モデルを扱うことです。

最近、大規模な Laravel プロジェクトの 1 つで、複雑なパフォーマンスの問題に対処する必要がありました。 はやく現場を整えてくれ。

管理者ユーザーに、システム内のすべての人の概要をテーブルで表示してもらい、そのテーブルの列に、各人についてその時点でアクティブな契約を一覧表示する必要があります。

間の関係 ContractPerson 以下のとおりであります:

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 そして彼らの habitantscontract_habitants ピボット テーブル、したがって 2 つの結合。

もう 1 つの制約は、アクティブなコントラクトのみが表示されるようにすることです。 これには、 Contract モデル。

基本クエリを配置したら、本当の魔法を追加します。熱心な読み込みをサポートします。 ここでパフォーマンスが向上します。1 人あたり 1 つのクエリを実行して契約を読み込むのではなく、1 つのクエリを実行してすべての契約を読み込み、後でこれらの契約を正しい人物にリンクします。

これは何 addEagerConstraintsinitRelationmatch に使用されます。 それらを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 の方法で解決するエレガントなソリューションです。

次の投稿
米国で毎年発生する最大のハリケーン
前の投稿
Destiny 2 暁旦のレシピ (2022): ギフト、報酬、変更点

ノート:

AZ: 動物の世界、ペット、ペット、野生の自然に関するカテゴリー記事…
SP:スポーツカテゴリー。
New vs Ne: ニュースコラム。
Te: テクノロジー カテゴリ。
Gt:エンターテインメントカテゴリー。
Bt: 占い、星占い、超常現象、超常現象。
Ta:人生コラム。