PHP での適切なリポジトリパターン設計? 質問する

PHP での適切なリポジトリパターン設計? 質問する

前書き: リレーショナル データベースを使用した 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: メソッドが多すぎる

このクラスは今のところ良さそうですが、実際のアプリではさらに多くのメソッドが必要になることはわかっています。たとえば、

  • 名前とステータスですべて検索
  • 国別検索
  • すべてのメールアドレスを検索
  • 年齢と性別ですべて検索
  • 年齢と性別ですべて検索年齢順
  • 等。

ご覧のとおり、考えられる方法のリストは非常に長くなります。さらに、上記のフィールド選択の問題を追加すると、問題はさらに悪化します。以前は、通常、このロジックをすべてコントローラーに直接配置していました。

<?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

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

免責事項: パターンやテクニックを説明するときに、必ずしも適切な用語を使用するとは限りません。申し訳ありません。

目標:

  • 表示および編集用の基本コントローラーの完全な例を作成しますUsers
  • すべてのコードは完全にテスト可能かつモック可能である必要があります。
  • コントローラーは、データがどこに保存されているかを認識しない必要があります (つまり、変更される可能性があります)。
  • SQL 実装を示す例 (最も一般的)。
  • パフォーマンスを最大限に高めるには、コントローラーは追加のフィールドなしで必要なデータのみを受信する必要があります。
  • 開発を容易にするために、実装では何らかのタイプのデータ マッパーを活用する必要があります。
  • 実装では、複雑なデータ検索を実行する機能が必要です。

ソリューション

私は永続ストレージ (データベース) のインタラクションを 2 つのカテゴリ、R (読み取り) とCUD (作成、更新、削除) に分割しています。私の経験では、読み取りがアプリケーションの速度低下の原因となっています。データ操作 (CUD) は実際には低速ですが、頻度ははるかに低いため、それほど問題にはなりません。

CUD(作成、更新、削除)は簡単です。実際の作業が必要になります。モデル、これらはRepositories永続化のために my に渡されます。リポジトリは引き続き Read メソッドを提供しますが、これは表示用ではなくオブジェクトの作成用です。これについては後で詳しく説明します。

R(読む)はそんなに簡単ではありません。ここにはモデルはありません。値オブジェクト配列を使用するご希望の場合はこれらのオブジェクトは、単一のモデル、または複数のモデルのブレンドを表すことができます。これらはそれ自体ではあまり興味深いものではありませんが、その生成方法は興味深いものです。私は と呼んでいるものを使用していますQuery Objects

コード:

ユーザーモデル

基本的なユーザー モデルから簡単に始めましょう。ORM 拡張やデータベース関連は一切ありません。純粋なモデルのみのメリットです。ゲッター、セッター、検証などを追加してください。

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

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

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

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

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

SQL リポジトリの実装

次はインターフェースの実装を作成します。前述のように、私の例ではSQLデータベースを使用します。データマッパー繰り返しの SQL クエリを記述する必要がなくなります。

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');
    }
}

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

CUD (作成、更新、削除)がリポジトリによって処理されるようになったので、R (読み取り) に集中できます。クエリ オブジェクトは、単に何らかのデータ検索ロジックをカプセル化したものです。クエリ ビルダーではありません。リポジトリのように抽象化することで、実装を変更し、テストを簡単に行うことができます。クエリ オブジェクトの例としては、AllUsersQueryAllActiveUsersQuery、さらには などが挙げられますMostCommonUserFirstNames

「これらのクエリ用のメソッドをリポジトリ内に作成するだけでよいのでは?」とお考えかもしれません。確かにそうですが、私がこれを行わない理由は次のとおりです。

  • 私のリポジトリはモデル オブジェクトを操作するためのものです。実際のアプリでは、passwordすべてのユーザーを一覧表示したい場合に、フィールドを取得する必要があるのはなぜでしょうか?
  • リポジトリはモデル固有であることが多いですが、クエリには複数のモデルが含まれることがよくあります。では、どのリポジトリにメソッドを配置しますか?
  • これにより、リポジトリが非常にシンプルになり、メソッドのクラスが肥大化することがなくなります。
  • すべてのクエリが独自のクラスに整理されるようになりました。
  • 実際、この時点では、リポジトリはデータベース レイヤーを抽象化するためにのみ存在します。

この例では、「AllUsers」を検索するクエリ オブジェクトを作成します。インターフェイスは次のとおりです。

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

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

ここで、データ マッパーを再び使用して開発をスピードアップできます。返されたデータセット (フィールド) に 1 つの調整を許可していることに注意してください。実行されたクエリを操作するのは、ここまでです。クエリ オブジェクトはクエリ ビルダーではないことに注意してください。特定のクエリを実行するだけです。ただし、さまざまな状況でこのオブジェクトを頻繁に使用する可能性が高いため、フィールドを指定できるようにしています。必要のないフィールドを返すことは絶対に避けたいからです。

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...";
    }
}

これにより、このレポートのすべてのロジックが 1 つのクラスにうまく保持され、テストが簡単になります。好きなだけモックを作成したり、まったく異なる実装を使用したりすることもできます。

コントローラー

次は楽しい部分です。すべてのピースをまとめます。依存性注入を使用していることに注意してください。通常、依存性はコンストラクターに注入されますが、私はコントローラー メソッド (ルート) に直接注入することを好みます。これにより、コントローラーのオブジェクト グラフが最小限に抑えられ、実際に読みやすくなります。この方法が気に入らない場合は、従来のコンストラクター メソッドを使用してください。

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

最終的な考え:

ここで注目すべき重要な点は、エンティティを変更 (作成、更新、または削除) するときに、実際のモデル オブジェクトを操作し、リポジトリを通じて永続化を実行していることです。

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

私のリポジトリは非常にクリーンな状態に保たれており、代わりにこの「混乱」はモデルクエリに整理されています。

一般的なタスクに繰り返し SQL を書くのは馬鹿げているので、開発を支援するためにデータ マッパーを使用します。ただし、必要な場合 (複雑なクエリ、レポートなど) には SQL を記述できます。記述すると、適切に名前が付けられたクラスにうまく格納されます。

私のアプローチについてのあなたの意見をぜひ聞かせてください!


2015年7月更新:

コメントで、どうしてこのような結論に至ったのかと聞かれました。まあ、実際はそれほど間違ってはいません。正直に言うと、私はまだリポジトリはあまり好きではありません。基本的な検索には過剰だと思いますし (特に ORM をすでに使用している場合)、より複雑なクエリを扱うときには扱いにくいと思います。

私は通常、ActiveRecord スタイルの ORM を使用しているため、ほとんどの場合、アプリケーション全体でそれらのモデルを直接参照するだけです。ただし、より複雑なクエリがある場合は、クエリ オブジェクトを使用して、これらをより再利用できるようにします。また、常にモデルをメソッドに挿入して、テストで簡単にモックできるようにしていることにも注意してください。

おすすめ記事