在PHP中正确的存储库模式devise?

前言:我试图在MVC体系结构中使用关系数据库中的存储库模式。

我最近开始在PHP中学习TDD,我意识到我的数据库与我的应用程序的其他部分密切相关。 我已经阅读了关于存储库,并使用IoC容器 “注入”到我的控制器。 非常酷的东西。 但是现在有关于存储库devise的一些实际问题。 考虑下面的例子。

<?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 all字段( SELECT * )的方法。 然而,在我的应用程序中,我总是试图限制我得到的字段数量,因为这往往会增加开销并降低速度。 对于那些使用这种模式,你如何处理这个?

问题#2:太多的方法

虽然这个类看起来不错,但我知道在真实世界的应用程序中,我需要更多的方法。 例如:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • 等等。

正如你所看到的,可能有非常非常长的可能的方法列表。 然后,如果你在上面的select问题中join,问题就会恶化。 在过去,我通常只是将所有这些逻辑放在我的控制器中:

 <?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:不可能匹配一个接口

我看到使用存储库接口的好处,所以我可以换出我的实现(用于testing目的或其他)。 我对接口的理解是他们定义了一个实现必须遵循的合约。 直到您开始向findAllInCountry()等存储库添加其他方法为止,这非常棒。 现在我需要更新我的接口也有这个方法,否则其他实现可能没有它,这可能会打破我的应用程序。 由此感到疯狂…尾巴摇摆的情况下的狗。

规格模式?

这使我相信,存储库应该只有固定数量的方法(如save()remove()find()findAll()等)。 但是,如何运行特定的查找? 我听说过规范模式 ,但在我看来,这只会减less整个logging集合(通过IsSatisfiedBy() ),如果您从数据库中提取数据,这显然会有明显的性能问题。

帮帮我?

显然,在处理存储库时,我需要重新思考一些问题。 任何人都可以启发这如何处理最好?

我想我会回答我自己的问题。 接下来的只是解决我原来问题中的问题1-3的一种方法。

免责声明:在描述模式或技巧时,我可能并不总是使用正确的术语。 对不起。

目标:

  • 创build一个用于查看和编辑Users的基本控制器的完整示例。
  • 所有的代码必须是完全可testing和可嘲弄的。
  • 控制器不应该知道数据的存储位置(意思是可以改变)。
  • 显示SQL实现的示例(最常见)。
  • 为获得最佳性能,控制器应只接收他们所需的数据 – 不需要额外的字段。
  • 实现应该利用某种types的数据映射器来简化开发。
  • 实现应该有能力执行复杂的数据查找。

解决scheme

我将持久性存储(数据库)交互分为两类: R (读)和CUD (创build,更新,删除)。 我的经验是,读取真的是导致应用程序减速的原因。 虽然数据处理(CUD)实际上比较慢,但是发生的频率要低得多,因此更不用担心。

CUD (创build,更新,删除)很容易。 这将涉及与实际模型 ,然后传递给我的Repositories持久性。 请注意,我的存储库仍然会提供一个Read方法,但只是为了创build对象,而不是显示。 稍后更多。

R (阅读)并不那么容易。 这里没有模型,只是值对象 。 如果你愿意,可以使用数组 这些对象可以代表一个模型,也可以代表许多模型的混合,任何事情都可以。 这些不是很有意思,但它们是如何产生的。 我正在使用我所谓的Query Objects

代码:

用户模型

让我们从我们的基本用户模型开始。 请注意,根本没有ORM扩展或数据库的东西。 只是纯粹的模特荣耀。 添加你的getter,setter,validation,不pipe。

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

存储库接口

在创build我的用户存储库之前,我想创build我的存储库接口。 这将定义存储库为了供控制器使用而必须遵循的“合同”。 请记住,我的控制器不会知道数据的实际存储位置。

请注意,我的存储库只会包含这三种方法。 save()方法负责创build和更新用户,这取决于用户对象是否设置了id。

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

