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

我最近开始学习PHP中的TDD,我意识到我的数据库与应用程序的其他部分耦合得太紧密了.我读过关于存储库和使用IoC container将其"注入"到控制器的文章.很酷的东西.但是现在我们有一些关于存储库设计的实际问题.考虑下面的例子.

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

问题2:方法太多

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

  • findAllByNameAndStatus
  • 芬达林乡村wine 店
  • findAllWithEmailAddressSet
  • FindallBageAndender
  • FindallBageAndenderOrderByAge

正如你所看到的,可能有一个非常非常长的方法列表.如果你加上上面的字段 Select 问题,问题就会恶化.在过go ,我通常只是将所有这些逻辑正确地放在我的控制器中:

<?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:无法匹配接口

我看到了为存储库使用接口的好处,因此我可以交换我的实现(用于测试或其他目的).我对接口的理解是,它们定义了实现必须遵循的契约.这很好,直到你开始向你的存储库添加额外的方法,比如芬达林乡村wine 店().现在我需要更新我的接口,使其也具有此方法,否则,其他实现可能没有此方法,这可能会 destruct 我的应用程序.这让我觉得很疯狂... tail 摇狗的例子.

规格式样?

这让我相信存储库应该只有固定数量的方法(如save()remove()find()findAll()等).但是,我如何运行特定的查找呢?我听说过Specification Pattern,但在我看来,这只会减少整个记录集(通过IsSatisfiedBy()),如果您从数据库中提取数据,这显然存在严重的性能问题.

帮助

显然,在使用存储库时,我需要重新思考一下.有人能告诉我们如何最好地处理这件事吗?

推荐答案

我想我应该试着回答我自己的问题.以下只是解决我原问题中1-3个问题的一种方法.

Disclaimer: I may not always use the right terms when describing patterns or techniques. Sorry for that.

目标是:

  • 创建一个用于查看和编辑Users的基本控制器的完整示例.
  • 所有代码必须是完全可测试和可模拟的.
  • 控制器应该不知道数据存储在哪里(意味着数据可以更改).
  • 显示SQL实现的示例(最常见).
  • 为了获得最高性能,控制器应该只接收它们需要的数据,而不是额外的字段.
  • 实现应该利用某种类型的数据映射器以便于开发.
  • 实现应该能够执行复杂的数据查找.

解决方案

我将持久性存储(数据库)交互分为两类:R(读取)和CUD(创建、更新、删除).我的经验是,阅读确实是导致应用程序速度变慢的原因.虽然数据操作(CUD)实际上较慢,但它发生的频率要低得多,因此也就不是什么大问题了.

CUD(创建、更新、删除)很简单.这将涉及使用实际的models,然后将其传递给我的Repositories进行持久性.注意,我的存储库仍将提供读取方法,但只是用于对象创建,而不是显示.稍后再谈.

R(读)不是那么容易的.这里没有模特,只有value objects个.使用数组if you prefer.这些对象可以表示单个模型,也可以表示多个模型的混合,实际上是任何东西.这些本身并不是很有趣,但是它们是如何生成的.我用的是我所谓的Query Objects.

代码:

用户模型

让我们从简单的基本用户模型开始.请注意,根本没有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数据库.请注意,使用data mapper可以避免编写重复的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(读取).查询对象只是某种类型的数据查找逻辑的封装.他们是not个查询生成器.通过将其抽象为我们的存储库,我们可以改变它的实现并更容易地进行测试.查询对象的示例可能是AllUsersQueryAllActiveUsersQuery,甚至MostCommonUserFirstNames.

你可能会想,"我不能在我的存储库中为这些查询创建方法吗?"是的,但我不这么做的原因如下:

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

对于我的示例,我将创建一个Query对象来查找"AllUsers".界面如下:

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

查询对象实现

这就是我们可以再次使用数据映射器来帮助加快开发速度的地方.请注意,我允许对返回的数据集(字段)进行一次调整.这就是我想要操作执行的查询的最大限度.请记住,我的查询对象不是查询构建器.它们只是执行特定的查询.但是,由于我知道在许多不同的情况下,我可能会经常使用这个字段,所以我给自己指定字段的能力.我永远不想返回我不需要的字段!

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.没问题,这个查询对象可能是这样的:

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

最后的 idea :

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

但是,当我显示( Select 数据并将其发送到视图)时,我使用的不是模型对象,而是普通的旧值对象.我只 Select 我需要的字段,它的设计使我可以最大限度地提高数据查找性能.

我的存储库保持非常干净,相反,这种"混乱"被组织到我的模型查询中.

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

我很想听听你对我的方法的看法!


July 2015 Update:

我在 comments 中被问到,我在哪里结束了这一切.其实没那么远.说实话,我还是不太喜欢仓库.我发现它们对于基本的查找(尤其是如果你已经在使用ORM的话)来说过于繁琐,而在处理更复杂的查询时,它们又很混乱.

我通常使用ActiveRecord样式的ORM,所以大多数情况下,我只会在整个应用程序中直接引用这些模型.但是,在有更复杂查询的情况下,我将使用查询对象使这些查询更具可重用性.我还应该注意到,我总是将模型注入到方法中,使它们更容易在测试中模拟.

Php相关问答推荐

在php中有没有办法比较两个包含相同枚举类型的枚举数组

有什么方法可以阻止WordPress主题升级通知吗?

无法在Laravel/Vue停靠容器中启动MySQL

如何将WooCommerce产品属性术语名称显示为链接术语?

将购买限制在WooCommerce中具有相同产品属性的项目

在Laravel中为表添加前缀,以将它们逻辑地分组

PHP空数组定义与开始或结束逗号

筛选器具有多个查询之一

返回WooCommerce中的所有已启用变体

PHP-准备用于TCP套接字的数据包

允许在WooCommerce管理订单列表中使用计费邮箱进行搜索

Regex|PHP捕获json字符串中的每个非法双引号

Woocommerce API - 图像问题

使用 wp_footer 挂钩添加一些 CSS 不起作用

通过ajax发送数据到下拉列表未完全设置为所选

正则表达式将文本替换为标签 html 以字符开头

Woocommerce注册中的自定义复选框验证错误提示问题

Php 的新WeakMap - 枚举曾经被垃圾收集过吗?

PHP 中的 curl 验证失败(openssh 连接正常)

复杂的mysql查询问题