简体   繁体   English

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

[英]Proper Repository Pattern Design in PHP?

Preface: I'm attempting to use the repository pattern in an MVC architecture with relational databases.前言:我正在尝试在具有关系数据库的 MVC 架构中使用存储库模式。

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.我最近开始在 PHP 中学习 TDD,并且我意识到我的数据库与我的应用程序的其余部分耦合得太紧密了。 I've read about repositories and using an IoC container to "inject" it into my controllers.我已经阅读了有关存储库并使用IoC 容器将其“注入”到我的控制器中的内容。 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)
    {
    }
}

Issue #1: Too many fields问题 #1:字段太多

All of these find methods use a select all fields ( SELECT * ) approach.所有这些查找方法都使用全选字段 ( SELECT * ) 方法。 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?对于使用这种模式的人,您如何处理?

Issue #2: Too many methods问题#2:方法太多

While this class looks nice right now, I know that in a real-world app I need a lot more methods.虽然这个类现在看起来不错,但我知道在现实世界的应用程序中我需要更多的方法。 For example:例如:

  • findAllByNameAndStatus findAllByNameAndStatus
  • findAllInCountry查找所有国家
  • findAllWithEmailAddressSet findAllWithEmailAddressSet
  • findAllByAgeAndGender按年龄和性别查找全部
  • findAllByAgeAndGenderOrderByAge findAllByAgeAndGenderOrderByAge
  • Etc.等等。

As you can see, there could be a 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));
    }
}

With my repository approach, I don't want to end up with this:使用我的存储库方法,我不想以这样的方式结束:

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

}

Issue #3: Impossible to match an interface问题 #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() .这很好,直到您开始向存储库添加其他方法,例如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.这感觉很疯狂……尾巴摇着狗的情况。

Specification Pattern?规格模式?

This leads me to believe that repository should only have a fixed number of methods (like save() , remove() , find() , findAll() , etc).这让我相信存储库应该只有固定数量的方法(如save()remove()find()findAll()等)。 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.我听说过Specification Pattern ,但在我看来,这只会减少一整套记录(通过IsSatisfiedBy() ),如果您从数据库中提取,这显然存在重大性能问题。

Help?帮助?

Clearly, I need to rethink things a little when working with repositories.显然,在使用存储库时,我需要重新考虑一些事情。 Can anyone enlighten on how this is best handled?任何人都可以启发如何最好地处理?

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.以下只是解决我最初问题中问题 1-3 的一种方法。

Disclaimer: I may not always use the right terms when describing patterns or techniques.免责声明:在描述模式或技术时,我可能不会总是使用正确的术语。 Sorry for that.对不起。

The Goals:目标:

  • Create a complete example of a basic controller for viewing and editing Users .创建用于查看和编辑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).显示 SQL 实现的示例(最常见)。
  • 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.实现应该能够执行复杂的数据查找。

The Solution解决方案

I'm splitting my persistent storage (database) interaction into two categories: R (Read) and CUD (Create, Update, Delete).我将持久存储(数据库)交互分为两类: R (读取)和CUD (创建、更新、删除)。 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 (Create, Update, Delete) is easy. CUD (创建、更新、删除)很容易。 This will involve working with actual models , which are then passed to my Repositories for persistence.这将涉及使用实际模型,然后将其传递到我的Repositories以进行持久化。 Note, my repositories will still provide a Read method, but simply for object creation, not display.请注意,我的存储库仍将提供 Read 方法,但仅用于创建对象,而不是显示。 More on that later.稍后再谈。

R (Read) is not so easy. R (读取)并不那么容易。 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 .我正在使用我所说的Query Objects

The Code:代码:

User Model用户模型

Let's start simple with our basic user model.让我们从我们的基本用户模型开始。 Note that there is no ORM extending or database stuff at all.请注意,根本没有 ORM 扩展或数据库内容。 Just pure model glory.只是纯粹的模特荣耀。 Add your getters, setters, validation, whatever.添加您的 getter、setter、验证等。

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.请记住,我的控制器不会知道数据实际存储在哪里。

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. save()方法负责创建和更新用户,仅取决于用户对象是否设置了 id。

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