SQL存储库实现

现在创build我的界面的实现。 如前所述,我的例子是与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 (创build,更新,删除)由我们的仓库照顾,我们可以专注于R (读)。 查询对象只是某种types的数据查找逻辑的封装。 他们不是查询build设者。 通过像我们的存储库一样抽象它,我们可以改变它的实现并且更容易地testing它。 查询对象的一个​​例子可能是AllUsersQueryAllActiveUsersQuery ,甚至是MostCommonUserFirstNames

你可能会想“我不能在这些查询的仓库中创build方法吗?” 是的,但这就是为什么我不这样做:

  • 我的存储库是用于处理模型对象的。 在一个真实世界的应用程序,为什么我需要得到password字段,如果我想列出所有的用户?
  • 存储库通常是特定于模型的,但查询通常涉及多个模型。 那么你把你的方法放在什么库?
  • 这使我的存储库非常简单 – 不是一个膨胀的方法类。
  • 所有查询现在都组织到他们自己的类中。
  • 真的,在这一点上,存储库仅仅是为了抽象我的数据库层而存在。

对于我的例子,我将创build一个查询对象来查找“AllUsers”。 这里是界面:

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

查询对象实现

这是我们可以再次使用数据映射器来帮助加速开发的地方。 请注意,我允许对返回的数据集进行一些调整 – 即字段。 这是关于我想要去操纵执行的查询。 请记住,我的查询对象不是查询构build器。 他们只是执行一个特定的查询。 然而,因为我知道我可能会在很多不同的情况下使用这个,我给自己指定字段的能力。 我永远不想回到我不需要的领域!

 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创build一个报告。 这可能是棘手的,我的数据映射器,我可能要编写一些实际的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..."; } } 

这很好地保留了我在这个报告中的所有逻辑,而且很容易testing。 我可以嘲笑我的心中的内容,甚至完全使用不同的实现。

控制器

现在有趣的部分 – 把所有的东西放在一起。 请注意,我正在使用dependency injection。 通常,依赖关系被注入到构造函数中,但我更喜欢将它们注入到我的控制器方法(路由)中。 这最大限度地减less了控制器的对象图,实际上我发现它更清晰。 请注意,如果您不喜欢这种方法,只需使用传统的构造函数方法即可。

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

最后的想法:

这里要注意的重要的事情是,当我修改(创build,更新或删除)实体时,我正在使用真实模型对象,并通过我的存储库执行持久性。

但是,当我显示(select数据并将其发送到视图)我不使用模型对象,而是旧的价值对象。 我只select我需要的字段,它的devise可以最大限度地提高我的数据查询性能。

我的存储库保持非常干净,而这个“混乱”被组织到我的模型查询中。

我使用数据映射器来帮助开发,因为为常见任务编写重复的SQL只是荒谬的。 但是,您绝对可以在需要的地方编写SQL(复杂的查询,报告等)。 而当你这样做的时候,它很好地被卷入一个正确命名的类。

我很乐意听取你的意见。


2015年7月更新:

我在评论中被问到了所有这些。 那么,实际上并不遥远。 说实话,我还是不太喜欢仓库。 我发现它们对于基本的查找过度(尤其是如果你已经使用了ORM),而且在处理更复杂的查询时也很麻烦。

我通常使用ActiveRecord风格的ORM,所以大多数时候我只是直接在我的应用程序中引用这些模型。 但是,在我有更复杂的查询的情况下,我将使用查询对象,使这些更可重用。 我还应该注意到,我总是将我的模型注入到我的方法中,使得它们更容易在我的testing中模拟。

根据我的经验,这里有一些你的问题的答案:

问:我们如何处理我们不需要的领域?

答:从我的经验来看,这实际上归结为处理完整的实体与专门的查询。

一个完整的实体就像一个User对象。 它具有属性和方法等。它是您的代码库中的一等公民。

