序文:リレーショナルデータベースを使用する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)
{
}
}
これらの検索メソッドはすべて、全フィールド選択(SELECT *
)アプローチを使用します。ただし、私のアプリでは、取得するフィールドの数を常に制限しようとしています。これにより、オーバーヘッドが増え、処理が遅くなることがよくあります。このパターンを使用している場合、これにどのように対処しますか?
このクラスは今はすてきに見えますが、実際のアプリではもっと多くのメソッドが必要であることを知っています。例えば:
ご覧のとおり、可能なメソッドのリストは非常に長い場合があります。そして、上記のフィールド選択の問題を追加すると、問題は悪化します。過去には、通常、このすべてのロジックをコントローラーに配置していました。
<?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))
}
}
リポジトリーにインターフェースを使用することには利点があるので、実装を交換することができます(テスト目的など)。インターフェイスの私の理解は、実装が従わなければならない契約を定義するということです。 findAllInCountry()
のような追加のメソッドをリポジトリに追加し始めるまで、これは素晴らしいことです。ここで、このメソッドを持つようにインターフェイスを更新する必要があります。そうしないと、他の実装がこのメソッドを持たない可能性があり、それによってアプリケーションが壊れる可能性があります。これにより狂ったように感じる...犬を振る尾の場合。
これは、リポジトリに固定数のメソッド(save()
、remove()
、find()
、findAll()
など)のみを含めるべきだと考えるように導きます。しかし、特定のルックアップを実行するにはどうすればよいですか? 仕様パターン を聞いたことがありますが、これはレコードのセット全体を(IsSatisfiedBy()
を介して)減らすだけであるように思えます。
リポジトリを操作するときは、明らかに物事を少し考え直す必要があります。誰がこれが最適に処理されるかについて啓発できますか?
私は自分の質問に答えるのに苦労すると思った。以下は、元の質問の問題1〜3を解決する1つの方法です。
免責事項:パターンまたはテクニックを説明するときに、常に正しい用語を使用するとは限りません。そのために残念。
Users
を表示および編集するための基本的なコントローラーの完全な例を作成します。永続ストレージ(データベース)の対話を2つのカテゴリに分割しています:R(読み取り)とCUD(作成、更新、削除)。私の経験では、読み取りは実際にアプリケーションの速度を低下させるものです。また、データ操作(CUD)は実際には低速ですが、発生頻度ははるかに低いため、心配する必要はほとんどありません。
CUD(作成、更新、削除)は簡単です。これには、実際の models での作業が含まれます。その後、永続化のためにRepositories
に渡されます。注:私のリポジトリーは引き続き読み取りメソッドを提供しますが、表示するのではなく、単にオブジェクトを作成するためのものです。それについては後で詳しく説明します。
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データベースを使用する予定でした。 data mapper を使用して、繰り返し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(読み取り)。クエリオブジェクトは、単にある種のデータルックアップロジックをカプセル化したものです。それらはnotクエリビルダーです。リポジトリのように抽象化することで、実装を変更し、テストを簡単に行うことができます。クエリオブジェクトの例としては、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つのクラスに保持され、テストが簡単になります。私の心のコンテンツにそれをcontent笑するか、完全に別の実装を使用することもできます。
ここからが楽しい部分です。すべてのピースをまとめます。依存性注入を使用していることに注意してください。通常、依存関係はコンストラクターに注入されますが、実際にはコントローラーメソッド(ルート)に直接注入することを好みます。これにより、コントローラーのオブジェクトグラフが最小化され、実際に見やすくなりました。この方法が気に入らない場合は、従来のコンストラクターメソッドを使用してください。
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を使用しているため、ほとんどの場合、アプリケーション全体でこれらのモデルを直接参照します。ただし、より複雑なクエリがある状況では、クエリオブジェクトを使用してこれらをより再利用可能にします。また、モデルをメソッドにインジェクトし、テストでモックを作成しやすくすることにも注意してください。
私の経験に基づいて、あなたの質問に対するいくつかの答えがあります:
Q:不要なフィールドを戻すにはどうすればよいですか?
A:私の経験から言えば、これは完全なエンティティとアドホッククエリの処理になります。
完全なエンティティは、User
オブジェクトのようなものです。プロパティやメソッドなどがあります。コードベースの第一級市民です。
アドホッククエリはいくつかのデータを返しますが、それ以上のことはわかりません。データがアプリケーションに渡されると、コンテキストなしで渡されます。 User
ですか? User
情報が添付されたOrder
?私たちは本当に知りません。
完全なエンティティを使用することを好みます。
使用しないデータを頻繁に持ち帰ることは正しいのですが、これにはさまざまな方法で対処できます。
User
を使用でき、AJAX呼び出しにはUserSmall
を使用できます。 1つには10個のプロパティがあり、もう1つには3つのプロパティがあります。アドホッククエリを使用する場合の欠点:
User
を使用すると、多くの呼び出しに対して本質的に同じselect *
を書くことになります。 1つの呼び出しは10個のフィールドのうち8個を取得し、1つは10個のうち5個を取得します。1つは10個のうち7個を取得します。これが悪い理由は、リファクタリング/テスト/モックするのが殺人だからです。User
がなぜ遅いのですか?」のようなステートメントの代わりに一度限りのクエリを追跡することになり、バグ修正は小規模でローカライズされる傾向があります。Q:リポジトリにメソッドが多すぎます。
A:コールを統合する以外、これを回避する方法は実際にはありません。リポジトリ内のメソッド呼び出しは、実際にアプリケーションの機能にマップします。より多くの機能、より多くのデータ固有の呼び出し。機能をプッシュバックして、同様の呼び出しを1つにマージしてみてください。
一日の終わりの複雑さはどこかに存在しなければなりません。リポジトリパターンを使用すると、大量のストアドプロシージャを作成する代わりに、リポジトリインターフェイスにプッシュします。
時には、「どこかに与えなければならなかった!銀の弾丸はない」と自分自身に言わなければなりません。
次のインターフェイスを使用します。
Repository
-エンティティのロード、挿入、更新、削除Selector
-リポジトリ内のフィルターに基づいてエンティティを検索しますFilter
-フィルタリングロジックをカプセル化します私のRepository
はデータベースに依存しません。実際、永続性は指定されていません。 SQLデータベース、xmlファイル、リモートサービス、宇宙からのエイリアンなど。検索機能の場合、Repository
は、フィルタリング可能なSelector
を構築し、LIMIT
- ed 、ソートおよびカウントされます。最後に、セレクターは永続性から1つ以上のEntities
をフェッチします。
以下にサンプルコードを示します。
<?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
などの他のセレクター実装は、特定のFilter
を使用して、InMemoryFilter
からInMemorySelectorFilterAdapter
に変換します。そのため、すべてのセレクター実装には独自のフィルターアダプターが付属しています。
この戦略を使用すると、クライアントコード(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();
追伸これは私の実際のコードの簡略化です
私は現在、これらすべてを自分で把握しようとしているので、これに少し追加します。
これは、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です。リポジトリをベースにすることはできません。これには、User extend eloquentが必要ですが、個人的には問題とは思いません。
ただし、ORMを避けたい場合は、探しているものを取得するために「独自にロール」する必要があります。
インターフェースは、ハードで高速な要件ではないはずです。インターフェイスを実装して追加できるものがあります。それができないのは、そのインターフェースの必要な機能を実装できないことです。クラスのようなインターフェイスを拡張して、物事をドライに保つこともできます。
そうは言っても、私は把握し始めたばかりですが、これらの認識は私を助けてくれました。
私は(私の会社で)これに対処する方法についてのみコメントできます。まず第一に、パフォーマンスは私たちにとってそれほど大きな問題ではありませんが、クリーンで適切なコードを持っていることは重要です。
まず、ORMを使用してUserModel
オブジェクトを作成する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のみがロードされます。この場合、OrderModel
は、参照された注文のIDのみでOrderEntity
sを作成します。 PersistentEntity::getField
がOrderEntity
によって呼び出されると、エンティティはすべてのフィールドをOrderEntity
sに遅延ロードするようモデルに指示します。 1つのUserEntityに関連付けられているすべてのOrderEntity
sは、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ではそれほど大きな問題ではありませんが、他のシステムにはお勧めしません。
これらは私が見たいくつかの異なるソリューションです。それぞれに長所と短所がありますが、決定するのはあなたです。
これは、アカウントに取り込む際に特に重要な側面です インデックスのみのスキャン 。この問題に対処するには、2つの解決策があります。関数を更新して、返す列のリストを含むオプションの配列パラメーターを受け取ることができます。このパラメーターが空の場合、クエリのすべての列を返します。これは少し奇妙なことがあります。パラメータに基づいて、オブジェクトまたは配列を取得できます。すべての関数を複製して、同じクエリを実行する2つの異なる関数を作成することもできますが、一方は列の配列を返し、もう一方はオブジェクトを返します。
public function findColumnsById($id, array $columns = array()){
if (empty($columns)) {
// use *
}
}
public function findById($id) {
$data = $this->findColumnsById($id);
}
私は Propel ORM で1年前に簡単に作業しましたが、これはその経験から私が思い出すことができるものに基づいています。 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]
}
}
これが少なくとも何らかの助けになることを願っています。
https://packagist.org/packages/prettus/l5-repository Laravel5でリポジトリ/基準などを実装するベンダーとして... D
@ ryan1234には、コード内で完全なオブジェクトを渡す必要があり、一般的なクエリメソッドを使用してそれらのオブジェクトを取得する必要があることに同意します。
Model::where(['attr1' => 'val1'])->get();
外部/エンドポイントの使用については、GraphQLメソッドが本当に好きです。
POST /api/graphql
{
query: {
Model(attr1: 'val1') {
attr2
attr3
}
}
}