SQL Repository Implementation SQL 存储库实现

Now to create my implementation of the interface.现在创建我的接口实现。 As mentioned, my example was going to be with an SQL database.如前所述,我的示例将使用 SQL 数据库。 Note the use of a data mapper to prevent having to write repetitive SQL queries.请注意使用数据映射器来防止必须编写重复的 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');
    }
}

Query Object Interface查询对象接口

Now with CUD (Create, Update, Delete) taken care of by our repository, we can focus on the R (Read).现在通过我们的存储库处理CUD (创建、更新、删除),我们可以专注于R (读取)。 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 .查询对象的一个​​例子可能是AllUsersQueryAllActiveUsersQuery ,甚至是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?在现实世界的应用程序中,如果我要列出所有用户,为什么还需要获取password字段?
  • 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.真的,在这一点上,存储库的存在只是为了抽象我的数据库层。

For my example I'll create a query object to lookup "AllUsers".对于我的示例,我将创建一个查询对象来查找“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();
    }
}

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 .也许我有一个报告引擎,需要为AllOverdueAccounts创建一个报告。 This could be tricky with my data mapper, and I may want to write some actual SQL in this situation.这对我的数据映射器来说可能很棘手,在这种情况下我可能想编写一些实际的SQL 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.我可以随心所欲地模拟它,甚至可以完全使用不同的实现。

The Controller控制器

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.我的存储库保持非常干净,而是将这种“混乱”组织到我的模型查询中。

I use a data mapper to help with development, as it's just ridiculous to write repetitive SQL for common tasks.我使用数据映射器来帮助开发,因为为常见任务编写重复的 SQL 是荒谬的。 However, you absolutely can write SQL where needed (complicated queries, reporting, etc.).但是,您绝对可以在需要的地方编写 SQL(复杂的查询、报告等)。 And when you do, it's nicely tucked away into a properly named class.当你这样做时,它很好地隐藏在一个正确命名的类中。

I'd love to hear your take on my approach!我很想听听你对我的方法的看法!


July 2015 Update: 2015 年 7 月更新:

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.我发现它们对于基本查找来说太过分了(特别是如果您已经在使用 ORM),并且在处理更复杂的查询时会很混乱。

I generally work with an ActiveRecord style ORM, so most often I'll just reference those models directly throughout my application.我通常使用 ActiveRecord 样式的 ORM,因此大多数情况下,我只会在整个应用程序中直接引用这些模型。 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.我还应该注意,我总是将我的模型注入到我的方法中,使它们更容易在我的测试中模拟。

Based on my experience, here are some answers to your questions:根据我的经验,以下是您的问题的一些答案:

Q: How do we deal with bringing back fields we don't need?问:我们如何处理带回不需要的字段?

A: From my experience this really boils down to dealing with complete entities versus ad-hoc queries.答:根据我的经验,这实际上归结为处理完整实体与临时查询。

A complete entity is something like a User object.一个完整的实体类似于一个User对象。 It has properties and methods, etc. It's a first class citizen in your codebase.它具有属性和方法等。它是您代码库中的一等公民。

An ad-hoc query returns some data, but we don't know anything beyond that.即席查询会返回一些数据,但除此之外我们一无所知。 As the data gets passed around the application, it is done so without context.当数据在应用程序中传递时,它是在没有上下文的情况下完成的。 Is it a User ?User吗? A User with some Order information attached?附加了一些Order信息的User We don't really know.我们真的不知道。

I prefer working with full entities.我更喜欢使用完整的实体。