即席查询返回一些数据,但除此之外我们什么都不知道。 随着数据被传递到应用程序,这是没有上下文的。 是User吗? 附带一些Order信息的User ? 我们真的不知道。

我更喜欢与完整的实体合作。

你是对的,你会经常带回你不会使用的数据,但是你可以用不同的方式来解决这个问题:

  1. 积极caching实体,所以你只能从数据库中支付一次读取价格。
  2. 花更多的时间来模拟你的实体,使它们之间有很好的区别。 (考虑将一个大的实体分成两个较小的实体,等等)
  3. 考虑有多个版本的实体。 你可以有一个User的后端,也许一个UserSmall的AJAX调用。 一个可能有10个属性,一个有3个属性。

使用即席查询的缺点:

  1. 你在许多查询中获得基本相同的数据。 例如,对于一个User ,您最终会为许多调用写入基本相同的select * 。 一个电话会得到10个字段中的8个,一个会得到10个中的5个,一个会得到10个中的7个。为什么不用10个电话中的10个电话来replace全部电话? 这是不好的原因是重新考虑/testing/模拟是谋杀。
  2. 随着时间的推移,你的代码在很高的层次上变得非常困难。 而不是像“为什么是User这么慢?”的陈述。 您最终会追查一次性查询,因此错误修复往往很小且本地化。
  3. 取代底层技术真的很难。 如果你现在把所有东西都存储在MySQL中,并且想要转移到MongoDB,那么replace100个临时调用要比less数几个实体更难。

问:我的仓库中有太多的方法。

答:除巩固通话外,我还没有看到任何其他办法。 该方法在您的存储库中调用真正映射到您的应用程序中的function。 更多的function,更具体的数据调用。 您可以推回function,并尝试将类似的调用合并为一个。

在一天结束的复杂性必须存在的地方。 使用存储库模式,我们已经将它推入了存储库接口,而不是制作一堆存储过程。

有时候我必须告诉自己:“它必须放弃,没有银弹。”

我使用以下接口:

  • Repository – 加载,插入,更新和删除实体
  • Selector – 在存储库中查找基于filter的实体
  • Filter – 封装过滤逻辑

我的Repository是数据库不可知的; 实际上它没有指定任何持久性; 它可以是任何东西:SQL数据库,XML文件,远程服务,来自外太空的外星人等。为了search能力, Repository构build可以被过滤, LIMIT ,sorting和计数的Selector 。 最后,select器从持久性中提取一个或多个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(); } 

那么,一个实现:

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

ideea是通用Selector使用Filter但实现SqlSelector使用SqlFilter ; SqlSelectorFilterAdapter将一个通用的Filter适配到一个具体的SqlFilter

客户端代码创buildFilter对象(即通用filter),但在select器的具体实现中,这些filter在SQLfilter中转换。

其他select器实现,如InMemorySelector ,使用其特定的InMemorySelectorFilterAdapterFilter转换为InMemorySelectorFilterAdapter ; 所以,每个select器实现都有自己的filter适配器。

