PHP中正确的存储库模式设计?

问题描述:

前言:我正在考虑在具有关系数据库的MVC架构中使用存储库模式。

Preface: I'm attemping to use the repository pattern in a MVC architecture with relational databases.

我最近开始学习TDD在PHP中,我意识到我的数据库与我的应用程序的其余部分太紧密。我已经阅读了关于存储库,并使用 IoC容器将它注入到我的控制器。非常酷的东西。但现在有一些关于存储库设计的实用问题。考虑以下示例。

I've recently started learning TDD in PHP, and I'm realizing that my database is coupled much too closely with the rest of my application. I've read about repositories, and using an IoC container to "inject" it into my controllers. Very cool stuff. But now have some practical questions about repository design. Consider the follow example.

<?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:太多字段



所有这些find方法都使用select所有字段( SELECT * )方法。但是,在我的应用程序中,我总是试图限制我获得的字段数,因为这通常会增加开销并减慢事情。

Issue #1: Too many fields

All of these find methods use a select all fields (SELECT *) approach. However, in my apps I'm always trying to limit the number of fields I get, as this often adds overhead and slows things down. For those using this pattern, how do you deal with this?

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

While this class looks nice right now, I know that in a real world app I need a lot more methods. For example:


  • findAllByNameAndStatus

  • findAllInCountry

  • findAllWithEmailAddressSet

  • findAllByAgeAndGender

  • findAllByAgeAndGenderOrderByAge

  • 等。

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Etc.

正如你所看到的,可能有非常长的可能的方法列表。然后如果你在上面的字段选择问题中添加,问题恶化。在过去,我通常只是把所有这些逻辑在我的控制器:

As you can see, there could be very, very long list of possible methods. And then if you add in the field selection issue above, the problem worsens. In the past I'd normally just put all this logic right in my controller:

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



有利于使用接口的存储库,所以我可以换出我的实现(为测试目的或其他)。我对接口的理解是他们定义了一个实现必须遵循的契约。这是伟大的,直到你开始添加额外的方法到您的存储库,如 findAllInCountry()。现在我需要更新我的界面也有这个方法,否则其他实现可能没有它,这可能会打破我的应用程序。

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.

这种情况下,导致我相信存储库应该只有固定数量的方法(如 save() remove() find() findAll()等)。但是如何运行特定的查找?我听说过规范模式,但在我看来,这只会减少一整套记录(通过 IsSatisfiedBy()),如果你从数据库中删除,它显然有很大的性能问题。

This leads me to believe that repository should only have a fixed number of methods (like save(), remove(), find(), findAll(), etc). But then how do I run specific lookups? I've heard of the Specification Pattern, but it seems to me that this only reduces an entire set of records (via IsSatisfiedBy()), which clearly has major performance issues if you're pulling from a database.

显然,我需要在处理存储库时重新考虑一些事情。

Clearly I need to rethink things a little when working with repositories. Can anyone enlighten on how this is best handled?

我想我会在回答自己的问题时采取行动。下面是解决我原来问题1-3中的问题的一种方法。

I thought I'd take a crack at answering my own question. What follows is just one way of solving the issues 1-3 in my original question.

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


  • 用于查看和编辑用户的基本控制器的完整示例。

  • 所有代码必须完全可测试和可模拟。

  • 控制器应该不知道数据存储在哪里(这意味着可以更改)。

  • 显示SQL实现的示例(最常见)。

  • 为了获得最佳性能,控制器应该只接收所需的数据,而不需要额外的字段。

  • 实施应利用某种类型的数据映射器, / li>
  • 实施应该能够执行复杂的数据查找。

  • 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 ,Delete)。我的经验是,读取真的是什么导致应用程序减速。

I'm splitting my persistent storage (database) interaction into two categories: R (Read) and CUD (Create, Update, Delete). My experience has been that reads are really what causes an application to slow down. And while data manipulation (CUD) is actually slower, it happens much less frequently, and is therefore much less of a concern.

CUD (CUD)创建,更新,删除)很容易。这将涉及使用实际的模型,然后传递到我的存储库用于持久性。注意,我的存储库仍然会提供一个Read方法,但只是对象创建,不显示。

CUD (Create, Update, Delete) is easy. This will involve working with actual models, which are then passed to my Repositories for persistence. Note, my repositories will still provide a Read method, but simply for object creation, not display. More on that later.

R (读取)并不容易。此处没有模型,只需值对象。如果您喜欢,请使用阵列。这些对象可以表示单个模型或许多模型的混合,任何真正的。这些不是很有趣的自己,但它们是如何生成的。我正在使用我调用查询对象

R (Read) is not so easy. No models here, just value objects. Use arrays if you prefer. These objects may represent a single model or a blend of many models, anything really. These are not very interesting on their own, but how they are generated is. I'm using what I'm calling Query Objects.

让我们从我们的基本用户模型开始。注意,根本没有ORM扩展或数据库东西。只是纯粹的模式荣耀。添加您的getter,setter,验证,任何。

Let's start simple with our basic user model. Note that there is no ORM extending or database stuff at all. Just pure model glory. Add your getters, setters, validation, whatever.

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



存储库接口



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

Repository Interface

Before I create my user repository, I want to create my repository interface. This will define the "contract" that repositories must follow in order to be used by my controller. Remember, my controller will not know where the data is actually stored.

请注意,我的存储库将只包含这三个方法。 save()方法负责创建和更新用户,只需根据用户对象是否设置了id。

