上周,我写了一篇关于Eloquent实体的Repository模板的无用性的文章 ,但我答应谈论如何部分地使用它以达到良好的使用效果。 为此,我将尝试分析该模板通常在项目中的使用方式。 存储库所需的最小方法集:
<?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范围实现,但是将实体类重载以负责自己的职责并不是一个好主意,并且将此职责放入存储库类中似乎是合乎逻辑的。 是这样吗 我专门从视觉上将这个界面分为两个部分。 方法的第一部分将在写操作中使用。
标准的写操作是:
- 构造一个新对象并调用PostRepository :: save
- PostRepository :: getById ,实体操作并调用PostRepository :: save
- 调用PostRepository ::删除
写操作中没有采样方法。 在读取操作中,仅使用get *方法。 如果您阅读了接口隔离原则 ( SOLID中的字母I ),将会很清楚我们的接口太大,并且至少承担两个不同的职责。 现在是将其分为两部分的时候了。 两者都需要getById方法,但是如果应用程序变得更加复杂,则其实现将有所不同。 我们稍后会看到。 我在上一篇文章中谈到了写部分的无用性,因此在此我只是忘了它。
在我看来,阅读该部分并不是没有用,因为即使对于Eloquent,也可以有多种实现。 如何命名课程? 您可以ReadPostRepository ,但它已经与Repository模板无关。 您可以简单地PostQueries :
<?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); } public function getLastPosts() { return Post::orderBy('created_at', 'desc') ->limit() ->get(); } public function getTopPosts() { return Post::orderBy('rating', 'desc') ->limit() ->get(); } 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; private $base; private $cache; public function __construct( PostQueries $base, Repository $cache) { $this->base = $base; $this->cache = $cache; } public function getLastPosts() { return $this->cache->remember('last_posts', self::LASTS_DURATION, function(){ return $this->base->getLastPosts(); }); }
忽略构造函数中的Repository接口。 由于某种原因,他们决定在Laravel中调用用于缓存的接口。
CachedPostQueries类仅实现缓存。 $ this-> cache-> remove检查给定的条目是否在缓存中,如果不存在,它将调用回调并将返回的值写入缓存。 它仅保留在应用程序中实现此类。 我们需要应用程序中要求实现PostQueries接口的所有类,以接收CachedPostQueries类的实例。 但是, 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); } }
提供者很自然地描述了我的所有愿望。 因此,我们仅通过编写一个类并更改容器配置来为请求实现缓存。 该应用程序其余部分的代码未更改。
当然,对于缓存的完整实现,仍然有必要实现无效,以使已删除的文章不再在网站上挂起更多时间,而是立即离开(最近写了一篇有关缓存的文章 ,它可以提供详细帮助)。
底线:我们使用的不是一个,而是两个完整的模式。 命令查询职责隔离(CQRS)模板在接口级别提供了读写操作的完全隔离。 我是通过接口隔离原理来找他的,这意味着我熟练地操作了模板和原理并相互推导为一个定理:)当然,并不是每个项目都需要对实体样本进行这样的抽象,但是我将与您共享重点。 在应用程序开发的初始阶段,您可以简单地通过Eloquent创建具有常规实现的PostQueries类:
<?php final class PostQueries { public function getById($id): Post { return Post::findOrFail($id); }
当需要缓存时,可以轻松地创建一个接口(或抽象类)来代替此PostQueries类,将其实现复制到EloquentPostQueries类中,然后转到我之前介绍的方案。 其余应用程序代码无需更改。
但是,使用相同的Post实体可以修改数据仍然存在问题。 这不完全是CQRS。

没有人愿意从PostQueries中获取Post实体, 对其进行修改并使用简单的-> 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; }
这样的模型可以是只读的,可以安全地进行缓存。

另一种选择:放弃雄辩。 可能有几个原因:
- 几乎不需要所有表字段。 对于lastPosts请求,通常仅需要id , title和published_at字段。 请求一些重磅的出版物文本只会给数据库或缓存带来不必要的负担。 雄辩的只能选择必填字段,但是所有这些都是非常隐含的。 如果不进入实现, 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; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); }
当然,所有这些似乎都使逻辑过于复杂。 “采用雄辩的范围,一切都会好起来的。为什么要提出所有这些建议?” 对于较简单的项目,这是正确的。 绝对没有必要重新设计范围。 但是,当项目规模较大且开发中涉及多个开发人员时,这些开发人员经常更改(他们退出并有新的开发人员),游戏规则会略有不同。 必须对代码进行保护,以使新开发人员在几年后不能做错任何事情。 当然,不可能完全排除这种可能性,但是有必要降低其可能性。 另外,这是常见的系统分解。 您可以收集所有缓存装饰器和类,以使缓存无效到某个“缓存模块”中,从而节省了其余有关缓存信息的应用程序。 我不得不遍历被缓存调用包围的大型请求。 它妨碍了。 特别是如果缓存逻辑不是如上所述的那么简单。