前言:我试图在关系数据库的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所有字段(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:不可能匹配接口

I see the benefit in using interfaces for repositories, so I can swap out my implementation (for testing purposes or other). My understanding of interfaces is that they define a contract that an implementation must follow. This is great until you start adding additional methods to your repositories like findAllInCountry(). Now I need to update my interface to also have this method, otherwise, other implementations may not have it, and that could break my application. By this feels insane...a case of the tail wagging the dog.

规范模式吗?

这让我相信存储库应该只有固定数量的方法(如save()、remove()、find()、findAll()等)。但是如何运行特定的查找呢?我听说过规范模式,但在我看来,这只减少了整个记录集(通过IsSatisfiedBy()),如果从数据库提取,这显然有主要的性能问题。

帮助吗?

显然,在使用存储库时,我需要重新考虑一些事情。有谁能告诉我这个最好怎么处理吗?


当前回答

   class Criteria {}
   class Select {}
   class Count {}
   class Delete {}
   class Update {}
   class FieldFilter {}
   class InArrayFilter {}
   // ...

   $crit = new Criteria();  
   $filter = new FieldFilter();
   $filter->set($criteria, $entity, $property, $value);
   $select = new Select($criteria);
   $count = new Count($criteria);
   $count->getRowCount();
   $select->fetchOne(); // fetchAll();

所以我认为

其他回答

我想我应该尝试回答我自己的问题。以下只是解决我最初问题中的问题1-3的一种方法。

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

目标:

Create a complete example of a basic controller for viewing and editing Users. All code must be fully testable and mockable. The controller should have no idea where the data is stored (meaning it can be changed). Example to show a SQL implementation (most common). For maximum performance, controllers should only receive the data they need—no extra fields. Implementation should leverage some type of data mapper for ease of development. Implementation should have the ability to perform complex data lookups.

解决方案

我将持久存储(数据库)交互分为两类:R(读取)和CUD(创建、更新、删除)。我的经验是,读取是真正导致应用程序变慢的原因。虽然数据操作(CUD)实际上更慢,但它发生的频率要低得多,因此不太值得关注。

CUD(创建,更新,删除)很容易。这将涉及使用实际模型,然后传递给我的存储库进行持久化。注意,我的存储库仍将提供Read方法,但只是用于对象创建,而不是显示。稍后再详细介绍。

R(读)不那么容易。这里没有模型,只有值对象。如果您愿意,可以使用数组。这些对象可以表示单个模型,也可以表示多个模型的混合,实际上什么都可以。它们本身并不是很有趣,但它们是如何产生的却很有趣。我使用的是我所谓的查询对象。

代码:

用户模型

让我们从基本用户模型开始。注意,这里根本没有ORM扩展或数据库之类的东西。纯粹是模特的荣耀。添加getter, setter,验证等等。

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

库接口

在创建用户存储库之前,我想创建存储库接口。这将定义存储库必须遵循的“契约”,以便由我的控制器使用。记住,我的控制器不知道数据实际存储在哪里。

注意,我的存储库将只包含这三个方法。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。

您可能会想“难道我不能在存储库中为这些查询创建方法吗?”是的,但我不这么做的原因是:

My repositories are meant for working with model objects. In a real world app, why would I ever need to get the password field if I'm looking to list all my users? Repositories are often model specific, yet queries often involve more than one model. So what repository do you put your method in? This keeps my repositories very simple—not an bloated class of methods. All queries are now organized into their own classes. Really, at this point, repositories exist simply to abstract my database layer.

对于我的例子,我将创建一个查询对象来查找“AllUsers”。界面如下:

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

查询对象实现

This is where we can use a data mapper again to help speed up development. Notice that I am allowing one tweak to the returned dataset—the fields. This is about as far as I want to go with manipulating the performed query. Remember, my query objects are not query builders. They simply perform a specific query. However, since I know that I'll probably be using this one a lot, in a number of different situations, I'm giving myself the ability to specify the fields. I never want to return fields I don't need!

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

在继续讨论控制器之前,我想展示另一个示例,以说明这是多么强大。也许我有一个报告引擎,需要为allverdueaccounts创建一个报告。对于我的数据映射器,这可能很棘手,在这种情况下,我可能需要编写一些实际的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...";
    }
}

这很好地将该报告的所有逻辑保存在一个类中,并且易于测试。我可以随心所欲地模拟它,甚至完全使用不同的实现。

控制器

现在是有趣的部分——把所有的部分组合在一起。注意,我使用的是依赖注入。通常依赖关系被注入到构造函数中,但实际上我更喜欢将它们直接注入到我的控制器方法(路由)中。这最小化了控制器的对象图,而且我发现它更容易读懂。注意,如果您不喜欢这种方法,可以使用传统的构造函数方法。

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,所以大多数情况下,我将在整个应用程序中直接引用这些模型。但是,在有更复杂查询的情况下,我将使用查询对象来提高这些查询的可重用性。我还应该指出,我总是将我的模型注入到我的方法中,使它们更容易在测试中模拟。

