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


291

序文:リレーショナルデータベースを使用する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
  • findAllByAgeAndGender
  • 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())レコードのセット全体しか削減しないようです。データベースからプルしている場合は、明らかに大きなパフォーマンスの問題があります。

助けて?

明らかに、リポジトリで作業するときは、少し考え直す必要があります。これがどのように最もよく処理されるかについて誰かが啓発できますか?

回答:


208

自分の質問に答えるのに一苦労すると思いました。以下は、私の元の質問の問題1〜3を解決する1つの方法にすぎません。

免責事項:パターンや手法を説明するときに、常に正しい用語を使用しているとは限りません。そのために残念。

目標:

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

ソリューション

永続ストレージ(データベース)の相互作用をR(読み取り)とCUD(作成、更新、削除)の2つのカテゴリーに分割しています。私の経験では、読み取りが実際にアプリケーションの速度を低下させる原因になっています。また、データ操作(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(読み取り)。クエリオブジェクトは、ある種のデータルックアップロジックのカプセル化にすぎません。クエリビルダーではありません。リポジトリのように抽象化することで、実装を変更し、テストを簡単に行うことができます。クエリオブジェクトの例としては、AllUsersQueryまたはAllActiveUsersQuery、またはがあり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つのクラスにうまく収められ、テストも簡単です。私はそれを心のこもった内容に模倣することができます、または完全に別の実装を使用することさえできます。

コントローラー

楽しい部分です。すべての要素を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を使用するため、ほとんどの場合、アプリケーション全体でこれらのモデルを直接参照します。ただし、より複雑なクエリがある場合は、クエリオブジェクトを使用して、クエリをより再利用できるようにします。また、モデルを常にメソッドに挿入しているため、テストでモックしやすくなっています。


4
@PeeHaa繰り返しますが、それは例を単純に保つことでした。手元のトピックに特に関係のないコードの一部は、例から除外するのが一般的です。実際には、依存関係を渡します。
ジョナサン

4
興味深いことに、読み取りから作成、更新、削除を分割しています。正式にそれを行うCommand Query Responsibility Segregation(CQRS)について言及する価値があると思います。martinfowler.com/bliki/CQRS.html
Adam

2
@Jonathan自分の質問に答えてから1年半になります。私はあなたがまだあなたの答えに満足しているかどうか、そしてこれが現在あなたのプロジェクトのほとんどの主な解決策であるかどうか疑問に思っていましたか?過去数週間、リポジトリの割り当てを読んでいて、割り当ての実装方法について、多くの人が独自の解釈を持っているのを見てきました。あなたはそれをクエリオブジェクトと呼んでいますが、これは既存のパターンですよね?他の言語で使われているのを見たと思います。
Boedy 2014年

1
@ジョナサン:ユーザーを「ID」ではなく、たとえば「ユーザー名」または複数の条件を持つさらに複雑なクエリにする必要があるクエリをどのように処理しますか?
Gizzmo 2014年

1
@Gizzmoクエリオブジェクトを使用して、さらに複雑なクエリに役立つパラメータを渡すことができます。たとえば、コンストラクタでこれを行うことができますnew Query\ComplexUserLookup($username, $anotherCondition)。または、setterメソッドを使用してこれを行います$query->setUsername($username);。実際にこれを設計することはできますが、特定のアプリケーションにとっては理にかなっており、クエリオブジェクトはここで多くの柔軟性を残していると思います。
ジョナサン

48

私の経験に基づいて、ここにあなたの質問に対するいくつかの答えがあります:

Q:不要なフィールドを戻すにはどうすればよいですか?

A:私の経験から言うと、これは、完全なエンティティとアドホッククエリのどちらを扱うかということになります。

完全なエンティティはUserオブジェクトのようなものです。プロパティやメソッドなどがあります。コードベースの第一級市民です。

アドホッククエリは一部のデータを返しますが、それ以上はわかりません。データはアプリケーションの周りを通過するため、コンテキストなしで行われます。それはUser?A UserいくつかのOrder情報が添付されていますか?本当にわかりません。

私は完全なエンティティで作業することを好みます。

使用しないデータを頻繁に戻すことは正しいですが、これにはさまざまな方法で対処できます。

  1. エンティティを積極的にキャッシュして、データベースからの読み取り価格を1回だけ支払う。
  2. エンティティのモデル化により多くの時間を費やして、エンティティを明確に区別できるようにします。(大きなエンティティを2つの小さなエンティティに分割することなどを検討してください)
  3. エンティティの複数のバージョンを持つことを検討してください。あなたUserはバックエンドのために、そしておそらくUserSmallAJAX呼び出しのために持つことができます。1つには10個のプロパティがあり、もう1つには3つのプロパティがあります。

アドホッククエリを使用する場合の欠点:

  1. 多くのクエリで基本的に同じデータが得られます。たとえば、を使用するUserselect *、多くの呼び出しで基本的に同じ内容を書き込むことになります。1回の呼び出しで10フィールドのうち8フィールドを取得し、1つは10フィールドのうち5つを取得し、1つは7フィールドのうち10つを取得します。これが悪い理由は、リファクタリング/テスト/モックすることが殺人であるからです。
  2. 時間の経過とともに、コードについて高レベルで推論することは非常に困難になります。「なぜUserこんなに遅いのですか?」1回限りのクエリを追跡することになるため、バグ修正は小規模でローカライズされる傾向があります。
  3. 基盤となるテクノロジーを置き換えることは本当に難しいです。すべてをMySQLに保存してMongoDBに移行したい場合、100個のアドホックコールを置き換えるのは、少数のエンティティよりもはるかに困難です。

Q:リポジトリにメソッドが多すぎます。

A:通話を統合する以外に、これを回避する方法は見たことがありません。リポジトリのメソッド呼び出しは、実際にはアプリケーションの機能にマッピングされます。より多くの機能、より多くのデータ固有の呼び出し。機能を押し戻して、同様の呼び出しを1つにマージすることができます。

結局のところ、複雑さはどこかに存在している必要があります。リポジトリー・パターンを使用して、ストアード・プロシージャーの束を作成する代わりに、それをリポジトリー・インターフェースにプッシュしました。

時々私は、「それはどこかに与える必要がありました!銀の弾丸はありません」と自分自身に言わなければなりません。


非常に完全な答えをありがとう。あなたは私に今考えさせられました。ここでの私の大きな懸念は、私が読んだすべてが言うことではなくSELECT *、必要なフィールドのみを選択することです。たとえば、この質問を参照してください。あなたが話すそれらすべてのアドホッククエリについては、私は確かにあなたがどこから来ているのか理解しています。現在、非常に大きなアプリがあり、その多くが含まれています。それは私の「まあ、どこかに与える必要があった!」でした。瞬間、私は最大のパフォーマンスを選びました。ただし、今はさまざまなクエリを処理しています。
ジョナサン

1
1つのフォローアップの考え。R-CUDアプローチを使用することをお勧めします。以来reads、パフォーマンスの問題が発生した場合、多くの場合、あなたは本当のビジネス・オブジェクトに変換されませんので、それらのためのより多くのカスタムクエリのアプローチを使用することができています。その後、用createupdateおよびdelete全体のオブジェクトで動作するORMを使用し、。そのアプローチについて何か考えはありますか?
ジョナサン

1
「select *」の使用上の注意として。私は過去にそれをやったことがあり、それは大丈夫です-varchar(max)フィールドに到達するまでは。それらは私たちのクエリを殺しました。したがって、intや小さなテキストフィールドなどを含むテーブルがある場合、それほど悪くはありません。不自然に感じますが、ソフトウェアはそうです。悪いことは突然良いことであり、逆もまた同様です。
ryan1234 2013

1
R-CUDアプローチは実際にはCQRSです
MikeSW

2
@ ryan1234「結局のところ、複雑さはどこかに存在している必要があります。」これありがとう。気分が良くなります。
ジョニー2014

20

私は次のインターフェースを使用します:

  • Repository -エンティティの読み込み、挿入、更新、削除
  • Selector -リポジトリで、フィルターに基づいてエンティティを検索します
  • Filter -フィルタリングロジックをカプセル化します

Repositoryはデータベースにとらわれません。実際、永続性は指定されていません。SQLデータベース、xmlファイル、リモートサービス、宇宙からの異星人など、何でもかまいません。検索機能のために、Repository構造は、Selectorフィルター処理、LIMIT抽出、ソート、およびカウントが可能なを構築します。最後に、セレクターEntitiesは永続性から1つ以上をフェッチします。

ここにいくつかのサンプルコードがあります:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

次に、1つの実装:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

アイデアは、ジェネリックがSelector使用するFilterが、実装SqlSelectorは使用するということSqlFilterです。はSqlSelectorFilterAdapterジェネリックFilterを具象に適応させSqlFilterます。

クライアントコードはFilterオブジェクト(汎用フィルター)を作成しますが、セレクターの具体的な実装では、これらのフィルターはSQLフィルターに変換されます。

などの他のセレクターの実装は、固有のを使用するようInMemorySelectorに変換されます。そのため、すべてのセレクターの実装には独自のフィルターアダプターが付属しています。FilterInMemoryFilterInMemorySelectorFilterAdapter

この戦略を使用すると、(bussinesレイヤーの)クライアントコードは特定のリポジトリーまたはセレクターの実装を気にしません。

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

PSこれは私の実際のコードの簡略化です


「リポジトリ-エンティティの読み込み、挿入、更新、削除」これは「サービスレイヤー」、「DAO」、「BLL」で実行できることです
Yousha Aleayoub

5

私は現在これをすべて自分で把握しようとしているので、これに少し追加します。

#1と2

これは、ORMが重い作業を行うのに最適な場所です。ある種のORMを実装するモデルを使用している場合は、そのメソッドを使用してこれらのことを処理できます。必要に応じて、Eloquentメソッドを実装する独自のorderBy関数を作成します。Eloquentを使用する例:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

あなたが探しているように見えるのはORMです。リポジトリをベースにすることができない理由はありません。これはユーザーが雄弁に話すことを必要とするでしょうが、私は個人的にはそれを問題として見ていません。

ただし、ORMを避けたい場合は、「自分でロール」して、探しているものを取得する必要があります。

#3

インターフェイスは、ハードで高速な要件ではありません。何かがインターフェースを実装し、それに追加することができます。それができないことは、そのインターフェースの必要な機能を実装することに失敗することです。クラスのようなインターフェースを拡張して、物事をドライに保つこともできます。

とは言っても、私は理解を始めたばかりですが、これらの実現は私を助けてくれました。


1
このメソッドの嫌いな点は、MongoUserRepositoryがある場合、それとDbUserRepositoryが異なるオブジェクトを返すことです。DbはEloquent \ Modelを返し、Mongoは独自のものを返します。確かに、より良い実装は、両方のリポジトリが別々のEntity \ Userクラスのインスタンス/コレクションを返すようにすることです。これにより、MongoRepositoryを使用するように切り替えたときに、Eloquent \ ModelのDBメソッドに誤って依存することがなくなります
danharper

1
私は間違いなくあなたに同意します。それを回避するために私がおそらく行うことは、Eloquent要求クラスの外部でこれらのメソッドを使用しないことです。したがって、get関数はおそらくプライベートであり、クラス内でのみ使用する必要があります。指摘したように、他のリポジトリではできなかったものを返すからです。
2013

3

私が(私の会社で)これに対処する方法についてのみコメントできます。まず第一に、パフォーマンスはそれほど問題ではありませんが、クリーン/適切なコードを持つことです。

まずUserModel、ORMを使用してUserEntityオブジェクトを作成するなどのモデルを定義します。ときにUserEntityモデルからロードされているすべてのフィールドがロードされます。外部エンティティを参照するフィールドでは、適切な外部モデルを使用してそれぞれのエンティティを作成します。これらのエンティティの場合、データはオンデマンドで読み込まれます。今、あなたの最初の反応は... ??? ... !!! 少し例を挙げましょう。

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

私たちの場合$db、エンティティをロードできるORMです。モデルは、特定のタイプのエンティティのセットをロードするようORMに指示します。ORMにはマッピングが含まれており、それを使用して、そのエンティティのすべてのフィールドをエンティティに挿入します。ただし、外部フィールドの場合、それらのオブジェクトのIDのみがロードされます。この場合、は、参照される注文のIDのみを使用してをOrderModel作成しますOrderEntity。ときPersistentEntity::getFieldによって呼び出されるOrderEntityエンティティへのすべてのフィールド遅延ロードへのモデルに指示OrderEntity秒。OrderEntity1つのUserEntityに関連付けられているすべてのは1つの結果セットとして扱われ、一度に読み込まれます。

ここでの魔法は、モデルとORMがすべてのデータをエンティティに挿入し、エンティティが提供するジェネリックgetFieldメソッドのラッパー関数を提供するだけであることPersistentEntityです。要約すると、常にすべてのフィールドをロードしますが、外部エンティティを参照するフィールドは必要に応じてロードされます。一連のフィールドをロードするだけでは、パフォーマンスの問題にはなりません。考えられるすべての外部エンティティをロードすると、パフォーマンスが大幅に低下します。

次に、where句に基づいて、特定のユーザーセットをロードします。一緒に接着できる単純な式を指定できるクラスのオブジェクト指向パッケージを提供します。コード例では、私はそれに名前を付けましたGetOptions。これは、選択クエリのすべての可能なオプションのラッパーです。where句、group by句、その他すべてのコレクションが含まれています。where句はかなり複雑ですが、単純なバージョンを簡単に作成できることは明らかです。

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

このシステムの最も簡単なバージョンは、クエリのWHERE部分を文字列として直接モデルに渡すことです。

この非常に複雑な応答でごめんなさい。私は、フレームワークをできるだけ迅速かつ明確に要約しようとしました。他にご不明な点がございましたら、お気軽にお問い合わせください。回答を更新いたします。

編集:さらに、いくつかのフィールドをすぐにロードしたくない場合は、ORMマッピングで遅延ロードオプションを指定できます。すべてのフィールドは最終的にgetFieldメソッドを介してロードされるため、そのメソッドが呼び出されたときに最後に一部のフィールドをロードできます。これはPHPではそれほど大きな問題ではありませんが、他のシステムにはお勧めしません。


3

これらは私が見たいくつかの異なる解決策です。それぞれに長所と短所がありますが、それはあなたが決めることです。

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

これは、特にインデックスのみのスキャンを考慮に入れる場合に重要な側面です。この問題に対処するには2つの解決策があります。関数を更新して、返す列のリストを含むオプションの配列パラメーターを取り込むことができます。このパラメーターが空の場合、クエリのすべての列が返されます。これは少し奇妙な場合があります。パラメータに基づいて、オブジェクトまたは配列を取得できます。すべての関数を複製して、同じクエリを実行する2つの異なる関数を作成することもできますが、一方は列の配列を返し、もう一方はオブジェクトを返します。

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

問題#2:方法が多すぎる

私は1年前にPropel ORMで簡単に作業しましたが、これはその経験から覚えていることを基にしています。Propelには、既存のデータベーススキーマに基づいてクラス構造を生成するオプションがあります。各テーブルに2つのオブジェクトを作成します。最初のオブジェクトは、現在リストしているものに類似したアクセス関数の長いリストです。findByAttribute($attribute_value)。次のオブジェクトはこの最初のオブジェクトを継承します。この子オブジェクトを更新して、より複雑なゲッター関数を組み込むことができます。

別の解決策は、__call()未定義の関数をアクション可能なものにマップするために使用することです。あなたの__callメソッドは、findByIdとfindByNameを異なるクエリに解析できるでしょう。

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

これが少なくとも何とか役立つことを願っています。



0

@ ryan1234に同意します。コード内で完全なオブジェクトを渡し、それらのオブジェクトを取得するには汎用クエリメソッドを使用する必要があります。

Model::where(['attr1' => 'val1'])->get();

外部/エンドポイントの使用については、GraphQLメソッドが本当に好きです。

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

問題#3:インターフェースを一致させることが不可能

リポジトリにインターフェースを使用することにはメリットがあるので、(テストなどの目的で)実装を入れ替えることができます。インターフェースについての私の理解は、実装が従わなければならないコントラクトを定義することです。これは、findAllInCountry()のような追加のメソッドをリポジトリに追加し始めるまではすばらしいことです。このメソッドを持つようにインターフェイスを更新する必要があります。そうしないと、他の実装にこのメソッドがない場合があり、アプリケーションが壊れる可能性があります。これにより、非常識に感じます...尾が犬を振っているケース。

私の直感は、これには多分ジェネリックメソッドと共にクエリ最適化メソッドを実装するインターフェイスが必要であることを教えてくれます。パフォーマンスに敏感なクエリにはターゲットメソッドが必要ですが、頻度の低いクエリや軽量のクエリはジェネリックハンドラーによって処理されますが、コントローラーが少しジャグリングを行うための費用がかかる可能性があります。

ジェネリックメソッドでは、任意のクエリを実装できるため、移行期間中に重大な変更が行われなくなります。ターゲットを絞ったメソッドを使用すると、必要に応じて呼び出しを最適化でき、複数のサービスプロバイダーに適用できます。

このアプローチは、特定の最適化されたタスクを実行するハードウェア実装に似ていますが、ソフトウェア実装は、軽作業または柔軟な実装を行います。


0

そのような場合、データリポジトリの複雑さを増すことなく大規模なクエリ言語を提供するには、graphQL適していると思います。

ただし、今のところgraphQLを使いたくない場合は、別の解決策があります。オブジェクトがプロセス間、この場合はサービス/コントローラーとリポジトリー間でデータを運ぶために使用されるDTOを使用する。

上でエレガントな答えがすでに提供されていますが、私はそれがより簡単で、新しいプロジェクトの出発点として役立つと思う別の例を挙げてみましょう。

コードに示すように、CRUD操作に必要なメソッドは4つだけです。このfindメソッドは、オブジェクト引数を渡すことにより、リストおよび読み取りに使用されます。バックエンドサービスは、URLクエリ文字列または特定のパラメーターに基づいて、定義されたクエリオブジェクトを構築できます。

クエリオブジェクト(SomeQueryDto)は、必要に応じて特定のインターフェイスを実装することもできます。複雑さを増すことなく、後で簡単に拡張できます。

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

使用例:

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.