使用这种策略,我的客户端代码(在bussines层)不关心特定的存储库或select器的实现。

 /** @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这是我的真实代码的简化

我只能评论我们(在我的公司)处理这个问题的方式。 首先,性能对我们来说不是一个太大的问题,但是具有干净/正确的代码是。

首先我们定义一些模型,例如使用ORM创buildUserEntity对象的UserModel 。 从模型加载UserEntity所有字段都将加载。 对于引用外部实体的字段,我们使用适当的外部模型来创build相应的实体。 对于这些实体,数据将被按需加载。 现在你最初的反应可能是… ??? !!! 让我举一个例子:

 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加载一组特定types的实体。 ORM包含一个映射,并使用它来将该实体的所有字段注入到实体中。 而对于外地领域,只有这些对象的身份证被加载。 在这种情况下, OrderModel只用参考订单的I​​D创buildOrderEntity 。 当由OrderEntity调用PersistentEntity::getField时,实体指示它的模型将所有字段延迟加载到OrderEntity 。 与一个用户实体关联的所有OrderEntity都被视为一个结果集,并且会一次加载。

这里的神奇之处在于,我们的模型和ORM将所有数据注入到实体中,并且实体仅为PersistentEntity提供的genericsgetField方法提供包装函数。 总而言之,我们总是加载所有的字段,但是必要时会加载引用外部实体的字段。 只是加载一堆字段不是一个真正的性能问题。 加载所有可能的外国实体,但是会有巨大的性能下降。

现在基于where子句加载一组特定的用户。 我们提供了一个面向对象的类包,允许您指定可以粘在一起的简单expression式。 在示例代码中,我将它命名为GetOptions 。 这是select查询的所有可能选项的包装。 它包含where子句,group by子句和其他所有内容的集合。 我们的where子句是相当复杂的,但你显然可以很容易地做出更简单的版本。

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

这个系统最简单的版本就是将查询的WHERE部分作为string直接传递给模型。

我很抱歉这个相当复杂的回应。 我试图尽可能快速和清楚地总结我们的框架。 如果您有任何其他问题随时问他们,我会更新我的答案。

编辑:另外,如果你真的不想马上加载一些字段,你可以在你的ORM映射中指定一个延迟加载选项。 因为所有的字段最终都是通过getField方法加载的,所以当调用该方法时,可以最后加载一些字段。 这在PHP中不是一个很大的问题,但我不会推荐给其他系统。

这些是我见过的一些不同的解决scheme。 他们每个人都有优点和缺点,但这是由你来决定的。

问题#1:太多领域

这是一个重要的方面,尤其是当您考虑索引唯一扫描 。 我看到了解决这个问题的两个解决scheme。 你可以更新你的函数来获取一个可选的数组参数,该参数将包含一列要返回的列。 如果此参数为空,则返回查询中的所有列。 这可能有点奇怪, 基于参数你可以检索一个对象或数组。 你也可以复制你的所有函数,这样你就有两个不同的函数运行相同的查询,但是一个返回一个列数组,另一个返回一个对象。

 public function findColumnsById($id, array $columns = array()){ if (empty($columns)) { // use * } } public function findById($id) { $data = $this->findColumnsById($id); } 

问题#2:太多的方法

我在一年前曾经和Propel ORM一起工作过,这是基于我能记得的经验。 Propel可以根据现有的数据库模式生成类结构。 它为每个表创build两个对象。 第一个对象是一个类似于你目前列出的访问函数的长列表; findByAttribute($attribute_value) 。 下一个对象从这个第一个对象inheritance。 你可以更新这个子对象来构build更复杂的getter函数。

另一种解决scheme是使用__call()将未定义的函数映射为可操作的。 你的__call方法将能够parsingfindById和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] } } 

我希望这至less有助于一些什么。

我会补充一点,因为我目前正试图自己掌握所有这些。

#1和2

这是您的ORM完成繁重任务的理想场所。 如果您正在使用实现某种ORM的模型,则可以使用它的方法来处理这些事情。 如果需要的话,可以自己订购实现了Eloquent方法的函数。 例如使用雄辩:

 class DbUserRepository implements UserRepositoryInterface { public function findAll() { return User::all(); } public function get(Array $columns) { return User::select($columns); } 

你似乎在寻找的是一个ORM。 没有理由你的仓库不能围绕一个。 这将需要用户扩展雄辩,但我个人并不认为这是一个问题。

如果你想避免一个ORM,那么你必须“自己动手”来获得你想要的东西。

#3

接口不应该是硬性和快速的要求。 东西可以实现一个接口并添加到它。 它不能做的是不能实现该接口所需的function。 你也可以像类一样扩展接口,使事情保持干爽。

也就是说,我刚刚开始掌握,但这些实现帮助了我。