PHP中正确的存储库模式设计?
前言:我试图在MVC体系结构中使用关系数据库的存储库模式 我最近开始学习PHP中的TDD,我意识到我的数据库与应用程序的其余部分耦合得太紧密了。我读过关于存储库和使用“注入”到控制器的文章。很酷的东西。但是现在有一些关于存储库设计的实际问题。考虑下面的例子。PHP中正确的存储库模式设计?,php,database,laravel,repository,repository-pattern,Php,Database,Laravel,Repository,Repository Pattern,前言:我试图在MVC体系结构中使用关系数据库的存储库模式 我最近开始学习PHP中的TDD,我意识到我的数据库与应用程序的其余部分耦合得太紧密了。我读过关于存储库和使用“注入”到控制器的文章。很酷的东西。但是现在有一些关于存储库设计的实际问题。考虑下面的例子。 根据我的经验,以下是对您的问题的一些回答: Q:我们如何处理带回不需要的字段 A:根据我的经验,这可以归结为处理完整实体而不是临时查询 完整的实体类似于用户对象。它具有属性和方法等。它是代码库中的一级公民 一个特殊的查询返回一些数据,但我们
根据我的经验,以下是对您的问题的一些回答:
Q:我们如何处理带回不需要的字段
A:根据我的经验,这可以归结为处理完整实体而不是临时查询
完整的实体类似于用户
对象。它具有属性和方法等。它是代码库中的一级公民
一个特殊的查询返回一些数据,但我们不知道除此之外的任何事情。当数据在应用程序中传递时,它是在没有上下文的情况下完成的。它是用户
?附加了一些订单
信息的用户
?我们真的不知道
我更喜欢使用完整的实体
您是对的,您经常会带回不使用的数据,但您可以通过多种方式解决这一问题:
主动缓存实体,这样您只需支付一次从数据库读取的费用
花更多的时间建模实体,使它们之间有很好的区别。(考虑将一个大型实体拆分为两个较小的实体等)
考虑拥有实体的多个版本。后端可以有一个User
,AJAX调用可以有一个UserSmall
。一个可能有10个属性,一个可能有3个属性
使用临时查询的缺点是:
在许多查询中,最终得到的数据基本相同。例如,对于一个用户
,您将为许多调用编写基本相同的select*
。一个呼叫将得到10个字段中的8个,一个将得到10个字段中的5个,一个将得到10个字段中的7个。为什么不把所有的电话都换成10个电话中的10个呢?这不好的原因是,重新考虑/测试/模拟是谋杀
随着时间的推移,很难对代码进行高层次的推理。而不是像“为什么<代码>用户代码>这么慢?”这样的陈述,你最终会跟踪一次性的查询,因此错误修复往往是小的和本地化的李>
很难替换底层技术。如果您现在将所有内容都存储在MySQL中,并希望迁移到MongoDB,那么替换100个临时调用要比替换少数实体困难得多
Q:我的存储库中将有太多方法
A:除了整合通话之外,我还没有找到其他解决方法。存储库中的方法调用实际上映射到应用程序中的功能。功能越多,特定于数据的调用就越多。您可以推回功能并尝试将类似的调用合并为一个
归根结底,复杂性肯定存在于某个地方。使用存储库模式,我们将其推入存储库界面,而不是制作一堆存储过程
有时我不得不告诉自己,“好吧,它必须有所回报!没有银弹。”我只能评论我们(在我的公司)处理这件事的方式。首先,性能对我们来说不是太大的问题,但是拥有干净/正确的代码是非常重要的
首先,我们定义了一些模型,比如使用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
s。当OrderEntity
调用PersistentEntity::getField
时,该实体指示其模型将所有字段延迟加载到OrderEntity
中。与一个UserEntity关联的所有OrderEntity
s将被视为一个结果集,并将立即加载
这里的神奇之处在于,我们的模型和ORM将所有数据注入到实体中,实体只为persistenentity
提供的通用getField
方法提供包装函数。总之,我们总是加载所有字段,但在必要时加载引用外部实体的字段。仅仅加载一堆字段并不是真正的性能问题。然而,加载所有可能的外部实体将导致性能大幅下降
现在,根据where子句加载一组特定的用户。我们提供了一个面向对象的类包,允许您指定可以粘合在一起的简单表达式。在示例代码中,我将其命名为GetOptions
。它是select查询所有可能选项的包装器。它包含where子句、group by子句和其他所有内容的集合。我们的where子句相当复杂,但您显然可以轻松地制作一个更简单的版本
$objOptions->getConditionHolder()->addConditionBind(
new ConditionBind(
new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
)
);
该系统的最简单版本是将查询的WHERE部分作为字符串直接传递给模型
我很抱歉这个相当复杂的回答。我试图尽可能快而清晰地总结我们的框架。如果你
public function findColumnsById($id, array $columns = array()){
if (empty($columns)) {
// use *
}
}
public function findById($id) {
$data = $this->findColumnsById($id);
}
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]
}
}
class DbUserRepository implements UserRepositoryInterface
{
public function findAll()
{
return User::all();
}
public function get(Array $columns)
{
return User::select($columns);
}
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
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');
}
}
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();
}
}
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;
}
}
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);
}
}
/** @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();
Model::where(['attr1' => 'val1'])->get();
POST /api/graphql
{
query: {
Model(attr1: 'val1') {
attr2
attr3
}
}
}
<?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);
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();