Правильный шаблон шаблона репозитория в PHP?

Предисловие: я использую шаблон репозитория в архитектуре MVC с реляционными базами данных.

Недавно я начал изучать TDD в PHP, и я понимаю, что моя база данных слишком тесно связана с остальной частью моего приложения. Я прочитал о репозиториях и с помощью контейнера 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) { } } 

Проблема №1: Слишком много полей

Все эти методы поиска используют подход select all fields ( SELECT * ). Однако в моих приложениях я всегда стараюсь ограничить количество полей, которые я получаю, так как это часто увеличивает накладные расходы и замедляет работу. Для тех, кто использует этот шаблон, как вы справляетесь с этим?

Проблема №2: Слишком много методов

Пока этот класс выглядит хорошо сейчас, я знаю, что в приложении реального мира мне нужно гораздо больше методов. Например:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • И т.п.

Как вы можете видеть, может быть очень, очень длинный список возможных методов. И тогда, если вы добавите проблему выбора поля выше, проблема ухудшится. Раньше я обычно просто использовал всю эту логику в своем контроллере:

 <?php class MyController { public function users() { $users = User::select('name, email, status')->byCountry('Canada')->orderBy('name')->rows() return View::make('users', array('users' => $users)) } } 

С моим подходом к хранилищу я не хочу этого делать:

 <?php class MyController { public function users() { $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada'); return View::make('users', array('users' => $users)) } } 

Проблема №3: ​​Невозможно сопоставить интерфейс

Я вижу преимущество в использовании интерфейсов для репозиториев, поэтому я могу поменять свою реализацию (для целей тестирования или другие). Мое понимание интерфейсов заключается в том, что они определяют контракт, которому должна следовать реализация. Это здорово, пока вы не начнете добавлять дополнительные методы в свои репозитории, такие как findAllInCountry() . Теперь мне нужно обновить свой интерфейс, чтобы иметь этот метод, иначе другие реализации могут его не иметь, и это может сломать мое приложение. Этим кажется сумасшедшим … случай хвоста виляет собакой.

Спецификация?

Это заставляет меня думать, что в репозитории должно быть только фиксированное количество методов (например, save() , remove() , find() , findAll() и т. Д.). Но как я могу запустить определенные поиски? Я слышал о IsSatisfiedBy() спецификации , но мне кажется, что это уменьшает весь набор записей (через IsSatisfiedBy() ), что явно имеет серьезные проблемы с производительностью, если вы извлекаете из базы данных.

Помогите?

Очевидно, мне нужно немного переосмыслить ситуацию при работе с репозиториями. Может ли кто-нибудь просветить, как это лучше всего обрабатывать?

Solutions Collecting From Web of "Правильный шаблон шаблона репозитория в PHP?"

Я подумал, что я взломаю ответ на свой вопрос. Далее следует лишь один из способов решения вопросов 1-3 в моем первоначальном вопросе.

Отказ от ответственности: я не всегда могу использовать правильные условия при описании шаблонов или методов. Простите за это.

Цели:

  • Создайте полный пример базового контроллера для просмотра и редактирования Users .
  • Весь код должен быть полностью проверен и макетироваться.
  • Контроллер не должен знать, где хранятся данные (что означает его изменение).
  • Пример, показывающий реализацию SQL (наиболее распространенный).
  • Для максимальной производительности контроллеры должны получать только нужные им данные – никаких дополнительных полей.
  • Реализация должна использовать некоторый тип преобразователя данных для простоты разработки.
  • Реализация должна иметь возможность выполнять сложные поиски данных.

Решение

Я разделяю взаимодействие с постоянным хранилищем (базой данных) на две категории: R (Read) и CUD (Create, Update, Delete). Мой опыт в том, что чтение действительно приводит к замедлению приложения. И хотя манипуляции с данными (CUD) на самом деле медленнее, это происходит гораздо реже, и поэтому гораздо меньше беспокоит.

CUD (Создать, обновить, удалить) легко. Это будет связано с работой с фактическими моделями , которые затем передаются в мои Repositories для сохранения. Обратите внимание: мои репозитории по-прежнему будут предоставлять метод Read, но просто для создания объекта, а не для отображения. Об этом позже.

R (Read) не так просто. Здесь нет моделей, просто оценивайте объекты . Если хотите, используйте массивы. Эти объекты могут представлять собой единую модель или смесь многих моделей, что угодно. Они не очень интересны сами по себе, но как они созданы. Я использую то, что я называю Query Objects .

Код:

Модель пользователя

Давайте начнем просто с нашей базовой модели пользователя. Обратите внимание, что нет ORM-расширения или базы данных вообще. Просто чистая модель славы. Добавьте своих геттеров, сеттеров, валидацию, что угодно.

 class User { public $id; public $first_name; public $last_name; public $gender; public $email; public $password; } 

Интерфейс репозитория

Прежде чем создать свой пользовательский репозиторий, я хочу создать свой интерфейс репозитория. Это определит «контракт», который должны использовать репозитории для использования моим контроллером. Помните, что мой контроллер не знает, где хранятся данные.

Обратите внимание, что мои хранилища будут содержать только три этих метода. Метод save() отвечает за создание и обновление пользователей, просто в зависимости от того, установлен ли идентификатор пользователя.

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

Реализация хранилища SQL

Теперь для создания моей реализации интерфейса. Как уже упоминалось, мой пример будет состоять из базы данных 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'); } } 

Интерфейс объекта запроса

Теперь, когда CUD (Create, Update, Delete) позаботится о нашем репозитории, мы можем сосредоточиться на R (Read). Объекты запроса – это просто инкапсуляция некоторой логики поиска данных. Они не являются сборщиками запросов. Абстрагируя его, как наш репозиторий, мы можем изменить его реализацию и протестировать его проще. Примером объекта запроса может быть AllUsersQuery или AllActiveUsersQuery или даже MostCommonUserFirstNames .

Возможно, вы думаете: «Не могу ли я просто создавать методы в своих репозиториях для этих запросов?» Да, но вот почему я этого не делаю:

  • Мои репозитории предназначены для работы с объектами модели. В приложении реального мира, почему мне нужно было бы получить поле password если я ищу список всех моих пользователей?
  • Хранилища часто зависят от модели, но запросы часто включают более одной модели. Итак, какой репозиторий вы вкладываете в свой метод?
  • Это делает мои репозитории очень простыми – не раздутый класс методов.
  • Все запросы теперь организованы в свои классы.
  • Действительно, на данный момент репозитории существуют просто для абстрагирования моего уровня базы данных.

В моем примере я создам объект запроса для поиска «AllUsers». Вот интерфейс:

 interface AllUsersQueryInterface { public function fetch($fields); } 

Реализация объекта запроса

Здесь мы снова можем использовать устройство отображения данных, чтобы ускорить разработку. Обратите внимание, что я разрешаю одну настройку возвращаемого набора данных – поля. Это касается, насколько я хочу, манипулировать выполненным запросом. Помните, что мои объекты запроса не являются сборщиками запросов. Они просто выполняют конкретный запрос. Однако, поскольку я знаю, что я, вероятно, буду использовать этот много, в нескольких разных ситуациях, я даю себе возможность указывать поля. Я никогда не хочу возвращать поля, которые мне не нужны!

 class AllUsersQuery implements AllUsersQueryInterface { protected $db; public function __construct(Database $db) { $this->db = $db; } public function fetch($fields) { return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows(); } } 

Прежде чем перейти к контроллеру, я хочу показать еще один пример, чтобы проиллюстрировать, насколько это мощно. Возможно, у меня есть механизм отчетности и вам нужно создать отчет для AllOverdueAccounts . Это может быть сложно с моим картотекой данных, и я могу захотеть написать некоторый фактический SQL в этой ситуации. Нет проблем, вот как выглядит этот объект запроса:

 class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface { protected $db; public function __construct(Database $db) { $this->db = $db; } public function fetch() { return $this->db->query($this->sql())->rows(); } public function sql() { return "SELECT..."; } } 

Это прекрасно сохраняет мою логику для этого отчета в одном классе, и его легко проверить. Я могу издеваться над этим до глубины души или даже полностью использовать другую реализацию.

Контроллер

Теперь самое интересное – собрать все части вместе. Обратите внимание, что я использую инъекцию зависимостей. Обычно зависимости встраиваются в конструктор, но я фактически предпочитаю вводить их прямо в мои методы (маршруты) контроллера. Это минимизирует граф объекта контроллера, и я на самом деле считаю его более четким. Обратите внимание: если вам не нравится этот подход, просто используйте традиционный метод конструктора.

 class UsersController { public function index(AllUsersQueryInterface $query) { // Fetch user data $users = $query->fetch(['first_name', 'last_name', 'email']); // Return view return Response::view('all_users.php', ['users' => $users]); } public function add() { return Response::view('add_user.php'); } public function insert(UserRepositoryInterface $repository) { // Create new user model $user = new User; $user->first_name = $_POST['first_name']; $user->last_name = $_POST['last_name']; $user->gender = $_POST['gender']; $user->email = $_POST['email']; // Save the new user $repository->save($user); // Return the id return Response::json(['id' => $user->id]); } public function view(SpecificUserQueryInterface $query, $id) { // Load user data if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) { return Response::notFound(); } // Return view return Response::view('view_user.php', ['user' => $user]); } public function edit(SpecificUserQueryInterface $query, $id) { // Load user data if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) { return Response::notFound(); } // Return view return Response::view('edit_user.php', ['user' => $user]); } public function update(UserRepositoryInterface $repository) { // Load user model if (!$user = $repository->find($id)) { return Response::notFound(); } // Update the user $user->first_name = $_POST['first_name']; $user->last_name = $_POST['last_name']; $user->gender = $_POST['gender']; $user->email = $_POST['email']; // Save the user $repository->save($user); // Return success return true; } public function delete(UserRepositoryInterface $repository) { // Load user model if (!$user = $repository->find($id)) { return Response::notFound(); } // Delete the user $repository->delete($user); // Return success return true; } } 

Последние мысли:

Здесь важно отметить, что когда я изменяю (создавая, обновляю и удаляя) объекты, я работаю с реальными объектами модели и выполняю упорство через свои репозитории.

Однако, когда я показываю (выбирая данные и отправляя их в представления), я не работаю с объектами модели, а довольно простыми старыми объектами значений. Я выбираю только те поля, которые мне нужны, и он сконструирован таким образом, что я могу максимально повысить производительность поиска данных.

Мои репозитории остаются очень чистыми, и вместо этого этот «беспорядок» организован в мои модельные запросы.

Я использую карту данных, чтобы помочь с разработкой, так как просто смешно писать повторяющийся SQL для общих задач. Однако вы абсолютно можете написать SQL, где это необходимо (сложные запросы, отчеты и т. Д.). И когда вы это делаете, он отлично убирается в правильно названный класс.

Хотелось бы услышать, как вы принимаете мой подход!


Июль 2015 г. Обновление:

Меня спрашивали в комментариях, где я закончил все это. Ну, не так уж и далеко. Честно говоря, мне все еще не нравятся репозитории. Я нахожу их излишними для базового поиска (особенно если вы уже используете ORM) и беспорядочно работаете с более сложными запросами.

Обычно я работаю с ORM ActiveRecord, поэтому чаще всего я просто ссылаюсь на эти модели непосредственно во всем приложении. Однако в ситуациях, когда у меня есть более сложные запросы, я буду использовать объекты запроса, чтобы сделать их более многоразовыми. Я должен также отметить, что я всегда добавляю свои модели в свои методы, что делает их более легкими для насмешек в моих тестах.

На основании моего опыта, вот несколько ответов на ваши вопросы:

Вопрос: Как мы имеем дело с возвратом полей, которые нам не нужны?

Ответ: По моему опыту это действительно сводится к работе с полными сущностями по сравнению с специальными запросами.

Полная сущность – это нечто вроде объекта User . Он имеет свойства и методы и т. Д. Это ваш первый класс в вашей кодовой базе.

Специальный запрос возвращает некоторые данные, но мы ничего не знаем. Поскольку данные передаются вокруг приложения, это делается без контекста. Это User ? User с информацией о заказе? Мы действительно не знаем.

Я предпочитаю работать с полными сущностями.

Вы правы, что часто будете возвращать данные, которые не будете использовать, но вы можете решить это различными способами:

  1. Агрессивно кэшируйте объекты, чтобы вы только платили цену за чтение один раз из базы данных.
  2. Потратьте больше времени на моделирование своих объектов, чтобы у них были хорошие различия между ними. (Рассмотрим расщепление большой сущности на две младшие сущности и т. Д.)
  3. Рассмотрим наличие нескольких версий объектов. У вас может быть User для задней части и, возможно, UserSmall для вызовов AJAX. Один из них может иметь 10 свойств и один имеет 3 свойства.

Недостатки работы со специальными запросами:

  1. Вы получаете по существу одни и те же данные по многим запросам. Например, с User вы в конечном итоге напишите по существу тот же самый select * для многих вызовов. Один звонок получит 8 из 10 полей, один получит 5 из 10, один получит 7 из 10. Почему бы не заменить все одним вызовом, который получает 10 из 10? Причина, по которой это плохо, заключается в том, что убийство ре-фактор / испытание / макет.
  2. С течением времени очень сложно рассуждать о высоком уровне вашего кода. Вместо таких утверждений, как «Почему User так медленно?» вы в конечном итоге отслеживаете одноразовые запросы, и поэтому исправления ошибок имеют тенденцию быть небольшими и локализованными.
  3. Трудно заменить базовую технологию. Если вы теперь сохранили все в MySQL и хотите перейти на MongoDB, намного сложнее заменить 100 специальных вызовов, чем несколько сущностей.

В: В моем репозитории у меня будет слишком много методов.

Ответ: Я не видел ничего подобного, кроме консолидации звонков. Метод, вызываемый в вашем репозитории, действительно соответствует функциям вашего приложения. Чем больше функций, тем больше вызовов, связанных с конкретными данными. Вы можете вернуться к функциям и попытаться объединить похожие вызовы в один.

Сложность в конце дня должна где-то существовать. С шаблоном репозитория мы переместили его в интерфейс репозитория, а не создавали кучу хранимых процедур.

Иногда я должен сказать себе: «Ну, это должно было дать где-то! Нет серебряных пуль».

Я использую следующие интерфейсы:

  • Repository – загружает, вставляет, обновляет и удаляет объекты
  • Selector – находит объекты на основе фильтров, в репозитории
  • Filter – инкапсулирует логику фильтрации

Мой Repository – агностик базы данных; на самом деле он не указывает на постоянство; это может быть что угодно: база данных SQL, xml-файл, удаленный сервис, инопланетянин из космоса и т. д. Для возможностей поиска Repository создает Selector который может быть отфильтрован, LIMIT -ed, отсортирован и подсчитан. В конце селектор выбирает одну или несколько Entities из персистентности.

Вот пример кода:

 <?php interface Repository { public function addEntity(Entity $entity); public function updateEntity(Entity $entity); public function removeEntity(Entity $entity); /** * @return Entity */ public function loadEntity($entityId); public function factoryEntitySelector():Selector } interface Selector extends \Countable { public function count(); /** * @return Entity[] */ public function fetchEntities(); /** * @return Entity */ public function fetchEntity(); public function limit(...$limit); public function filter(Filter $filter); public function orderBy($column, $ascending = true); public function removeFilter($filterName); } interface Filter { public function getFilterName(); } 

Затем одна реализация:

 class SqlEntityRepository { ... public function factoryEntitySelector() { return new SqlSelector($this); } ... } class SqlSelector implements Selector { ... private function adaptFilter(Filter $filter):SqlQueryFilter { return (new SqlSelectorFilterAdapter())->adaptFilter($filter); } ... } class SqlSelectorFilterAdapter { public function adaptFilter(Filter $filter):SqlQueryFilter { $concreteClass = (new StringRebaser( 'Filter\\', 'SqlQueryFilter\\')) ->rebase(get_class($filter)); return new $concreteClass($filter); } } 

Идея заключается в том, что общий Selector использует Filter но реализация SqlSelector использует 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(); 

PS Это упрощение моего реального кода

Я могу только прокомментировать то, как мы (в моей компании) справляемся с этим. Прежде всего, производительность для нас не слишком важна, но имеет чистый / правильный код.

Прежде всего мы определяем Модели, такие как UserModel который использует ORM для создания объектов UserEntity . Когда 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 содержит сопоставление и использует это для ввода всех полей для этого объекта в объект. Однако для иностранных полей загружаются только идентификаторы этих объектов. В этом случае OrderModel создает OrderEntity s только с идентификаторами ссылочных заказов. Когда PersistentEntity::getField OrderEntity сущность дает команду, чтобы модель PersistentEntity::getField загружать все поля в OrderEntity s. Все OrderEntity связанные с одним UserEntity, рассматриваются как один набор результатов и будут загружены сразу.

Магия здесь заключается в том, что наша модель и ORM вводят все данные в сущности и что объекты просто предоставляют функции-обертки для общего метода getField предоставленного PersistentEntity . Подводя итог, мы всегда загружаем все поля, но при необходимости загружаются поля, ссылающиеся на внешний объект. Просто загрузка кучи полей на самом деле не является проблемой производительности. Загрузите все возможные внешние объекты, но это будет ОГРОМНОЕ снижение производительности.

Теперь загрузите определенный набор пользователей на основе предложения where. Мы предоставляем объектно-ориентированный пакет классов, который позволяет вам указать простое выражение, которое можно склеить. В примере кода я назвал его GetOptions . Это оболочка для всех возможных вариантов выбора запроса. Он содержит коллекцию предложений, клаузула и всего остального. Наши предложения довольно сложны, но вы, очевидно, можете упростить версию.

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

Простейшей версией этой системы было бы передать часть запроса WHERE в виде строки непосредственно в модель.

Прошу прощения за этот довольно сложный ответ. Я постарался как можно скорее и четко изложить нашу структуру. Если у вас есть дополнительные вопросы, не стесняйтесь спрашивать их, и я обновлю свой ответ.

EDIT: Кроме того, если вы действительно не хотите сразу загружать некоторые поля, вы можете указать параметр ленивой загрузки в вашем ORM-сопоставлении. Поскольку все поля в конечном итоге загружаются через метод getField вы можете загрузить некоторые поля в последнюю минуту при вызове этого метода. Это не очень большая проблема в PHP, но я бы не рекомендовал для других систем.

Это несколько разных решений, которые я видел. Для каждого из них есть плюсы и минусы, но вам решать.

Проблема №1: Слишком много полей

Это важный аспект, особенно когда вы используете учетные записи только для индексирования . Я вижу два решения для решения этой проблемы. Вы можете обновить свои функции, чтобы взять необязательный параметр массива, который будет содержать список возвращаемых столбцов. Если этот параметр пуст, вы должны вернуть все столбцы в запросе. Это может быть немного странно; основанный на параметре, который вы могли бы получить для объекта или массива. Вы также можете дублировать все свои функции, чтобы иметь две различные функции, которые запускают один и тот же запрос, но один возвращает массив столбцов, а другой возвращает объект.

 public function findColumnsById($id, array $columns = array()){ if (empty($columns)) { // use * } } public function findById($id) { $data = $this->findColumnsById($id); } 

Проблема №2: Слишком много методов

Я кратко работал с Propel ORM год назад, и это основано на том, что я помню из этого опыта. Propel имеет возможность генерировать свою структуру класса, основанную на существующей схеме базы данных. Он создает два объекта для каждой таблицы. Первый объект – это длинный список функций доступа, аналогичный тому, что вы сейчас перечисляете; findByAttribute($attribute_value) . Следующий объект наследуется от этого первого объекта. Вы можете обновить этот дочерний объект для создания более сложных функций getter.

Другим решением будет использование __call() для сопоставления не определенных функций с чем-то действующим. Ваш метод __call будет способен анализировать findById и findByName в разные запросы.

 public function __call($function, $arguments) { if (strpos($function, 'findBy') === 0) { $parameter = substr($function, 6, strlen($function)); // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0] } } 

Надеюсь, это поможет хотя бы кое-чему.

Я добавлю немного об этом, поскольку в настоящее время я пытаюсь понять все это сам.

№ 1 и 2

Это идеальное место для вашего ОРМ для тяжелого подъема. Если вы используете модель, которая реализует какой-то ORM, вы можете просто использовать ее методы, чтобы позаботиться об этих вещах. Создайте собственные функции OrderBy, которые реализуют методы Eloquent, если вам нужно. Например, использование Eloquent:

 class DbUserRepository implements UserRepositoryInterface { public function findAll() { return User::all(); } public function get(Array $columns) { return User::select($columns); } 

Кажется, что вы ищете ORM. Нет причин, по которым ваш репозиторий не может быть основан на одном. Это потребует от пользователя красноречия, но я лично не вижу в этом проблемы.

Если вы все же хотите избежать ORM, вам тогда придется «сворачивать», чтобы получить то, что вы ищете.

# 3

Интерфейсы не должны быть жесткими и быстрыми требованиями. Что-то может реализовать интерфейс и добавить к нему. То, что он не может сделать, – это не реализовать требуемую функцию этого интерфейса. Вы также можете расширять интерфейсы, такие как классы, чтобы держать вещи DRY.

Тем не менее, я только начинаю понимать, но эти реализации помогли мне.