Note that my repositories will only every contain these three methods. The save() method is responsible for both creating and updating users, simply depending on whether or not the user object has an id set.

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



SQL Repository Implementation



现在创建我的接口的实现。如上所述,我的例子将是一个SQL数据库。请注意,使用数据映射器可防止写入重复的SQL查询。

SQL Repository Implementation

Now to create my implementation of the interface. As mentioned, my example was going to be with an SQL database. Note the use of a data mapper to prevent having to write repetitive SQL queries.

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

Query Object Interface

Now with CUD (Create, Update, Delete) taken care of by our repository, we can focus on the R (Read). Query objects are simply an encapsulation of some type of data lookup logic. They are not query builders. By abstracting it like our repository we can change it's implementation and test it easier. An example of a Query Object might be an AllUsersQuery or AllActiveUsersQuery, or even MostCommonUserFirstNames.

您可能在想我不能在我的存储库中为这些查询创建方法?是的,但这里是为什么我不这样做:

You may be thinking "can't I just create methods in my repositories for those queries?" Yes, but here is why I'm not doing this:


  • 我的存储库用于处理模型对象。在现实世界的应用程序中,如果我要列出所有用户,为什么我需要获取密码字段?

  • 存储库通常是模型特定的,但查询通常涉及多个模型。

  • 这使我的存储库非常简单 - 不是一个笨重的方法类。


  • 真的,在这一点上,存储库的存在只是为了抽象我的数据库层。

  • 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。这里是界面:

For my example I'll create a query object to lookup "AllUsers". Here is the interface:

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



查询对象实现



这是我们可以再次使用数据映射器来帮助加快开发。请注意,我允许对返回的数据集(字段)进行一次调整。这是关于我想要与操纵执行的查询。记住,我的查询对象不是查询构建器。他们只是执行一个特定的查询。但是,由于我知道我可能会使用这个很多,在许多不同的情况下,我给自己的能力指定字段。我不想返回字段,我不需要!

Query Object Implementation

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



在进入控制器之前,我想显示另一个例子来说明这是多么强大。也许我有一个报告引擎,需要创建 AllOverdueAccounts 的报告。这可能是棘手的我的数据映射器,我可能想在这种情况下写一些实际 SQL 。没有问题,这里是这个查询对象可能是什么样子:

Before moving on to the controller, I want to show another example to illustrate how powerful this is. Maybe I have a reporting engine and need to create a report for AllOverdueAccounts. This could be tricky with my data mapper, and I may want to write some actual SQL in this situation. No problem, here is what this query object could look like:

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

这很好地将我的所有逻辑保存在一个类,它很容易测试。

This nicely keeps all my logic for this report in one class, and it's easy to test. I can mock it to my hearts content, or even use a different implementation entirely.

现在,您可以使用不同的实现方式有趣的部分 - 把所有的片段在一起。注意,我使用依赖注入。通常依赖注入到构造函数中,但我实际上更喜欢将它们注入我的控制器方法(路由)。这最小化了控制器的对象图,我实际上发现它更清晰。注意,如果你不喜欢这种方法,只需使用传统的构造方法。

Now the fun part—bringing all the pieces together. Note that I am using dependency injection. Typically dependencies are injected into the constructor, but I actually prefer to inject them right into my controller methods (routes). This minimizes the controller's object graph, and I actually find it more legible. Note, if you don't like this approach, just use the traditional constructor method.

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



最后想法:



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

Final Thoughts:

The important things to note here are that when I'm modifying (creating, updating or deleting) entities, I'm working with real model objects, and performing the persistance through my repositories.

但是,当我显示(选择数据并发送到视图)我不使用模型对象,而是纯旧的值对象。我只选择我需要的字段,它的设计使我可以最大限度地获得我的数据查找性能。

However, when I'm displaying (selecting data and sending it to the views) I'm not working with model objects, but rather plain old value objects. I only select the fields I need, and it's designed so I can maximum my data lookup performance.

我的存储库保持非常干净,而是我的模型查询。

My repositories stay very clean, and instead this "mess" is organized into my model queries.

我使用数据映射器来帮助开发,因为为常见任务编写重复的SQL是可笑的。但是,你绝对可以在需要的地方写SQL(复杂的查询,报告等)。

I use a data mapper to help with development, as it's just ridiculous to write repetitive SQL for common tasks. However, you absolutely can write SQL where needed (complicated queries, reporting, etc.). And when you do, it's nicely tucked away into a properly named class.

我很想听听你对我的做法!

I'd love to hear your take on my approach!

2015年7月更新:

July 2015 Update:

我在评论中被问到我结束了所有这一切。嗯,不是那么遥远的实际上。说实话,我还是不喜欢仓库。我发现他们基本的查找(特别是如果你已经使用一个ORM),和使用更复杂的查询时,凌乱凌辱。

I've been asked in the comments where I ended up with all this. Well, not that far off actually. Truthfully, I still don't really like repositories. I find them overkill for basic lookups (especially if you're already using an ORM), and messy when working with more complicated queries.

我通常使用ActiveRecord风格ORM,所以我经常在我的应用程序中直接引用这些模型。但是,在我有更复杂的查询的情况下,我将使用查询对象使这些更可重用。我还应该注意,我总是把我的模型注入我的方法,使他们更容易模拟我的测试。

I generally work with an ActiveRecord style ORM, so most often I'll just reference those models directly throughout my application. However, in situations where I have more complex queries, I'll use query objects to make these more reusable. I should also note that I always inject my models into my methods, making them easier to mock in my tests.