我只能对我们(在我的公司)处理此事的方式发表评论。首先,性能对我们来说不是太大的问题,但拥有干净/适当的代码才是。

首先,我们定义模型,例如使用ORM创建UserEntity对象的UserModel。当一个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)
    {
        //...
    }
}

In our case $db is an ORM that is able to load entities. The model instructs the ORM to load a set of entities of a specific type. The ORM contains a mapping and uses that to inject all the fields for that entity in to the entity. For foreign fields however only the id's of those objects are loaded. In this case the OrderModel creates OrderEntitys with only the id's of the referenced orders. When PersistentEntity::getField gets called by the OrderEntity the entity instructs it's model to lazy load all the fields into the OrderEntitys. All the OrderEntitys associated with one UserEntity are treated as one result-set and will be loaded at once.

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

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

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

该系统最简单的版本是将查询的WHERE部分作为字符串直接传递给模型。

我很抱歉回答这么复杂。我试着尽可能快速和清晰地总结我们的框架。如果你有任何其他问题,请随时问他们,我会更新我的答案。

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

我使用以下接口:

存储库——加载、插入、更新和删除实体 选择器——在存储库中基于过滤器查找实体 过滤器——封装过滤逻辑

我的存储库是数据库不可知的;事实上,它没有指定任何持久性;它可以是任何东西:SQL数据库,xml文件,远程服务,来自外太空的外星人等等。 对于搜索功能,存储库构造了一个选择器,可以过滤、限制、排序和计数。最后,选择器从持久化中获取一个或多个entity。

下面是一些示例代码:

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

这个想法是,通用的选择器使用过滤器,但实现SqlSelector使用SqlFilter;SqlSelectorFilterAdapter将一个通用的Filter适配到一个具体的SqlFilter。

客户端代码创建Filter对象(通用过滤器),但在选择器的具体实现中,这些过滤器在SQL过滤器中转换。

其他选择器实现,如InMemorySelector,使用它们特定的InMemorySelectorFilterAdapter从Filter转换为InMemoryFilter;因此,每个选择器实现都有自己的过滤器适配器。

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

/** @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();

附注:这是对我实际代码的简化

Issue #3: Impossible to match an interface I see the benefit in using interfaces for repositories, so I can swap out my implementation (for testing purposes or other). My understanding of interfaces is that they define a contract that an implementation must follow. This is great until you start adding additional methods to your repositories like findAllInCountry(). Now I need to update my interface to also have this method, otherwise, other implementations may not have it, and that could break my application. By this feels insane...a case of the tail wagging the dog.

我的直觉告诉我,这可能需要一个实现查询优化方法和泛型方法的接口。对性能敏感的查询应该有针对性的方法,而不频繁或轻量级的查询则由通用处理程序处理,这可能会使控制器做更多的杂耍。

泛型方法将允许实现任何查询,因此将防止在过渡期间发生破坏性更改。目标方法允许您在有意义时优化调用,并且它可以应用于多个服务提供者。

这种方法类似于硬件实现执行特定的优化任务,而软件实现只做简单的工作或灵活的实现。

根据我的经验,以下是对你们问题的一些回答:

问:我们如何处理收回我们不需要的土地?

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

一个完整的实体类似于一个User对象。它有属性和方法等。它是代码库中的一等公民。

一个特别查询返回一些数据,但除此之外我们什么都不知道。当数据在应用程序中传递时,是在没有上下文的情况下完成的。它是用户吗?附带一些订单信息的用户?我们真的不知道。

我更喜欢与完整的实体一起工作。

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

积极地缓存实体,这样您只需从数据库中支付一次读取代价。 花更多的时间建模你的实体,这样它们之间就有了很好的区别。(考虑将一个大实体分成两个小实体,等等) 考虑拥有多个版本的实体。你可以有一个User用于后端,也可以有一个UserSmall用于AJAX调用。一个可能有10个属性,一个有3个属性。

使用临时查询的缺点:

You end up with essentially the same data across many queries. For example, with a User, you'll end up writing essentially the same select * for many calls. One call will get 8 of 10 fields, one will get 5 of 10, one will get 7 of 10. Why not replace all with one call that gets 10 out of 10? The reason this is bad is that it is murder to re-factor/test/mock. It becomes very hard to reason at a high level about your code over time. Instead of statements like "Why is the User so slow?" you end up tracking down one-off queries and so bug fixes tend to be small and localized. It's really hard to replace the underlying technology. If you store everything in MySQL now and want to move to MongoDB, it's a lot harder to replace 100 ad-hoc calls than it is a handful of entities.

问:我的存储库中会有太多的方法。

答:除了整合通话之外,我还没有找到其他解决办法。存储库中的方法调用实际映射到应用程序中的功能。特性越多,特定于数据的调用就越多。您可以向后推功能,并尝试将类似的调用合并为一个。

一天结束的时候,复杂性必须存在于某个地方。使用存储库模式,我们将其推入存储库接口,而不是制造一堆存储过程。

有时我不得不告诉自己,“好吧,它必须在某个地方让步!”没有银弹。”