先週、私はEloquentエンティティーのリポジトリー・テンプレートの役に立たないことに関する記事を書きましたが、それを部分的に使用して利益を得る方法について話すことを約束しました。 これを行うには、このテンプレートがプロジェクトで通常どのように使用されるかを分析します。 リポジトリに最低限必要なメソッドのセット:
<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); }
ただし、実際のプロジェクトでは、リポジトリを使用することが決定された場合、多くの場合、レコードを選択するためのメソッドが追加されます。
<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
これらのメソッドはEloquentスコープを介して実装できますが、自身をフェッチする責任を持つエンティティクラスをオーバーロードすることは良い考えではなく、この責任をリポジトリクラスに入れることは論理的に思えます。 そうですか? このインターフェイスを特に視覚的に2つの部分に分割しました。 メソッドの最初の部分は、書き込み操作で使用されます。
標準の書き込み操作は次のとおりです。
- 新しいオブジェクトを作成してPostRepositoryを呼び出す:: save
- PostRepository :: getById 、エンティティの操作およびPostRepositoryの呼び出し::保存
- PostRepositoryを呼び出す::削除
書き込み操作にはサンプリング方法はありません。 読み取り操作では、get *メソッドのみが使用されます。 インターフェース分離の原則 ( SOLIDの文字I )について読むと、インターフェースが大きすぎて、少なくとも2つの異なる責任を果たしていることが明らかになります。 それを2つに分ける時間です。 getByIdメソッドは両方で必要ですが、アプリケーションがより複雑になると 、その実装は異なります。 これについては後ほど説明します。 書き込み部分の無用性については以前の記事で書いたので、これでは単純にそれを忘れます。
Eloquentの場合でもいくつかの実装が存在する可能性があるため、Readの部分はあまり役に立たないようです。 クラスに名前を付けるには? ReadPostRepositoryは使用できますが、 Repositoryテンプレートとはほとんど関係がありません。 単にクエリをポストできます:
<?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
Eloquentを使用した実装は非常に簡単です。
<?php final class EloquentPostQueries implements PostQueries { public function getById($id): Post { return Post::findOrFail($id); } /** * @return Post[] | Collection */ public function getLastPosts() { return Post::orderBy('created_at', 'desc') ->limit(/*some limit*/) ->get(); } /** * @return Post[] | Collection */ public function getTopPosts() { return Post::orderBy('rating', 'desc') ->limit(/*some limit*/) ->get(); } /** * @param int $userId * @return Post[] | Collection */ public function getUserPosts($userId) { return Post::whereUserId($userId) ->orderBy('created_at', 'desc') ->get(); } }
インターフェイスは、たとえばAppServiceProviderで実装に関連付ける必要があります。
<?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, EloquentPostQueries::class); } }
このクラスはすでに便利です。 彼は、コントローラーまたはエンティティークラスのいずれかをアンロードすることにより、自分の責任を認識しています。 コントローラーでは、次のように使用できます。
<?php final class PostsController extends Controller { public function lastPosts(PostQueries $postQueries) { return view('posts.last', [ 'posts' => $postQueries->getLastPosts(), ]); } }
PostsController :: lastPostsメソッドは、単にPostsQueriesの実装を要求し、それを処理します。 プロバイダーでは、 PostQueriesをEloquentPostQueriesクラスに関連付けました。このクラスはコントローラーに置き換えられます。
私たちのアプリケーションが非常にポピュラーになったと想像してみましょう。 毎分数千のユーザーが最新の出版物でページを開きます。 最も人気のある出版物も非常に頻繁に読まれます。 データベースはこのような負荷にうまく対処できないため、標準ソリューションであるキャッシュを使用します。 データベースに加えて、特定のナゲットデータは、特定の操作( memcachedまたはredis)向けに最適化されたリポジトリに保存されます。
通常、キャッシングロジックはそれほど複雑ではありませんが、EloquentPostQueriesでの実装はあまり正確ではありません(少なくとも単一責任原則のため )。 Decoratorテンプレートを使用し、メインアクションを装飾するようにキャッシュを実装する方がはるかに自然です。
<?php use Illuminate\Contracts\Cache\Repository; final class CachedPostQueries implements PostQueries { const LASTS_DURATION = 10; /** @var PostQueries */ private $base; /** @var Repository */ private $cache; public function __construct( PostQueries $base, Repository $cache) { $this->base = $base; $this->cache = $cache; } /** * @return Post[] | Collection */ public function getLastPosts() { return $this->cache->remember('last_posts', self::LASTS_DURATION, function(){ return $this->base->getLastPosts(); }); } // }
コンストラクターでRepositoryインターフェースを無視します 。 何らかの理由で、彼らはLaravelでキャッシュするためのインターフェースを呼び出すことにしました。
CachedPostQueriesクラスはキャッシングのみを実装します。 $ this-> cache-> rememberは、指定されたエントリがキャッシュ内にあるかどうかを確認し、そうでない場合はコールバックを呼び出し、返された値をキャッシュに書き込みます。 このクラスをアプリケーションに実装するだけです。 CachedPostQueriesクラスのインスタンスを受け取るには、アプリケーションでPostQueriesインターフェイスの実装を要求するすべてのクラスが必要です。 ただし、 CachedPostQueries自体は、コンストラクターのパラメーターとしてEloquentPostQueriesクラスを受け取る必要があります。「実際の」実装なしでは機能しないためです。 AppServiceProviderを変更します 。
<?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, CachedPostQueries::class); $this->app->when(CachedPostQueries::class) ->needs(PostQueries::class) ->give(EloquentPostQueries::class); } }
私の願いはすべてプロバイダーに自然に記述されています。 したがって、1つのクラスを記述し、コンテナー構成を変更することによってのみ、要求のキャッシュを実装しました。 残りのアプリケーションのコードは変更されていません。
もちろん、キャッシングを完全に実装するには、削除された記事がしばらくサイトにハングアップしないように無効化を実装する必要がありますが、すぐに残されます(最近キャッシングに関する記事を書いたので、詳細に役立ちます)。
結論:1つではなく、2つのパターン全体を使用しました。 Command Query Responsibility Segregation(CQRS)テンプレートは、インターフェイスレベルで読み取り操作と書き込み操作を完全に分離します。 Interface Segregation Principleから彼に来ました。つまり、テンプレートと原則を巧みに操作し、定理として互いに推測します:)もちろん、すべてのプロジェクトがエンティティサンプルのそのような抽象化を必要とするわけではありませんが、焦点を共有します。アプリケーション開発の初期段階では、Eloquentを使用して通常の実装でPostQueriesクラスを簡単に作成できます。
<?php final class PostQueries { public function getById($id): Post { return Post::findOrFail($id); } // }
キャッシングの必要が生じた場合、このPostQueriesクラスの代わりにインターフェース(または抽象クラス)を簡単に作成し、その実装をEloquentPostQueriesクラスにコピーして、前述のスキームに進むことができます。 残りのアプリケーションコードを変更する必要はありません。
ただし、データを変更できる同じPostエンティティの使用には問題が残っていました。 これは正確にはCQRSではありません。
PostQueriesからPostエンティティを取得し、それを変更し、 simple- > save()で変更を保存する手間はありません。 そして、それは動作します。
しばらくすると、チームはデータベース内のマスタースレーブレプリケーションに切り替わり 、 PostQueriesはリードレプリカで動作します。 通常、リードレプリカの書き込み操作はブロックされます。 エラーが開きますが、すべてのそのような妨害を修正するために一生懸命働く必要があります。
明らかな解決策は、 読み取り部分と書き込み部分を完全に分離することです。 Eloquentは引き続き使用できますが、読み取り専用モデルのクラスを作成します。 例: https : //github.com/adelf/freelance-example/blob/master/app/ReadModels/ReadModel.phpすべてのデータ変更操作がブロックされます。 ReadPostなどの新しいモデルを作成します( Postはそのままにして、別のネームスペースに移動できます)。
<?php final class ReadPost extends ReadModel { protected $table = 'posts'; } interface PostQueries { public function getById($id): ReadPost; }
このようなモデルは読み取り専用で、安全にキャッシュできます。
別のオプション:Eloquentを放棄します。 これにはいくつかの理由があります。
- すべてのテーブルフィールドはほとんど必要ありません。 lastPostsリクエストの場合、 通常はid 、 title 、およびたとえばpublished_atフィールドのみが必要です。 パブリケーションのいくつかの重いテキストをリクエストしても、データベースまたはキャッシュに不必要な負荷がかかるだけです。 Eloquentは必須フィールドのみを選択できますが、これはすべて暗黙的です。 PostQueriesの顧客は 、実装内に入ることなく、どのフィールドが選択されるかを正確に知りません。
- キャッシュはデフォルトでシリアル化を使用します。 雄弁なクラスは、直列化された形式ではスペースを取りすぎます。 これが単純なエンティティで特に顕著でない場合、関係を持つ複雑なエンティティでこれが問題になります(メガバイトサイズのオブジェクトをキャッシュからドラッグする方法もあります)。 あるプロジェクトでは、キャッシュ内にパブリックフィールドを持つ通常のクラスは、Eloquentオプションよりも10倍少ないスペースを使用しました(小さなサブエンティティがたくさんありました)。 キャッシュするときにのみ属性をキャッシュできますが、これによりプロセスが大幅に複雑になります。
これがどのように見えるかの簡単な例:
<?php final class PostHeader { public int $id; public string $title; public DateTime $publishedAt; } final class Post { public int $id; public string $title; public string $text; public DateTime $publishedAt; } interface PostQueries { public function getById($id): Post; /** * @return PostHeader[] */ public function getLastPosts(); /** * @return PostHeader[] */ public function getTopPosts(); /** * @var int $userId * @return PostHeader[] */ public function getUserPosts($userId); }
もちろん、これはすべて、論理の過度の複雑化のように思えます。 「Eloquentスコープを使用すると、すべてがうまくいきます。なぜこれをすべて考え出すのですか?」 より単純なプロジェクトの場合、これは正しいです。 スコープを作り直す必要はまったくありません。 しかし、プロジェクトが大規模で、多くの開発者が開発に関与している場合、それらは頻繁に変更されます(終了し、新しい開発者が来ます)、ゲームのルールはわずかに異なります。 数年後に新しい開発者が何か間違ったことをしないように、コードは保護された状態で記述されなければなりません。 もちろん、そのような確率を完全に除外することは不可能ですが、その確率を減らす必要があります。 さらに、これは一般的なシステムの分解です。 キャッシュを無効にするためのすべてのキャッシングデコレータとクラスを収集して、一種の「キャッシングモジュール」にすることができます。これにより、キャッシングに関する情報の残りのアプリケーションを保存できます。 キャッシュコールに囲まれた大規模なリクエストを処理する必要がありました。 邪魔になります。 特に、キャッシングロジックが上記のように単純でない場合。