You are right that you will often bring back data you won't use, but you can address this in various ways:您经常会带回不会使用的数据是对的,但您可以通过多种方式解决此问题:

  1. Aggressively cache the entities so you only pay the read price once from the database.积极缓存实体,因此您只需从数据库中支付一次读取费用。
  2. Spend more time modeling your entities so they have good distinctions between them.花更多时间为您的实体建模,以便它们之间有很好的区别。 (Consider splitting a large entity into two smaller entities, etc.) (考虑将一个大实体拆分为两个较小的实体等)
  3. Consider having multiple versions of entities.考虑拥有多个版本的实体。 You can have a User for the back end and maybe a UserSmall for AJAX calls.您可以有一个User作为后端,也可以有一个UserSmall用于 AJAX 调用。 One might have 10 properties and one has 3 properties.一个可能有 10 个属性,一个可能有 3 个属性。

The downsides of working with ad-hoc queries:使用临时查询的缺点:

  1. 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.例如,对于User ,您最终会为许多调用编写基本相同的select * 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?一个调用将获得 10 个字段中的 8 个,一个将获得 10 个中的 5 个,一个将获得 10 个中的 7 个。为什么不将所有调用替换为一个调用,该调用获得 10 个中的 10 个? The reason this is bad is that it is murder to re-factor/test/mock.这很糟糕的原因是重构/测试/模拟是谋杀。
  2. 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?"而不是像“为什么User这么慢?”这样的陈述。 you end up tracking down one-off queries and so bug fixes tend to be small and localized.您最终会跟踪一次性查询,因此错误修复往往很小且本地化。
  3. 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.如果您现在将所有内容都存储在 MySQL 中并希望迁移到 MongoDB,那么替换 100 个临时调用比替换少数实体要困难得多。

Q: I will have too many methods in my repository.问:我的存储库中有太多方法。

A: I haven't really seen any way around this other than consolidating calls.答:除了合并电话之外,我还没有真正看到任何解决方法。 The method calls in your repository really map to features in your application.存储库中的方法调用实际上映射到应用程序中的功能。 The more features, the more data specific calls.功能越多,特定数据调用就越多。 You can push back on features and try to merge similar calls into one.您可以推迟功能并尝试将类似的调用合并为一个。

The complexity at the end of the day has to exist somewhere.一天结束时的复杂性必须存在于某个地方。 With a repository pattern we've pushed it into the repository interface instead of maybe making a bunch of stored procedures.使用存储库模式,我们将它推送到存储库界面,而不是制作一堆存储过程。

Sometimes I have to tell myself, "Well it had to give somewhere! There are no silver bullets."有时我不得不告诉自己,“好吧,它必须给某个地方!没有灵丹妙药。”

I use the following interfaces:我使用以下接口:

  • Repository - loads, inserts, updates and deletes entities Repository - 加载、插入、更新和删除实体
  • Selector - finds entities based on filters, in a repository Selector - 根据过滤器在存储库中查找实体
  • Filter - encapsulates the filtering logic Filter - 封装过滤逻辑

My Repository is database agnostic;我的Repository与数据库无关; in fact it doesn't specify any persistence;事实上,它没有指定任何持久性; it could be anything: SQL database, xml file, remote service, an alien from outer space etc. For searching capabilities, the Repository constructs an Selector which can be filtered, LIMIT -ed, sorted and counted.它可以是任何东西:SQL 数据库、xml 文件、远程服务、来自外太空的外星人等。为了搜索功能, Repository构建了一个可以过滤、 LIMIT 、排序和计数的Selector In the end, the selector fetches one or more Entities from the persistence.最后,选择器从持久化中获取一个或多个Entities

Here is some sample code:下面是一些示例代码:

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

Then, one implementation:然后,一种实现:

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

The ideea is that the generic Selector uses Filter but the implementation SqlSelector uses SqlFilter ;想法是泛型Selector使用Filter而实现SqlSelector使用SqlFilter the SqlSelectorFilterAdapter adapts a generic Filter to a concrete SqlFilter . SqlSelectorFilterAdapter使通用Filter适应具体的SqlFilter

The client code creates Filter objects (that are generic filters) but in the concrete implementation of the selector those filters are transformed in SQL filters.客户端代码创建Filter对象(即通用过滤器),但在选择器的具体实现中,这些过滤器在 SQL 过滤器中进行了转换。

