ChatGPT解决这个技术问题 Extra ChatGPT

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

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

我最近开始学习 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

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() 等)。但是,我该如何运行特定的查找呢?我听说过 Specification Pattern,但在我看来,这只会减少整个记录集(通过 IsSatisfiedBy()),如果您从数据库中提取,这显然会产生重大的性能问题。

帮助?

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


C
Community

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

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

目标:

创建一个用于查看和编辑用户的基本控制器的完整示例。

所有代码都必须是完全可测试和可模拟的。

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

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

为了获得最佳性能,控制器应该只接收他们需要的数据——没有额外的字段。

实现应该利用某种类型的数据映射器来简化开发。

实现应该具有执行复杂数据查找的能力。

解决方案

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

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

R(读)没那么容易。这里没有模型,只有 value objects。使用数组 if you prefer。这些对象可能代表单个模型或多个模型的混合,实际上是任何东西。这些本身并不是很有趣,但是它们是如何生成的。我正在使用我称之为 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 数据库。请注意使用 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(读取)。查询对象只是某种类型的数据查找逻辑的封装。他们不是查询构建器。通过像我们的存储库一样抽象它,我们可以更改它的实现并更容易地对其进行测试。查询对象的示例可能是 AllUsersQueryAllActiveUsersQuery,甚至是 MostCommonUserFirstNames

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

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

存储库通常是特定于模型的,但查询通常涉及多个模型。那么你将你的方法放在哪个存储库中?

这使我的存储库非常简单——而不是臃肿的方法类。

所有查询现在都组织到它们自己的类中。

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

对于我的示例,我将创建一个查询对象来查找“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,所以通常我会在我的应用程序中直接引用这些模型。但是,在我有更复杂的查询的情况下,我将使用查询对象来使这些更可重用。我还应该注意,我总是将我的模型注入我的方法中,使它们更容易在我的测试中模拟。


@PeeHaa 同样,这是为了使示例保持简单。如果代码片段与手头的主题不相关,则将其排除在示例之外是很常见的。实际上,我会传递我的依赖项。
有趣的是,您将“创建”、“更新”和“删除”从“阅读”中分离出来。认为值得一提的是命令查询职责分离(CQRS),它正式做到了这一点。 martinfowler.com/bliki/CQRS.html
@Jonathan自从您回答自己的问题以来已经一年半了。我想知道您是否仍然对您的回答感到满意,这是否是您现在大多数项目的主要解决方案?在过去的几周里,我一直在阅读关于存储库的分配,并且我看到很多人对如何实施分配有自己的解释。您称其为查询对象,但这是现有模式对吗?我想我已经看到它被用于其他语言。
@Jonathan:您如何处理应该让用户不是“ID”而是“用户名”的查询,甚至是具有多个条件的更复杂的查询?
@Gizzmo 使用查询对象,您可以传递额外的参数来帮助您进行更复杂的查询。例如,您可以在构造函数中执行此操作:new Query\ComplexUserLookup($username, $anotherCondition)。或者,通过 setter 方法 $query->setUsername($username); 执行此操作。您可以真正设计它,但它对您的特定应用程序有意义,我认为查询对象在这里留下了很大的灵活性。
r
ryan1234

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

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

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

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

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

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

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

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

使用临时查询的缺点:

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

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

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

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

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


感谢您非常彻底的回答。你让我现在开始思考了。我在这里最担心的是我读到的所有内容都不是SELECT *,而是只选择您需要的字段。例如,see this question。至于你所说的所有这些临时查询,我当然明白你来自哪里。我现在有一个非常大的应用程序,其中有很多。那是我的“嗯,它必须给某个地方!”那一刻,我选择了最高性能。但是,现在我正在处理很多不同的查询。
一个跟进的想法。我看到了使用 R-CUD 方法的建议。由于 reads 通常是出现性能问题的地方,因此您可以对它们使用更自定义的查询方法,这种方法不会转化为实际的业务对象。然后,对于 createupdatedelete,使用 ORM,它适用于整个对象。对这种方法有什么想法吗?
作为使用“选择 *”的说明。我过去做过,它工作正常 - 直到我们点击 varchar(max) 字段。那些杀死了我们的查询。因此,如果您有带有整数、小文本字段等的表格,那还不错。感觉不自然,但软件就是这样。坏事突然变好,反之亦然。
R-CUD 方法实际上是 CQRS
@ryan1234 “一天结束时的复杂性必须存在于某个地方。”这次真是万分感谢。让我感觉好多了。
C
Constantin Galbenu

我使用以下接口:

存储库 - 加载、插入、更新和删除实体

选择器 - 根据过滤器在存储库中查找实体

Filter - 封装过滤逻辑

我的 Repository 与数据库无关;实际上它没有指定任何持久性;它可以是任何东西:SQL 数据库、xml 文件、远程服务、来自外太空的外星人等。对于搜索功能,Repository 构造了一个可以过滤、LIMIT-ed、排序和计数的 Selector。最后,选择器从持久性中获取一个或多个 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);
    }
}

想法是通用 Selector 使用 Filter,但实现 SqlSelector 使用 SqlFilterSqlSelectorFilterAdapter 将通用 Filter 改编为具体 SqlFilter

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

其他选择器实现,如 InMemorySelector,使用其特定的 InMemorySelectorFilterAdapterFilter 转换为 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();

PS这是我真实代码的简化


“存储库 - 加载、插入、更新和删除实体”这是“服务层”、“DAO”、“BLL”可以做的事情
W
Will

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

#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。

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


我不喜欢这种方法的是,如果你有一个 MongoUserRepository,那和你的 DbUserRepository 会返回不同的对象。 Db 返回一个 Eloquent\Model 和 Mongo 自己的东西。当然,更好的实现是让两个存储库都返回一个单独的 Entity\User 类的实例/集合。这样,当您切换到使用 MongoRepository 时,您就不会错误地依赖 Eloquent\Model 的 DB 方法
我绝对同意你的观点。为了避免这种情况,我可能会做的是永远不要在 Eloquent 要求类之外使用这些方法。因此,get 函数可能应该是私有的,并且只能在类中使用,因为正如您所指出的,它会返回其他存储库无法返回的东西。
T
TFennis

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

首先,我们定义模型,例如使用 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)
    {
        //...
    }
}

在我们的例子中,$db 是一个能够加载实体的 ORM。该模型指示 ORM 加载一组特定类型的实体。 ORM 包含一个映射,并使用该映射将该实体的所有字段注入到实体中。然而,对于外部字段,仅加载这些对象的 id。在这种情况下,OrderModel 仅使用所引用订单的 ID 创建 OrderEntity。当 PersistentEntity::getFieldOrderEntity 调用时,实体指示它的模型将所有字段延迟加载到 OrderEntity 中。与一个 UserEntity 关联的所有 OrderEntity 都被视为一个结果集,并将立即加载。

这里的神奇之处在于我们的模型和 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 中不是一个很大的问题,但我不建议其他系统使用。


L
Logan Bailey

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

问题 #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]
    }
}

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


k
kordy

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

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

上面已经提供了一个优雅的 answer,但是我将尝试给出另一个示例,我认为它更简单并且可以作为新项目的起点。

如代码所示,我们只需要 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);

a
abenevaut

我建议 https://packagist.org/packages/prettus/l5-repository 作为供应商在 Laravel5 中实现存储库/标准等:D


A
AVProgrammer

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

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

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

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

B
Brian

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

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

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

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


S
Sudo
   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();

所以我认为