333

前言:我正在尝试在具有关系数据库的 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 *) 方法。但是,在我的应用程序中,我总是试图限制我获得的字段数量,因为这通常会增加开销并减慢速度。对于那些使用这种模式的人,你如何处理这个问题?

问题 #2:方法太多

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

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • 查找AllByAgeAndGender
  • 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:无法匹配接口

我看到了将接口用于存储库的好处,因此我可以换掉我的实现(用于测试目的或其他目的)。我对接口的理解是它们定义了实现必须遵循的契约。在您开始将其他方法添加到您的存储库(如findAllInCountry(). 现在我需要更新我的界面以使其也具有此方法,否则其他实现可能没有它,这可能会破坏我的应用程序。这感觉很疯狂……尾巴摇着狗的情况。

规范模式?

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

帮助?

显然,在使用存储库时,我需要重新考虑一些事情。任何人都可以启发如何最好地处理?

4

11 回答 11

233

我想我会尽力回答我自己的问题。以下只是解决我最初问题中问题 1-3 的一种方法。

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

目标:

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

解决方案

我将我的持久存储(数据库)交互分为两类:R(读取)和CUD(创建、更新、删除)。我的经验是读取确实是导致应用程序变慢的原因。尽管数据操作 (CUD) 实际上速度较慢,但​​它发生的频率要低得多,因此也不那么令人担忧。

CUD(创建、更新、删除)很容易。这将涉及使用实际模型,然后将其传递给我Repositories的持久性。请注意,我的存储库仍将提供 Read 方法,但仅用于创建对象,而不是显示。稍后再谈。

R(读)不是那么容易。这里没有模型,只有值对象如果您愿意,请使用数组。这些对象可能代表单个模型或多个模型的混合,实际上是任何东西。这些本身并不是很有趣,但是它们是如何生成的。我正在使用我正在调用的内容Query Objects

编码:

用户模型

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

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(读取)。查询对象只是某种类型的数据查找逻辑的封装。他们不是查询构建器。通过像我们的存储库一样抽象它,我们可以更改它的实现并更容易地对其进行测试。查询对象的一个​​示例可能是AllUsersQueryor AllActiveUsersQuery,甚至是MostCommonUserFirstNames

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

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

对于我的示例,我将创建一个查询对象来查找“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;
    }
}

最后的想法:

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

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

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

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

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


2015 年 7 月更新:

我在评论中被问到我在哪里结束了这一切。嗯,其实也没有那么远。老实说,我仍然不太喜欢存储库。我发现它们对于基本查找来说太过分了(特别是如果你已经在使用 ORM),并且在处理更复杂的查询时会很混乱。

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

于 2013-04-26T13:45:44.123 回答
53

根据我的经验,以下是您问题的一些答案:

问:我们如何处理带回我们不需要的字段?

答:根据我的经验,这实际上归结为处理完整的实体与临时查询。

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

即席查询返回一些数据,但除此之外我们一无所知。当数据在应用程序中传递时,它是在没有上下文的情况下完成的。是User吗?User附上一些信息Order?我们真的不知道。

我更喜欢使用完整的实体。

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

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

使用临时查询的缺点:

  1. 您最终会在许多查询中得到基本相同的数据。例如,使用 a User,您最终会select *为许多调用编写基本相同的内容。一个呼叫将获得 10 个字段中的 8 个,一个将获得 10 个字段中的 5 个,一个将获得 10 个字段中的 7 个。为什么不将所有字段都替换为一个获得 10 个字段中的 10 个的呼叫?这不好的原因是重构/测试/模拟是谋杀。
  2. 随着时间的推移,很难对您的代码进行高层次的推理。而不是像“为什么User这么慢?”这样的陈述。您最终会跟踪一次性查询,因此错误修复往往很小且本地化。
  3. 底层技术真的很难替代。如果您现在将所有内容都存储在 MySQL 中并想迁移到 MongoDB,那么替换 100 个 ad-hoc 调用比替换少数实体要困难得多。

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

答:除了合并电话之外,我还没有真正看到解决此问题的任何方法。存储库中的方法调用真正映射到应用程序中的功能。功能越多,数据特定调用就越多。您可以推回功能并尝试将类似的调用合并为一个。

归根结底,复杂性必须存在于某个地方。使用存储库模式,我们将其推送到存储库界面,而不是制作一堆存储过程。

有时我不得不告诉自己,“嗯,它必须给某个地方!没有灵丹妙药。”

于 2013-04-23T22:33:47.237 回答
28

我使用以下接口:

  • Repository- 加载、插入、更新和删除实体
  • Selector- 在存储库中根据过滤器查找实体
  • Filter- 封装过滤逻辑

MyRepository与数据库无关;实际上它没有指定任何持久性;它可以是任何东西:SQL 数据库、xml 文件、远程服务、来自外太空的外星人等。对于搜索功能,可以过滤、编辑、排序和计数的Repository构造。最后,选择器从持久性中获取一个或多个。SelectorLIMITEntities

这是一些示例代码:

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

想法是泛型Selector使用Filter但实现SqlSelector使用SqlFilter;使SqlSelectorFilterAdapter泛型适应Filter具体SqlFilter

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

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

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

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

于 2016-08-18T09:45:31.077 回答
5

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