Other selector implementations, like InMemorySelector , transform from Filter to InMemoryFilter using their specific InMemorySelectorFilterAdapter ;其他选择器实施方式中,像InMemorySelector ,从变换FilterInMemoryFilter使用其特定InMemorySelectorFilterAdapter ; so, every selector implementation comes with its own filter adapter.因此,每个选择器实现都带有自己的过滤器适配器。

Using this strategy my client code (in the bussines layer) doesn't care about a specific repository or selector implementation.使用此策略,我的客户端代码(在业务层中)不关心特定的存储库或选择器实现。

/** @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 This is a simplification of my real code PS这是我的真实代码的简化

I'll add a bit on this as I am currently trying to grasp all of this myself.我会在这方面补充一点,因为我目前正试图自己掌握所有这些。

#1 and 2 #1 和 2

This is a perfect place for your ORM to do the heavy lifting.这是您的 ORM 完成繁重工作的理想场所。 If you are using a model that implements some kind of ORM, you can just use it's methods to take care of these things.如果您正在使用实现某种 ORM 的模型,您可以使用它的方法来处理这些事情。 Make your own orderBy functions that implement the Eloquent methods if you need to.如果需要,创建自己的 orderBy 函数来实现 Eloquent 方法。 Using Eloquent for instance:以 Eloquent 为例:

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

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

What you seem to be looking for is an ORM.您似乎正在寻找的是 ORM。 No reason your Repository can't be based around one.没有理由您的存储库不能基于一个。 This would require User extend eloquent, but I personally don't see that as a problem.这将需要用户扩展雄辩,但我个人不认为这是一个问题。

If you do however want to avoid an ORM, you would then have to "roll your own" to get what you're looking for.但是,如果您确实想避免使用 ORM,则您必须“自己动手”才能获得所需的内容。

#3 #3

Interfaces aren't supposed be hard and fast requirements.接口不应该是硬性要求。 Something can implement an interface and add to it.有些东西可以实现一个接口并添加到它。 What it can't do is fail to implement a required function of that interface.它不能做的是无法实现该接口所需的功能。 You can also extend interfaces like classes to keep things DRY.您还可以扩展像类这样的接口以保持 DRY。

That said, I'm just starting to get a grasp, but these realizations have helped me.也就是说,我才刚刚开始掌握,但这些认识对我有所帮助。

I can only comment on the way we (at my company) deal with this.我只能评论我们(在我的公司)处理这个问题的方式。 First of all performance is not too much of an issue for us, but having clean/proper code is.首先,性能对我们来说不是太大的问题,但拥有干净/正确的代码才是。

First of all we define Models such as a UserModel that uses an ORM to create UserEntity objects.首先,我们定义模型,例如使用 ORM 创建UserEntity对象的UserModel When a UserEntity is loaded from a model all fields are loaded.当从模型加载UserEntity所有字段都会加载。 For fields referencing foreign entities we use the appropriate foreign model to create the respective entities.对于引用外部实体的字段,我们使用适当的外部模型来创建相应的实体。 For those entities the data will be loaded ondemand.对于这些实体,数据将按需加载。 Now your initial reaction might be ...???...!!!现在你的第一反应可能是......?......!!! let me give you an example a bit of an example:让我给你举个例子:

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.在我们的例子中, $db是一个能够加载实体的 ORM。 The model instructs the ORM to load a set of entities of a specific type.该模型指示 ORM 加载一组特定类型的实体。 The ORM contains a mapping and uses that to inject all the fields for that entity in to the entity. ORM 包含一个映射,并使用它将该实体的所有字段注入到该实体中。 For foreign fields however only the id's of those objects are loaded.然而,对于外部字段,只加载这些对象的 id。 In this case the OrderModel creates OrderEntity s with only the id's of the referenced orders.在这种情况下, OrderModel创建OrderEntity s,其中仅包含引用订单的 id。 When PersistentEntity::getField gets called by the OrderEntity the entity instructs it's model to lazy load all the fields into the OrderEntity s.PersistentEntity::getFieldOrderEntity调用时,实体会指示它的模型将所有字段延迟加载到OrderEntity All the OrderEntity s associated with one UserEntity are treated as one result-set and will be loaded at once.与一个 UserEntity 关联的所有OrderEntity都被视为一个结果集,并将立即加载。

The magic here is that our model and ORM inject all data into the entities and that entities merely provide wrapper functions for the generic getField method supplied by PersistentEntity .这里的神奇之处在于我们的模型和 ORM 将所有数据注入到实体中,而实体仅为PersistentEntity提供的通用getField方法提供包装函数。 To summarize we always load all the fields, but fields referencing a foreign entity are loaded when necessary.总而言之,我们总是加载所有字段,但在必要时加载引用外部实体的字段。 Just loading a bunch of fields is not really a performance issue.只是加载一堆字段并不是真正的性能问题。 Load all possible foreign entities however would be a HUGE performance decrease.然而,加载所有可能的外部实体会导致巨大的性能下降。

Now on to loading a specific set of users, based on a where clause.现在根据 where 子句加载一组特定的用户。 We provide an object oriented package of classes that allow you to specify simple expression that can be glued together.我们提供了一个面向对象的类包,允许您指定可以粘合在一起的简单表达式。 In the example code I named it GetOptions .在示例代码中,我将其命名为GetOptions It's a wrapper for all possible options for a select query.它是选择查询的所有可能选项的包装器。 It contains a collection of where clauses, a group by clause and everything else.它包含 where 子句、group by 子句和其他所有内容的集合。 Our where clauses are quite complicated but you could obviously make a simpler version easily.我们的 where 子句相当复杂,但您显然可以轻松制作一个更简单的版本。

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

A simplest version of this system would be to pass the WHERE part of the query as a string directly to the model.该系统的最简单版本是将查询的 WHERE 部分作为字符串直接传递给模型。

I'm sorry for this quite complicated response.对于这个相当复杂的回复,我很抱歉。 I tried to summarize our framework as quickly and clear as possible.我试图尽可能快速和清晰地总结我们的框架。 If you have any additional questions feel free to ask them and I'll update my answer.如果您有任何其他问题,请随时问他们,我会更新我的答案。

EDIT: Additionally if you really don't want to load some fields right away you could specify a lazy loading option in your ORM mapping.编辑:此外,如果您真的不想立即加载某些字段,则可以在 ORM 映射中指定延迟加载选项。 Because all fields are eventually loaded through the getField method you could load some fields last minute when that method is called.因为所有字段最终都是通过getField方法加载的,所以您可以在调用该方法时最后一分钟加载一些字段。 This is not a very big problem in PHP, but I would not recommend for other systems.这在 PHP 中不是一个很大的问题,但我不会推荐用于其他系统。

These are some different solutions I've seen.这些是我见过的一些不同的解决方案。 There are pros and cons to each of them, but it is for you to decide.它们各有利弊,但由您来决定。

Issue #1: Too many fields问题 #1:字段太多

This is an important aspect especially when you take in to account Index-Only Scans .这是一个重要的方面,尤其是当您考虑仅索引扫描时 I see two solutions to dealing with this problem.我看到了处理这个问题的两种解决方案。 You can update your functions to take in an optional array parameter that would contain a list of a columns to return.您可以更新您的函数以接受一个可选的数组参数,该参数将包含要返回的列列表。 If this parameter is empty you'd return all of the columns in the query.如果此参数为空,您将返回查询中的所有列。 This can be a little weird;这可能有点奇怪; based off the parameter you could retrieve an object or an array.根据您可以检索对象或数组的参数。 You could also duplicate all of your functions so that you have two distinct functions that run the same query, but one returns an array of columns and the other returns an object.您还可以复制所有函数,以便您有两个不同的函数来运行相同的查询,但一个返回一个列数组,另一个返回一个对象。

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

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

Issue #2: Too many methods问题#2:方法太多

I briefly worked with Propel ORM a year ago and this is based off what I can remember from that experience.一年前我曾与Propel ORM进行过短暂的合作,这是基于我从那次经历中所记得的。 Propel has the option to generate its class structure based off the existing database schema. Propel 可以选择根据现有数据库模式生成其类结构。 It creates two objects for each table.它为每个表创建两个对象。 The first object is a long list of access function similar to what you have currently listed;第一个对象是一长串访问功能,类似于您当前列出的; findByAttribute($attribute_value) . findByAttribute($attribute_value) The next object inherits from this first object.下一个对象继承自第一个对象。 You can update this child object to build in your more complex getter functions.您可以更新此子对象以构建更复杂的 getter 函数。

Another solution would be using __call() to map non defined functions to something actionable.另一种解决方案是使用__call()将未定义的函数映射到可操作的东西。 Your __call method would be would be able to parse the findById and findByName into different queries.您的__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]
    }
}

I hope this helps at least some what.我希望这至少有一些帮助。

I think graphQL is a good candidate in such a case to provide a large scale query language without increasing the complexity of data repositories.我认为在这种情况下, graphQL是一个很好的候选者,它可以在不增加数据存储库复杂性的情况下提供大规模查询语言。

However, there's another solution if you don't want to go for the graphQL for now.但是,如果您暂时不想使用 graphQL,还有另一种解决方案。 By using a DTO where an object is used for carring the data between processes, in this case between the service/controller and the repository.通过使用DTO ,其中对象用于在进程之间传输数据,在这种情况下是在服务/控制器和存储库之间。

An elegant answer is already provided above, however I'll try to give another example that I think it's simpler and could serve as a starting point for a new project.上面已经提供了一个优雅的答案,但是我将尝试举另一个例子,我认为它更简单,可以作为新项目的起点。

As shown in the code, we would need only 4 methods for CRUD operations.如代码所示,我们只需要 4 个方法来进行 CRUD 操作。 the find method would be used for listing and reading by passing object argument. find方法将用于通过传递对象参数来列出和读取。 Backend services could build the defined query object based on a URL query string or based on specific parameters.后端服务可以基于 URL 查询字符串或基于特定参数构建定义的查询对象。

The query object ( SomeQueryDto ) could also implement specific interface if needed.如果需要,查询对象( SomeQueryDto )也可以实现特定的接口。 and is easy to be extended later without adding complexity.并且很容易在不增加复杂性的情况下进行扩展。

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

Example usage:用法示例:

$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);

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

I agree with @ryan1234 that you should pass around complete objects within the code and should use generic query methods to get those objects.我同意@ryan1234,你应该在代码中传递完整的对象,并且应该使用通用查询方法来获取这些对象。

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

For external/endpoint usage I really like the GraphQL method.对于外部/端点使用,我非常喜欢 GraphQL 方法。

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

Issue #3: Impossible to match an interface问题 #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().这很好,直到您开始向存储库添加其他方法,例如 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.这感觉很疯狂……尾巴摇着狗的情况。

My gut tells me this maybe requires an interface that implements query optimized methods alongside generic methods.我的直觉告诉我,这可能需要一个接口来实现查询优化方法和通用方法。 Performance sensitive queries should have targeted methods, while infrequent or light-weight queries get handled by a generic handler, maybe the the expense of the controller doing a little more juggling.性能敏感的查询应该有针对性的方法,而不频繁或轻量级的查询由通用处理程序处理,可能是控制器做更多杂耍的费用。

The generic methods would allow any query to be implemented, and so would prevent breaking changes during a transition period.通用方法将允许实现任何查询,因此将防止在过渡期间破坏性更改。 The targeted methods allow you to optimize a call when it makes sense to, and it can be applied to multiple service providers.目标方法允许您在有意义的时候优化调用,并且它可以应用于多个服务提供商。

This approach would be akin to hardware implementations performing specific optimized tasks, while software implementations do the light work or flexible implementation.这种方法类似于执行特定优化任务的硬件实现,而软件实现则是轻量级的或灵活的实现。

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

So i think所以我认为

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM