1. ホーム
  2. php

[解決済み】PHPで適切なリポジトリパターンを設計するには?

2022-05-10 08:10:51

質問

はじめに リレーショナルデータベースを使用したMVCアーキテクチャで、リポジトリパターンを使用しようとしています。

私は最近 PHP で TDD を学び始めましたが、データベースが私のアプリケーションの残りの部分とあまりにも密接に結合されていることに気づきました。リポジトリについて読んだことがあります。 IoC コンテナ を使用して、それをコントローラに注入しています。非常にクールな内容です。しかし、今度はリポジトリの設計について、いくつかの実用的な質問があります。次のような例を考えてみましょう。

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

問題点1:フィールドが多すぎる

これらの検索メソッドはすべて、すべてのフィールドを選択する ( SELECT * ) アプローチを使用します。しかし、私のアプリケーションでは、これはしばしばオーバーヘッドを追加し、物事を遅くするので、私は常に取得するフィールドの数を制限しようとしています。このパターンを使用している方々は、どのようにこれに対処しているのでしょうか?

問題2:多すぎるメソッド

このクラスは今はいい感じですが、実際のアプリケーションではもっとたくさんのメソッドが必要なことが分かっています。たとえば

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet(メールアドレスセット付き
  • 年齢と性別ですべてを見つける
  • FindAllByAgeAndGenderOrderByAge(年齢・性別順
  • などなど。

ご覧の通り、可能なメソッドのリストは非常に非常に長くなる可能性があります。そして、上記のフィールド選択の問題を加えると、問題はさらに悪化します。以前なら、このロジックはすべてコントローラに置くのが普通でした。

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

私のリポジトリアプローチでは、こうはなりたくない。

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

課題3: インタフェースのマッチングができない

リポジトリにインターフェイスを使用することで、(テスト目的などで)自分の実装を入れ替えることができるという利点があると思います。インターフェイスについての私の理解は、インターフェイスは、実装が従わなければならない契約を定義するということです。これは、次のような追加のメソッドをリポジトリに追加するまでは素晴らしいことです。 findAllInCountry() . さもなければ、他の実装がこのメソッドを持っていない可能性があり、私のアプリケーションを壊してしまうかもしれません。そうでなければ、他の実装がこのメソッドを持っていない可能性があり、私のアプリケーションを壊してしまうかもしれません。

仕様のパターン?

このことから、リポジトリは決まった数のメソッドしか持ってはいけないと考えます(例えば save() , remove() , find() , findAll() など)。しかし、それでは具体的にどのようにルックアップを実行すればよいのでしょうか?私が聞いたのは 指定パターン を使うことができますが、これはレコードのセット全体を減らすだけのように思えます。 IsSatisfiedBy() を経由して)、これは明らかにデータベースから引き出している場合、大きなパフォーマンスの問題があるように思えます。

ヘルプ?

明らかに、私はリポジトリで作業するときに物事を少し考え直す必要があります。これをどのように処理するのが最善か、どなたか教えていただけませんか。

どのように解決するのですか?

私は自分自身の質問に答えることに挑戦してみようと思いました。以下は、私の元の質問における問題点 1 ~ 3 を解決するための 1 つの方法にすぎません。

免責事項: パターンやテクニックを説明するとき、私は常に正しい用語を使用しないかもしれません。その点については申し訳ありません。

目標

  • 表示と編集のための基本的なコントローラの完全な例を作成する。 Users .
  • すべてのコードは、完全にテスト可能でモック可能でなければなりません。
  • コントローラはデータがどこに保存されているか分からないようにしなければなりません(つまり、変更可能であること)。
  • SQLの実装を示す例(最も一般的)。
  • パフォーマンスを最大化するために、コントローラは必要なデータのみを受け取り、余分なフィールドを持たないようにします。
  • 開発を容易にするために、ある種のデータマッパーを利用した実装にすべきです。
  • 実装は、複雑なデータ検索を実行する能力を持つべきです。

ソリューション

永続記憶 (データベース) のインタラクションを2つのカテゴリに分割しています。 R (読み込み)と CUD (Create、Update、Delete)です。私の経験では、読み取りは本当にアプリケーションの速度を低下させる原因となっています。そして、データ操作 (CUD) は実際に遅くなりますが、発生する頻度がはるかに少ないため、懸念されることははるかに少ないのです。

CUD (Create、Update、Delete)は簡単です。これには実際の モデル に渡され、それを私の Repositories に渡されます。私のリポジトリはまだReadメソッドを提供しますが、単にオブジェクトを作成するためであり、表示するためではないことに注意してください。これについては後で詳しく説明します。

R (Read)はそう簡単ではありません。ここではモデルはなく、ただ 値オブジェクト . 配列を使用する を使用します。 . これらのオブジェクトは、単一のモデル、または多くのモデルのブレンド、本当に何でも表すことができます。これらはそれ自体ではあまり面白くありませんが、それらがどのように生成されるかが重要です。私は、以下のように呼んでいるものを使っています。 Query Objects .

コードです。

ユーザーモデル

基本的なユーザーモデルでシンプルに始めましょう。ORM の拡張やデータベースに関するものは全くないことに注意してください。純粋にモデルの栄光だけです。ゲッター、セッター、バリデーション、何でも追加してください。

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

リポジトリインターフェース

ユーザーリポジトリを作成する前に、リポジトリインターフェースを作成したいと思います。これは、コントローラが使用するためにリポジトリが従わなければならない契約("contract")を定義します。私のコントローラは、実際にデータがどこに保存されているかを知らないことに注意してください。

私のリポジトリはこれら3つのメソッドのみを含むことに注意してください。 メソッドはユーザーの作成と更新の両方を担当し、ユーザーオブジェクトに id が設定されているかどうかだけに依存します。

save()

SQL リポジトリの実装

さて、インターフェースの実装を作成しましょう。前述のように、私の例では SQL データベースを使用するつもりでした。この例では データマッパー を使うことで、SQLクエリを繰り返し書くことを防いでいます。

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

クエリオブジェクトインタフェース

現在では CUD (Create、Update、Delete) はリポジトリで処理されるので、我々は R (Read)に集中できます。クエリオブジェクトは単にある種のデータ検索ロジックをカプセル化したものです。それらは ではなく クエリビルダです。これをリポジトリのように抽象化することで、実装を変更したり、テストを容易にしたりすることができます。クエリオブジェクトの例としては class SQLUserRepository implements UserRepositoryInterface { protected $db; public function __construct(Database $db) { $this->db = $db; } public function find($id) { // Find a record with the id = $id // from the 'users' table // and return it as a User object return $this->db->find($id, 'users', 'User'); } public function save(User $user) { // Insert or update the $user // in the 'users' table $this->db->save($user, 'users'); } public function remove(User $user) { // Remove the $user // from the 'users' table $this->db->remove($user, 'users'); } } あるいは AllUsersQuery あるいは AllActiveUsersQuery .

これらのクエリのためにリポジトリにメソッドを作成することはできないのでしょうか?

  • 私のリポジトリは、モデル オブジェクトを操作するためのものです。実世界のアプリケーションでは、なぜ私は MostCommonUserFirstNames フィールドを取得する必要があるのでしょうか?
  • リポジトリはしばしばモデルに依存しますが、クエリはしばしば複数のモデルを伴います。では、どのリポジトリにメソッドを置くのでしょうか?
  • これは私のリポジトリを非常にシンプルに保ちます-メソッドの肥大化したクラスではありません。
  • すべてのクエリは、独自のクラスに整理されました。
  • 本当に、この時点では、リポジトリは単に私のデータベース層を抽象化するために存在しています。

この例では、"AllUsers" を検索するためのクエリオブジェクトを作成します。以下はそのインターフェイスです。

password

クエリオブジェクトの実装

ここでもデータマッパーを使うことで、開発のスピードアップを図ることができます。返されるデータセットに1つだけ手を加えることを許可していることに注目してください。これは、実行されたクエリを操作したい範囲とほぼ同じです。私のクエリオブジェクトはクエリビルダではないことに注意してください。単に特定のクエリを実行するだけのものです。しかし、このオブジェクトはさまざまな場面で頻繁に使用することになるでしょうから、フィールドを指定できるようにしています。必要のないフィールドを返すようなことは決してしたくありません!

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

コントローラに移る前に、これがいかに強力であるかを説明するために、別の例を示したいと思います。例えば、レポートエンジンがあって、その中で class AllUsersQuery implements AllUsersQueryInterface { protected $db; public function __construct(Database $db) { $this->db = $db; } public function fetch($fields) { return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows(); } } . これはデータマッパーにとって厄介なことで、実際にいくつかの AllOverdueAccounts を書きたいかもしれません。問題ありません。このクエリオブジェクトは次のようなものです。

SQL

これで、このレポートのためのすべてのロジックを一つのクラスにうまく収めることができ、テストも簡単になりました。私は心行くまでモックを作ることができますし、完全に異なる実装を使用することさえできます。

コントローラ

さて、楽しいのはすべてのピースを一緒にすることです。依存性注入を使っていることに注意してください。一般的に依存性はコンストラクタに注入されますが、私は実際にコントローラのメソッド(ルート)に直接注入することを好みます。これにより、コントローラのオブジェクトグラフを最小化することができ、実際、より見やすくなると思います。もしこの方法が好きでないなら、従来のコンストラクタの方法を使えばいいのです。

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

最後の感想

ここで注意すべき重要なことは、私がエンティティを修正(作成、更新、削除)しているとき、私は実際のモデルオブジェクトを操作しており、リポジトリを通じて永続化を実行していることです。

しかし、表示(データを選択してビューに送信)しているときは、モデルオブジェクトではなく、単なる古い値オブジェクトを操作しています。私は必要なフィールドだけを選択し、データ検索のパフォーマンスを最大化できるように設計されています。

私のリポジトリは非常にクリーンで、その代わりに、このquot;mess"は私のモデルクエリに整理されています。

一般的なタスクのために繰り返しSQLを書くのは馬鹿げているので、私は開発を助けるためにデータマッパーを使用しています。しかし、必要な場所 (複雑なクエリー、レポート作成など) では、絶対に SQL を書くことができます。そして、そのような場合は、適切に命名されたクラスにうまく格納されます。

私のアプローチに対するあなたの意見を聞きたいと思います!


2015年7月の更新情報です。

コメントで「結局どこに行き着いたんですか?まあ、実際にはそれほど外れてはいないのですが。実のところ、私はまだリポジトリがあまり好きではありません。基本的な検索には過剰であり (特に ORM をすでに使用している場合)、より複雑なクエリで作業する場合は厄介だと思います。

私は一般的にActiveRecordスタイルのORMで作業するので、ほとんどの場合、私のアプリケーションを通してこれらのモデルを直接参照します。しかし、より複雑なクエリを持っている状況では、これらをより再利用可能にするために、クエリオブジェクトを使用します。また、私は常にモデルをメソッドに注入し、テストでのモック作成を容易にしています。