Jon*_*han 265 php database repository repository-pattern laravel
前言:我正在尝试在具有关系数据库的MVC架构中使用存储库模式.
我最近开始用PHP学习TDD,并且我意识到我的数据库与我的应用程序的其余部分非常接近.我已经阅读了有关存储库和使用IoC容器将其"注入"我的控制器的内容.很酷的东西.但是现在有一些关于存储库设计的实际问题.考虑以下示例.
<?php
class DbUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct($db)
{
$this->db = $db;
}
public function findAll()
{
}
public function findById($id)
{
}
public function findByName($name)
{
}
public function create($user)
{
}
public function remove($user)
{
}
public function update($user)
{
}
}
Run Code Online (Sandbox Code Playgroud)
所有这些find方法都使用select all fields(SELECT *)方法.但是,在我的应用程序中,我总是试图限制我获得的字段数量,因为这通常会增加开销并减慢速度.对于那些使用这种模式的人,你如何处理这个?
虽然这个类现在看起来不错,但我知道在现实世界的应用程序中我需要更多的方法.例如:
如您所见,可能存在非常长的可能方法列表.然后,如果您添加上面的字段选择问题,问题就会恶化.在过去,我通常只是将所有这些逻辑放在我的控制器中:
<?php
class MyController
{
public function users()
{
$users = User::select('name, email, status')
->byCountry('Canada')->orderBy('name')->rows();
return View::make('users', array('users' => $users));
}
}
Run Code Online (Sandbox Code Playgroud)
使用我的存储库方法,我不想最终得到这个:
<?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))
}
}
Run Code Online (Sandbox Code Playgroud)
我看到使用存储库接口的好处,所以我可以换出我的实现(用于测试目的或其他).我对接口的理解是它们定义了一个实现必须遵循的契约.这很棒,直到你开始向你的存储库添加其他方法findAllInCountry().现在我需要更新我的界面也有这个方法,否则,其他实现可能没有它,这可能会破坏我的应用程序.这感觉很疯狂......一个尾巴摇着狗的情况.
这使我相信,库应该只有方法固定数量(如save(),remove(),find(),findAll(),等).但是,我如何运行特定的查找?我听说过规范模式,但在我看来,这只会减少整个记录集(通过IsSatisfiedBy()),如果您从数据库中提取数据,这显然会产生严重的性能问题.
显然,在使用存储库时,我需要重新思考一些事情.任何人都可以启发如何最好地处理这个问题?
Jon*_*han 196
我以为我会回答我自己的问题.以下是我原始问题中解决问题1-3的一种方法.
免责声明:在描述模式或技术时,我可能并不总是使用正确的术语.对不起.
Users.我将持久存储(数据库)交互分为两类:R(读取)和CUD(创建,更新,删除).我的经验是读取确实是导致应用程序变慢的原因.虽然数据操作(CUD)实际上较慢,但它的发生频率要低得多,因此不太重要.
CUD(创建,更新,删除)很简单.这将涉及使用实际模型,然后将其传递给我Repositories的持久性.请注意,我的存储库仍将提供Read方法,但仅用于创建对象,而不是显示.稍后会详细介绍.
R(阅读)并不那么容易.这里没有模型,只是重视对象.如果您愿意,请使用数组.这些对象可能代表单个模型或许多模型的混合,实际上是任何东西.这些并不是很有趣,但它们是如何生成的.我正在使用我正在呼唤的东西Query Objects.
让我们从基本的用户模型开始.请注意,根本没有ORM扩展或数据库内容.只是纯粹的模特荣耀.添加你的getter,setter,验证等等.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Run Code Online (Sandbox Code Playgroud)
在创建用户存储库之前,我想创建我的存储库接口.这将定义存储库必须遵循的"合同",以便我的控制器使用.请记住,我的控制器不知道数据的实际存储位置.
请注意,我的存储库只会包含这三种方法.该save()方法负责创建和更新用户,这取决于用户对象是否具有id集.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Run Code Online (Sandbox Code Playgroud)
现在创建我的接口实现.如上所述,我的例子将是一个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');
}
}
Run Code Online (Sandbox Code Playgroud)
现在我们的存储库处理了CUD(创建,更新,删除),我们可以专注于R(读取).查询对象只是某种类型的数据查找逻辑的封装.它们不是查询构建器.通过像我们的存储库那样抽象它,我们可以更改它的实现并更容易地测试它.查询对象的例子可能是一个AllUsersQuery或AllActiveUsersQuery,甚至MostCommonUserFirstNames.
您可能会想"我不能只在我的存储库中为这些查询创建方法吗?" 是的,但这就是为什么我不这样做:
password如果我想列出所有用户,为什么我需要获得该字段?对于我的例子,我将创建一个查询对象来查找"AllUsers".这是界面:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Run Code Online (Sandbox Code Playgroud)
这是我们可以再次使用数据映射器来帮助加快开发的地方.请注意,我允许对返回的数据集进行一次调整 - 字段.这就是我想要操纵执行的查询.请记住,我的查询对象不是查询构建器.他们只是执行特定的查询.但是,因为我知道我可能会经常使用这个,在许多不同的情况下,我给自己指定字段的能力.我永远不想回到我不需要的领域!
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();
}
}
Run Code Online (Sandbox Code Playgroud)
在继续讨论控制器之前,我想展示另一个例子来说明这是多么强大.也许我有一个报告引擎,需要为其创建报告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...";
}
}
Run Code Online (Sandbox Code Playgroud)
这很好地将我的所有逻辑保存在一个类中,并且很容易测试.我可以嘲笑我的内容,甚至完全使用不同的实现.
现在是有趣的部分 - 将所有部分组合在一起.请注意,我正在使用依赖注入.通常,依赖项被注入到构造函数中,但实际上我更喜欢将它们注入到我的控制器方法(路由)中.这最小化了控制器的对象图,我实际上发现它更清晰.注意,如果您不喜欢这种方法,只需使用传统的构造方法.
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;
}
}
Run Code Online (Sandbox Code Playgroud)
这里要注意的重要事项是,当我修改(创建,更新或删除)实体时,我正在使用真实的模型对象,并通过我的存储库执行持久性.
但是,当我显示(选择数据并将其发送到视图)时,我不使用模型对象,而是使用普通的旧值对象.我只选择了我需要的字段,并且它的设计使我可以最大化我的数据查找性能.
我的存储库保持非常干净,而这个"混乱"被组织到我的模型查询中.
我使用数据映射器来帮助开发,因为为常见任务编写重复的SQL是非常荒谬的.但是,您绝对可以在需要的地方编写SQL(复杂的查询,报告等).当你这样做时,它很好地隐藏在一个正确命名的类中.
我很想听听你对我的态度的看法!
2015年7月更新:
我在评论中被问到我最终得到了这一切.嗯,实际上并没有那么远.说实话,我仍然不喜欢存储库.我觉得它们对于基本查找来说有点过分(特别是如果你已经在使用ORM),并且在处理更复杂的查询时会很麻烦.
我通常使用ActiveRecord样式的ORM,所以我经常在整个应用程序中直接引用这些模型.但是,在我有更复杂的查询的情况下,我将使用查询对象使这些更可重用.我还应该注意,我总是将我的模型注入到我的方法中,使我们在测试中更容易模拟.
rya*_*234 48
根据我的经验,以下是您的问题的一些答案:
问:我们如何处理带回我们不需要的字段?
答:根据我的经验,这实际上归结为处理完整实体与临时查询.
一个完整的实体就像一个User对象.它具有属性和方法等.它是您的代码库中的一等公民.
ad-hoc查询返回一些数据,但除此之外我们不知道任何事情.当数据在应用程序中传递时,它就是在没有上下文的情况下完成的.是一个User吗?A User附有一些Order信息?我们真的不知道.
我更喜欢使用完整的实体.
你是对的,你经常会带回你不会使用的数据,但你可以通过各种方式解决这个问题:
User用于后端,也可以UserSmall用于AJAX调用.一个可能有10个属性,一个有3个属性.使用即席查询的缺点:
User,您最终会select *为许多调用编写基本相同的内容.一个电话将获得10个字段中的8个,一个将获得10个中的5个,一个将获得7个中的7个.为什么不用10个中的10个中的一个调用替换所有字段?这很糟糕的原因是重新分解/测试/模拟是谋杀.User这么慢?"这样的陈述.您最终会追踪一次性查询,因此错误修复往往很小并且本地化.问:我的存储库中有太多方法.
答:除了整合电话,我还没有真正看到过这方面的任何方式.存储库中的方法调用实际上映射到应用程序中的功能.功能越多,数据特定的调用越多.您可以推回功能并尝试将类似的呼叫合并为一个.
一天结束时的复杂性必须存在于某个地方.使用存储库模式,我们已将其推送到存储库界面,而不是制作一堆存储过程.
有时候我必须告诉自己,"好吧,它必须放在某个地方!没有银子弹."
Con*_*enu 18
我使用以下接口:
Repository - 加载,插入,更新和删除实体Selector - 在存储库中查找基于过滤器的实体Filter - 封装过滤逻辑我Repository的数据库不可知; 实际上它没有指定任何持久性; 它可以是任何东西:SQL数据库,xml文件,远程服务,来自外太空的外星人等.对于搜索功能,可以过滤,编码,排序和计算的Repository构造.最后,选择器从持久性中提取一个或多个.SelectorLIMITEntities
以下是一些示例代码:
<?php
interface Repository
{
public function addEntity(Entity $entity);
public function updateEntity(Entity $entity);
public function removeEntity(Entity $entity);
/**
* @return Entity
*/
public function loadEntity($entityId);
public function factoryEntitySelector():Selector
}
interface Selector extends \Countable
{
public function count();
/**
* @return Entity[]
*/
public function fetchEntities();
/**
* @return Entity
*/
public function fetchEntity();
public function limit(...$limit);
public function filter(Filter $filter);
public function orderBy($column, $ascending = true);
public function removeFilter($filterName);
}
interface Filter
{
public function getFilterName();
}
Run Code Online (Sandbox Code Playgroud)
然后,一个实现:
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);
}
}
Run Code Online (Sandbox Code Playgroud)
ideea是泛型Selector用途,Filter但实现SqlSelector使用SqlFilter; 使SqlSelectorFilterAdapter通用适应Filter具体SqlFilter.
客户端代码创建Filter对象(即通用过滤器),但在选择器的具体实现中,这些过滤器在SQL过滤器中进行转换.
其他选择器实现,比如InMemorySelector,转换Filter为InMemoryFilter使用它们的特定InMemorySelectorFilterAdapter; 所以,每个选择器实现都有自己的过滤器适配器.
使用此策略,我的客户端代码(在bussines层中)不关心特定的存储库或选择器实现.
/** @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();
Run Code Online (Sandbox Code Playgroud)
PS这是我的真实代码的简化
我将在此方面加一点,因为我目前正试图自己掌握所有这些。
这是您的ORM进行繁重工作的理想场所。如果您使用的是实现某种ORM的模型,则可以使用它的方法来处理这些事情。根据需要制作实现Eloquent方法的orderBy函数。例如,使用Eloquent:
class DbUserRepository implements UserRepositoryInterface
{
public function findAll()
{
return User::all();
}
public function get(Array $columns)
{
return User::select($columns);
}
Run Code Online (Sandbox Code Playgroud)
您似乎正在寻找的是ORM。没有理由您的存储库不能基于一个。这需要用户扩展才能,但我个人认为这不是问题。
但是,如果您确实想避免使用ORM,则必须“自己动手”以获取所需的内容。
接口不是硬性要求和快速要求。可以实现某个接口并将其添加到其中。它不能做的是无法实现该接口的必需功能。您还可以扩展类之类的接口,以保持事物干燥。
就是说,我才刚刚开始了解,但是这些认识帮助了我。
| 归档时间: |
|
| 查看次数: |
60842 次 |
| 最近记录: |