php PHP中正确的存储库模式设计?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/16176990/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Proper Repository Pattern Design in PHP?
提问by Jonathan
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. I've read about repositories and using an IoC containerto "inject" it into my controllers. Very cool stuff. But now have some practical questions about repository design. Consider the follow example.
我最近开始在 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)
{
}
}
Issue #1: Too many fields
问题 #1:字段太多
All of these find methods use a select all fields (SELECT *
) approach. However, in my apps, I'm always trying to limit the number of fields I get, as this often adds overhead and slows things down. For those using this pattern, how do you deal with this?
所有这些查找方法都使用全选字段 ( SELECT *
) 方法。然而,在我的应用程序中,我总是试图限制我获得的字段数量,因为这通常会增加开销并减慢速度。对于使用这种模式的人,您如何处理?
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
- findAllInCountry
- findAllWithEmailAddressSet
- findAllByAgeAndGender
- findAllByAgeAndGenderOrderByAge
- Etc.
- findAllByNameAndStatus
- 查找所有国家
- findAllWithEmailAddressSet
- 按年龄和性别查找全部
- findAllByAgeAndGenderOrderByAge
- 等等。
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()
. 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.
我看到了使用存储库接口的好处,所以我可以换掉我的实现(用于测试目的或其他目的)。我对接口的理解是它们定义了一个实现必须遵循的契约。这很好,直到您开始向存储库添加其他方法,例如findAllInCountry()
. 现在我需要更新我的接口以也有这个方法,否则其他实现可能没有它,这可能会破坏我的应用程序。这感觉很疯狂……尾巴摇着狗的情况。
Specification Pattern?
规格模式?
This leads me to believe that repository should only have a fixed number of methods (like save()
, remove()
, find()
, findAll()
, etc). But then how do I run specific lookups? I've heard of the Specification Pattern, but it seems to me that this only reduces an entire set of records (via IsSatisfiedBy()
), which clearly has major performance issues if you're pulling from a database.
这使我相信,库应该只有方法固定数量(如save()
,remove()
,find()
,findAll()
,等)。但是,我如何运行特定的查找?我听说过Specification Pattern,但在我看来,这只会减少一整套记录(via IsSatisfiedBy()
),如果您从数据库中提取,这显然存在重大性能问题。
Help?
帮助?
Clearly, I need to rethink things a little when working with repositories. Can anyone enlighten on how this is best handled?
显然,在使用存储库时,我需要重新考虑一些事情。任何人都可以启发如何最好地处理?
采纳答案by Jonathan
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
. - All code must be fully testable and mockable.
- The controller should have no idea where the data is stored (meaning it can be changed).
- Example to show a SQL implementation (most common).
- For maximum performance, controllers should only receive the data they need—no extra fields.
- Implementation should leverage some type of data mapper for ease of development.
- Implementation should have the ability to perform complex data lookups.
- 创建用于查看和编辑的基本控制器的完整示例
Users
。 - 所有代码都必须是完全可测试和可模拟的。
- 控制器应该不知道数据存储在哪里(意味着它可以更改)。
- 显示 SQL 实现的示例(最常见)。
- 为了获得最佳性能,控制器应该只接收他们需要的数据——没有额外的字段。
- 实现应该利用某种类型的数据映射器来简化开发。
- 实现应该能够执行复杂的数据查找。
The Solution
解决方案
I'm splitting my persistent storage (database) interaction into two categories: R(Read) and CUD(Create, Update, Delete). My experience has been that reads are really what causes an application to slow down. And while data manipulation (CUD) is actually slower, it happens much less frequently, and is therefore much less of a concern.
我将我的持久存储(数据库)交互分为两类:R(读取)和CUD(创建、更新、删除)。我的经验是,读取确实是导致应用程序变慢的原因。虽然数据操作 (CUD) 实际上更慢,但它发生的频率要低得多,因此不太值得关注。
CUD(Create, Update, Delete) is easy. This will involve working with actual models, which are then passed to my Repositories
for persistence. Note, my repositories will still provide a Read method, but simply for object creation, not display. More on that later.
CUD(创建、更新、删除)很容易。这将涉及使用实际模型,然后将其传递给我Repositories
的持久性。请注意,我的存储库仍将提供 Read 方法,但仅用于创建对象,而不是显示。稍后再谈。
R(Read) is not so easy. No models here, just value objects. Use arrays if you prefer. These objects may represent a single model or a blend of many models, anything really. These are not very interesting on their own, but how they are generated is. I'm using what I'm calling Query Objects
.
R(读取)并不那么容易。这里没有模型,只有值对象。如果您愿意,请使用数组。这些对象可能代表单个模型或多个模型的混合,实际上是任何东西。这些本身并不是很有趣,但它们是如何生成的。我正在使用我所说的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. Just pure model glory. Add your getters, setters, validation, whatever.
让我们从我们的基本用户模型开始。请注意,根本没有 ORM 扩展或数据库内容。只是纯粹的模特荣耀。添加您的 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. Note the use of a data mapperto prevent having to write repetitive SQL queries.
现在创建我的接口实现。如前所述,我的示例将使用 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');
}
}
Query Object Interface
查询对象接口
Now with CUD(Create, Update, Delete) taken care of by our repository, we can focus on the R(Read). Query objects are simply an encapsulation of some type of data lookup logic. They are notquery 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
.
现在通过我们的存储库处理CUD(创建、更新、删除),我们可以专注于R(读取)。查询对象只是某种类型的数据查找逻辑的封装。它们不是查询构建器。通过像我们的存储库一样抽象它,我们可以更改它的实现并更容易地测试它。查询对象的一个示例可能是AllUsersQuery
或AllActiveUsersQuery
,甚至是MostCommonUserFirstNames
。
You may be thinking "can't I just create methods in my repositories for those queries?" Yes, but here is why I'm not doing this:
您可能会想“我不能在我的存储库中为这些查询创建方法吗?” 是的,但这就是我不这样做的原因:
- My repositories are meant for working with model objects. In a real world app, why would I ever need to get the
password
field if I'm looking to list all my users? - Repositories are often model specific, yet queries often involve more than one model. So what repository do you put your method in?
- This keeps my repositories very simple—not an bloated class of methods.
- All queries are now organized into their own classes.
- Really, at this point, repositories exist simply to abstract my database layer.
- 我的存储库用于处理模型对象。在现实世界的应用程序中,
password
如果我要列出所有用户,为什么还需要获取该字段? - 存储库通常是特定于模型的,但查询通常涉及多个模型。那么你把你的方法放在哪个存储库中?
- 这使我的存储库非常简单——而不是一个臃肿的方法类。
- 所有查询现在都组织到自己的类中。
- 真的,在这一点上,存储库的存在只是为了抽象我的数据库层。
For my example I'll create a query object to lookup "AllUsers". Here is the interface:
对于我的示例,我将创建一个查询对象来查找“AllUsers”。这是界面:
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
. This could be tricky with my data mapper, and I may want to write some actual SQL
in this situation. No problem, here is what this query object could look like:
在继续讨论控制器之前,我想展示另一个示例来说明它的强大之处。也许我有一个报告引擎,需要为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...";
}
}
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. However, you absolutely can write SQL where needed (complicated queries, reporting, etc.). And when you do, it's nicely tucked away into a properly named class.
我使用数据映射器来帮助开发,因为为常见任务编写重复的 SQL 是荒谬的。但是,您绝对可以在需要的地方编写 SQL(复杂的查询、报告等)。当你这样做时,它会很好地隐藏在一个正确命名的类中。
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. 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.
我通常使用 ActiveRecord 样式的 ORM,因此大多数情况下,我只会在整个应用程序中直接引用这些模型。但是,在我有更复杂查询的情况下,我将使用查询对象来使这些更可重用。我还应该注意,我总是将我的模型注入到我的方法中,使它们更容易在我的测试中模拟。
回答by ryan1234
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. It has properties and methods, etc. It's a first class citizen in your codebase.
一个完整的实体就像一个User
对象。它具有属性和方法等。它是您代码库中的一等公民。
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
? A User
with some Order
information attached? We don't really know.
即席查询会返回一些数据,但除此之外我们一无所知。当数据在应用程序中传递时,它是在没有上下文的情况下完成的。是User
吗?一个User
与一些Order
信息附?我们真的不知道。
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:
您经常会带回不会使用的数据是对的,但您可以通过多种方式解决此问题:
- Aggressively cache the entities so you only pay the read price once from the database.
- Spend more time modeling your entities so they have good distinctions between them. (Consider splitting a large entity into two smaller entities, etc.)
- Consider having multiple versions of entities. You can have a
User
for the back end and maybe aUserSmall
for AJAX calls. One might have 10 properties and one has 3 properties.
- 积极缓存实体,因此您只需从数据库中支付一次读取费用。
- 花更多时间为您的实体建模,以便它们之间有很好的区别。(考虑将一个大实体拆分为两个较小的实体等)
- 考虑拥有多个版本的实体。您可以有一个
User
用于后端,也可以有一个UserSmall
用于 AJAX 调用。一个可能有 10 个属性,一个可能有 3 个属性。
The downsides of working with ad-hoc queries:
使用临时查询的缺点:
- You end up with essentially the same data across many queries. For example, with a
User
, you'll end up writing essentially the sameselect *
for many calls. 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? The reason this is bad is that it is murder to re-factor/test/mock. - 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?" you end up tracking down one-off queries and so bug fixes tend to be small and localized. - 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.
- 您最终会在许多查询中获得基本相同的数据。例如,使用 a
User
,您最终会select *
为许多调用编写基本相同的代码。一个调用将获得 10 个字段中的 8 个,一个将获得 10 个中的 5 个,一个将获得 10 个中的 7 个。为什么不将所有调用替换为一个调用,该调用获得 10 个中的 10 个?这很糟糕的原因是重构/测试/模拟是谋杀。 - 随着时间的推移,很难对您的代码进行高层次的推理。而不是像“为什么
User
这么慢?”这样的陈述。您最终会跟踪一次性查询,因此错误修复往往很小且本地化。 - 替换底层技术真的很难。如果您现在将所有内容都存储在 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."
有时我不得不告诉自己,“好吧,它必须给某个地方!没有灵丹妙药。”
回答by Constantin Galbenu
I use the following interfaces:
我使用以下接口:
Repository
- loads, inserts, updates and deletes entitiesSelector
- finds entities based on filters, in a repositoryFilter
- encapsulates the filtering logic
Repository
- 加载、插入、更新和删除实体Selector
- 在存储库中根据过滤器查找实体Filter
- 封装过滤逻辑
My Repository
is database agnostic; 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. In the end, the selector fetches one or more Entities
from the persistence.
我Repository
的数据库不可知;事实上,它没有指定任何持久性;它可以是任何东西:SQL 数据库、xml 文件、远程服务、来自外太空的外星人等。对于搜索功能,Repository
构造 anSelector
可以过滤、LIMIT
-ed、排序和计数。最后,选择器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
; the SqlSelectorFilterAdapter
adapts a generic Filter
to a concrete SqlFilter
.
想法是泛型Selector
使用Filter
但实现SqlSelector
使用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
; so, every selector implementation comes with its own filter adapter.
其他选择器的实现,如InMemorySelector
,从变换Filter
到InMemoryFilter
使用其特定InMemorySelectorFilterAdapter
; 因此,每个选择器实现都带有自己的过滤器适配器。
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();
P.S. This is a simplification of my real code
PS这是我的真实代码的简化
回答by Will Meldon
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. 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. Make your own orderBy functions that implement the Eloquent methods if you need to. Using Eloquent for instance:
这是您的 ORM 完成繁重工作的理想场所。如果您正在使用实现某种 ORM 的模型,您可以使用它的方法来处理这些事情。如果需要,创建自己的 orderBy 函数来实现 Eloquent 方法。以 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. 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.
您似乎正在寻找的是 ORM。没有理由您的存储库不能基于一个。这将需要用户扩展雄辩,但我个人不认为这是一个问题。
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.
也就是说,我才刚刚开始掌握,但这些认识对我有所帮助。
回答by TFennis
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. When a UserEntity
is loaded from a model all fields are loaded. 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:
首先,我们定义了诸如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)
{
//...
}
}
In our case $db
is an ORM that is able to load entities. The model instructs the ORM to load a set of entities of a specific type. The ORM contains a mapping and uses that to inject all the fields for that entity in to the entity. For foreign fields however only the id's of those objects are loaded. In this case the OrderModel
creates OrderEntity
s with only the id's of the referenced orders. When PersistentEntity::getField
gets called by the OrderEntity
the entity instructs it's model to lazy load all the fields into the OrderEntity
s. All the OrderEntity
s associated with one UserEntity are treated as one result-set and will be loaded at once.
在我们的例子中$db
是一个能够加载实体的 ORM。该模型指示 ORM 加载一组特定类型的实体。ORM 包含一个映射,并使用它将该实体的所有字段注入到该实体中。然而,对于外部字段,仅加载这些对象的 id。在这种情况下,仅使用所引用订单的 id 来OrderModel
创建OrderEntity
s。当PersistentEntity::getField
被OrderEntity
实体调用时,它会指示它的模型将所有字段延迟加载到OrderEntity
s 中。OrderEntity
与一个 UserEntity 关联的所有s 都被视为一个结果集,并将立即加载。
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
. 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.
这里神奇的是,我们的模型和ORM注入所有的数据到实体和实体仅仅提供包装的通用功能getField
的提供方法PersistentEntity
。总而言之,我们总是加载所有字段,但在必要时加载引用外部实体的字段。只是加载一堆字段并不是真正的性能问题。然而,加载所有可能的外部实体会导致巨大的性能下降。
Now on to loading a specific set of users, based on a where clause. 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
. 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. Our where clauses are quite complicated but you could obviously make a simpler version easily.
现在根据 where 子句加载一组特定的用户。我们提供了一个面向对象的类包,允许您指定可以粘合在一起的简单表达式。在示例代码中,我将其命名为GetOptions
. 它是选择查询的所有可能选项的包装器。它包含 where 子句、group by 子句和其他所有内容的集合。我们的 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. Because all fields are eventually loaded through the getField
method you could load some fields last minute when that method is called. This is not a very big problem in PHP, but I would not recommend for other systems.
编辑:此外,如果您真的不想立即加载某些字段,则可以在 ORM 映射中指定延迟加载选项。因为所有字段最终都是通过该getField
方法加载的,所以您可以在调用该方法时最后一分钟加载一些字段。这在 PHP 中不是一个很大的问题,但我不会推荐用于其他系统。
回答by Logan Bailey
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 ORMa year ago and this is based off what I can remember from that experience. Propel has the option to generate its class structure based off the existing database schema. 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)
. The next object inherits from this first object. You can update this child object to build in your more complex getter functions.
一年前我曾与Propel ORM进行过短暂的合作,这是基于我从那次经历中所记得的。Propel 可以选择根据现有数据库模式生成其类结构。它为每个表创建两个对象。第一个对象是一长串访问功能,类似于您当前列出的;findByAttribute($attribute_value)
. 下一个对象继承自第一个对象。您可以更新此子对象以构建更复杂的 getter 函数。
Another solution would be using __call()
to map non defined functions to something actionable. Your __call
method would be would be able to parse the findById and findByName into different queries.
另一种解决方案是使用__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]
}
}
I hope this helps at least some what.
我希望这至少有一些帮助。
回答by abenevaut
I suggest https://packagist.org/packages/prettus/l5-repositoryas vendor to implement Repositories/Criterias etc ... in Laravel5 :D
我建议https://packagist.org/packages/prettus/l5-repository作为供应商在 Laravel5 中实现 Repositories/Criterias 等......:D
回答by AVProgrammer
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
}
}
}
回答by Brian
Issue #3: Impossible to match an interface
I see the benefit in using interfaces for repositories, so I can swap out my implementation (for testing purposes or other). My understanding of interfaces is that they define a contract that an implementation must follow. This is great until you start adding additional methods to your repositories like findAllInCountry(). Now I need to update my interface to also have this method, otherwise, other implementations may not have it, and that could break my application. By this feels insane...a case of the tail wagging the dog.
问题 #3:无法匹配接口
我看到了使用存储库接口的好处,所以我可以换掉我的实现(用于测试目的或其他目的)。我对接口的理解是它们定义了一个实现必须遵循的契约。这很好,直到您开始向存储库添加其他方法,例如 findAllInCountry()。现在我需要更新我的接口以也有这个方法,否则其他实现可能没有它,这可能会破坏我的应用程序。这感觉很疯狂……尾巴摇着狗的情况。
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.
这种方法类似于执行特定优化任务的硬件实现,而软件实现则是轻量级的或灵活的实现。
回答by kordy
I think graphQLis 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. By using a DTOwhere an object is used for carring the data between processes, in this case between the service/controller and the repository.
但是,如果您暂时不想使用 graphQL,还有另一种解决方案。通过使用DTO,其中对象用于在进程之间传输数据,在这种情况下是在服务/控制器和存储库之间。
An elegant answeris 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. the find
method would be used for listing and reading by passing object argument.
Backend services could build the defined query object based on a URL query string or based on specific parameters.
如代码所示,我们只需要 4 个方法来进行 CRUD 操作。该find
方法将用于通过传递对象参数来列出和读取。后端服务可以基于 URL 查询字符串或基于特定参数构建定义的查询对象。
The query object (SomeQueryDto
) could also implement specific interface if needed. and is easy to be extended later without adding complexity.
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]/', '_$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);
', $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:
用法示例:
##代码##