#1 和 2

这是您的 ORM 进行繁重工作的理想场所。如果您正在使用实现某种 ORM 的模型,您可以使用它的方法来处理这些事情。如果需要,可以创建自己的实现 Eloquent 方法的 orderBy 函数。以 Eloquent 为例:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

您似乎正在寻找的是 ORM。没有理由您的存储库不能基于一个。这需要用户扩展雄辩,但我个人认为这不是问题。

但是,如果您确实想避免使用 ORM,则必须“自己动手”才能获得所需的内容。

#3

接口不应该是硬性要求。有些东西可以实现一个接口并添加到它。它不能做的是未能实现该接口所需的功能。您还可以扩展类之类的接口以保持 DRY。

也就是说,我才刚刚开始掌握,但这些认识对我有所帮助。

于 2013-04-25T21:06:51.257 回答
3

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

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

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 加载一组特定类型的实体。ORM 包含一个映射,并使用该映射将该实体的所有字段注入到实体中。然而,对于外部字段,仅加载这些对象的 id。在这种情况下,OrderModel创建OrderEntitys 仅具有引用订单的 id。当PersistentEntity::getFieldOrderEntity实体调用时,它指示它的模型将所有字段延迟加载到OrderEntitys. 与一个 UserEntity 关联的所有OrderEntitys 都被视为一个结果集,并将立即加载。

这里的神奇之处在于我们的模型和 ORM 将所有数据注入到实体中,而实体只是getFieldPersistentEntity. 总而言之,我们总是加载所有字段,但在必要时会加载引用外部实体的字段。仅仅加载一堆字段并不是真正的性能问题。然而,加载所有可能的外国实体会大大降低性能。

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

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

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

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

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

于 2013-04-25T14:26:17.330 回答
3

这些是我见过的一些不同的解决方案。它们每个都有优点和缺点,但由您决定。

问题 #1:字段过多

这是一个重要方面,尤其是当您考虑Index-Only Scans时。我看到解决这个问题的两种解决方案。您可以更新您的函数以接受一个可选的数组参数,该参数将包含要返回的列的列表。如果此参数为空,您将返回查询中的所有列。这可能有点奇怪。根据参数,您可以检索对象或数组。您还可以复制所有函数,以便拥有两个运行相同查询的不同函数,但一个返回一个列数组,另一个返回一个对象。

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

问题 #2:方法太多

一年前我曾短暂地与Propel ORM合作过,这是基于我从那次经历中所记得的。Propel 可以选择根据现有的数据库模式生成其类结构。它为每个表创建两个对象。第一个对象是一长串访问功能,类似于您当前列出的内容;findByAttribute($attribute_value). 下一个对象继承自第一个对象。您可以更新此子对象以构建更复杂的 getter 函数。

另一种解决方案是将__call()未定义的功能映射到可操作的东西。您的__call方法将能够将 findById 和 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]
    }
}

我希望这至少能有所帮助。

于 2013-04-25T14:58:58.557 回答
1

我认为在这种情况下, graphQL是一个很好的候选者,可以提供一种大规模的查询语言,而不会增加数据存储库的复杂性。

但是,如果您现在不想使用 graphQL,还有另一种解决方案。通过使用DTO,其中一个对象用于在进程之间传输数据,在这种情况下是在服务/控制器和存储库之间。

上面已经提供了一个优雅的答案,但是我将尝试举另一个例子,我认为它更简单,可以作为新项目的起点。

如代码所示,我们只需要 4 个方法来进行 CRUD 操作。该find方法将用于通过传递对象参数来列出和读取。后端服务可以基于 URL 查询字符串或基于特定参数构建定义的查询对象。

如果需要,查询对象 ( SomeQueryDto) 也可以实现特定的接口。并且很容易在以后扩展而不会增加复杂性。

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

示例用法:

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);
于 2020-03-03T17:06:57.133 回答
0

我建议https://packagist.org/packages/prettus/l5-repository作为供应商在 Laravel5 中实现 Repositories/Criterias 等:D

于 2018-11-12T17:47:47.927 回答
0

我同意@ryan1234 的观点,即您应该在代码中传递完整的对象,并且应该使用通用查询方法来获取这些对象。

Model::where(['attr1' => 'val1'])->get();

对于外部/端点使用,我真的很喜欢 GraphQL 方法。

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}
于 2018-12-22T00:57:39.163 回答
0

问题 #3:无法匹配接口

我看到了将接口用于存储库的好处,因此我可以换掉我的实现(用于测试目的或其他目的)。我对接口的理解是它们定义了实现必须遵循的契约。在您开始向存储库添加其他方法(如 findAllInCountry())之前,这非常有用。现在我需要更新我的接口以使其也具有此方法,否则其他实现可能没有它,这可能会破坏我的应用程序。这感觉很疯狂……尾巴摇着狗的情况。

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

通用方法将允许实现任何查询,因此将防止在过渡期间发生重大更改。有针对性的方法允许您在有意义时优化呼叫,并且可以应用于多个服务提供商。

这种方法类似于执行特定优化任务的硬件实现,而软件实现则完成轻量级的工作或灵活的实现。

于 2020-02-03T23:05:01.997 回答
0
   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();

所以我认为

于 2020-06-29T21:18:13.457 回答