Я подумал, что смогу ответить на свой вопрос. Далее следует лишь один из способов решения проблем 1-3 в моем первоначальном вопросе.
Отказ от ответственности: я не всегда могу использовать правильные термины при описании моделей или методов. Простите за это.
Цели:
- Создайте полный пример базового контроллера для просмотра и редактирования
Users
.
- Весь код должен быть полностью тестируемым и проверяемым.
- Контроллер не должен знать, где хранятся данные (то есть их можно изменить).
- Пример, показывающий реализацию SQL (наиболее распространенная).
- Для максимальной производительности контроллеры должны получать только те данные, которые им нужны, без дополнительных полей.
- Реализация должна использовать некоторые типы картографических данных для простоты разработки.
- Реализация должна иметь возможность выполнять сложные поиски данных.
Решение
Я делю свое взаимодействие с постоянным хранилищем (базой данных) на две категории: R (чтение) и CUD (создание, обновление, удаление). Мой опыт показывает, что чтение действительно вызывает замедление работы приложения. И хотя манипулирование данными (CUD) на самом деле медленнее, это происходит гораздо реже, и, следовательно, гораздо меньше проблем.
CUD (создать, обновить, удалить) легко. Это будет связано с работой с реальными моделями , которые затем будут переданы мне Repositories
для настойчивости. Обратите внимание, что мои репозитории будут по-прежнему предоставлять метод Read, но просто для создания объекта, а не для отображения. Подробнее об этом позже.
R (Читать) не так просто. Здесь нет моделей, только объекты стоимости . Используйте массивы, если хотите . Эти объекты могут представлять одну модель или смесь множества моделей, чего угодно. Они не очень интересны сами по себе, но как они создаются. Я использую то, что я звоню 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');
}
}
Интерфейс Query Object
Теперь, когда 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, поэтому чаще всего я просто ссылаюсь на эти модели непосредственно во всем приложении. Однако в ситуациях, когда у меня есть более сложные запросы, я буду использовать объекты запросов, чтобы сделать их более пригодными для повторного использования. Я также должен отметить, что я всегда внедряю свои модели в свои методы, что облегчает имитацию в моих тестах.
new Query\ComplexUserLookup($username, $anotherCondition)
. Или сделать это с помощью методов установки$query->setUsername($username);
. Вы можете действительно спроектировать это, однако это имеет смысл для вашего конкретного приложения, и я думаю, что объекты запросов оставляют здесь большую гибкость.Исходя из моего опыта, вот несколько ответов на ваши вопросы:
Q: Как мы имеем дело с возвращением полей, которые нам не нужны?
A: Из моего опыта это действительно сводится к работе с полными сущностями, а не со специальными запросами.
Полная сущность - это что-то вроде
User
объекта. У него есть свойства, методы и т. Д. Это первоклассный гражданин в вашей кодовой базе.Специальный запрос возвращает некоторые данные, но мы не знаем ничего кроме этого. Поскольку данные передаются по приложению, это делается без контекста. Это
User
? АUser
с какой-тоOrder
информацией прилагается? Мы действительно не знаем.Я предпочитаю работать с полными лицами.
Вы правы в том, что часто будете возвращать данные, которые не будете использовать, но вы можете решить эту проблему различными способами:
User
для бэкенда и, возможно,UserSmall
для AJAX-звонков. У одного может быть 10 свойств, а у другого - 3.Недостатки работы со специальными запросами:
User
вы в конечном итоге будете писать практически одинаковоselect *
для многих вызовов. Один вызов получит 8 из 10 полей, одно получит 5 из 10, один получит 7 из 10. Почему бы не заменить всех одним вызовом, который получает 10 из 10? Причина, по которой это плохо, заключается в том, что повторно анализировать / тестировать / издеваться - убийство.User
так медленно?» вы в конечном итоге отслеживаете разовые запросы, поэтому исправления ошибок, как правило, небольшие и локализованные.Q: У меня будет слишком много методов в моем хранилище.
A: Я действительно не видел никакого другого пути, кроме как консолидировать звонки. Вызовы методов в вашем репозитории действительно соответствуют функциям вашего приложения. Чем больше функций, тем больше данных конкретных звонков. Вы можете отказаться от функций и попытаться объединить похожие вызовы в один.
Сложность в конце дня должна где-то существовать. С шаблоном репозитория мы поместили его в интерфейс репозитория вместо того, чтобы делать кучу хранимых процедур.
Иногда я должен сказать себе: «Ну, это должно было где-то дать! Серебряных пуль нет».
источник
SELECT *
, а только выбирает те поля, которые вам нужны. Например, посмотрите этот вопрос . Что касается всех тех рекламных запросов, о которых вы говорите, я, конечно, понимаю, откуда вы. У меня сейчас очень большое приложение, в котором их много. Это было мое "Ну, это должно было дать где-то!" момент, я выбрал максимальную производительность. Тем не менее, сейчас я имею дело с большим количеством различных запросов.reads
часто возникают проблемы с производительностью, вы можете использовать для них более индивидуальный подход к запросам, который не превращается в реальные бизнес-объекты. Тогда, дляcreate
,update
иdelete
, использовать ORM, который работает с целыми объектами. Есть мысли об этом подходе?Я использую следующие интерфейсы:
Repository
- загружает, вставляет, обновляет и удаляет объектыSelector
- находит сущности на основе фильтров в хранилищеFilter
- инкапсулирует логику фильтрацииМоя
Repository
база данных не зависит; на самом деле это не указывает на постоянство; это может быть что угодно: база данных SQL, файл XML, удаленный сервис, пришелец из космоса и т.д. Для поиска возможностей, тоRepository
конструирует ,Selector
которые могут быть отфильтрованы,LIMIT
-ed, сортирует и подсчитано. В конце концов, селектор выбирает один или несколькоEntities
из персистентности.Вот пример кода:
Тогда одна реализация:
Идея состоит в том, что универсальный
Selector
использует,Filter
но реализацияSqlSelector
используетSqlFilter
;SqlSelectorFilterAdapter
адаптирует родовыеFilter
к бетонуSqlFilter
.Клиентский код создает
Filter
объекты (которые являются общими фильтрами), но в конкретной реализации селектора эти фильтры преобразуются в фильтры SQL.Другие реализации селектора, как
InMemorySelector
, преобразование из ,Filter
чтобы сInMemoryFilter
помощью их специфическихInMemorySelectorFilterAdapter
; Итак, каждая реализация селектора поставляется с собственным адаптером фильтра.Используя эту стратегию, мой клиентский код (на уровне bussines) не заботится о конкретном репозитории или реализации селектора.
PS Это упрощение моего реального кода
источник
Я добавлю немного к этому, поскольку в настоящее время я пытаюсь понять все это самостоятельно.
№ 1 и 2
Это идеальное место для вашего ORM, чтобы сделать тяжелую работу. Если вы используете модель, которая реализует какой-то ORM, вы можете просто использовать ее методы, чтобы позаботиться об этих вещах. Создайте свои собственные функции orderBy, которые реализуют методы Eloquent, если вам нужно. Используя Eloquent, например:
То, что вы, похоже, ищете, это ORM. Нет причин, по которым ваш репозиторий не может быть основан на одном. Это потребует от пользователя расширения eloquent, но я лично не вижу в этом проблемы.
Однако если вы хотите избежать ORM, вам придется «свернуть свое», чтобы получить то, что вы ищете.
# 3
Интерфейсы не должны быть жесткими и быстрыми требованиями. Что-то может реализовать интерфейс и добавить к нему. Чего он не может сделать, так это не реализовать требуемую функцию этого интерфейса. Вы также можете расширить интерфейсы, такие как классы, чтобы сохранить вещи СУХОЙ.
Тем не менее, я только начинаю понимать, но эти реализации помогли мне.
источник
Я могу только прокомментировать, как мы (в моей компании) имеем дело с этим. Прежде всего, производительность не является большой проблемой для нас, но наличие чистого / правильного кода.
Прежде всего, мы определяем модели, такие как,
UserModel
который использует ORM для созданияUserEntity
объектов. Когда аUserEntity
загружается из модели, загружаются все поля. Для полей, ссылающихся на иностранные объекты, мы используем соответствующую внешнюю модель для создания соответствующих объектов. Для этих объектов данные будут загружаться по требованию. Теперь ваша первоначальная реакция может быть ... ??? ... !!! позвольте мне привести пример немного примера:В нашем случае
$db
это ORM, который может загружать объекты. Модель инструктирует ORM загружать набор сущностей определенного типа. ORM содержит отображение и использует его для внедрения всех полей этого объекта в объект. Однако для внешних полей загружаются только идентификаторы этих объектов. В этом случаеOrderModel
создаетOrderEntity
s только с идентификаторами ссылочных заказов. КогдаPersistentEntity::getField
вызываются самойOrderEntity
сущность инструктирует это модель для ленивых нагрузок всех полей вOrderEntity
с. ВсеOrderEntity
объекты, связанные с одним UserEntity, обрабатываются как один набор результатов и будут загружены одновременно.Волшебство здесь в том, что наша модель и ORM внедряют все данные в сущности, и эти сущности просто предоставляют функции-оболочки для универсального
getField
метода, предоставляемогоPersistentEntity
. Подводя итог, мы всегда загружаем все поля, но поля, ссылающиеся на стороннюю сущность, загружаются при необходимости. Просто загрузка нескольких полей не является проблемой производительности. Загрузка всех возможных сторонних объектов, однако, будет ОГРОМНОЕ снижение производительности.Теперь перейдем к загрузке определенного набора пользователей, основываясь на предложении where. Мы предоставляем объектно-ориентированный пакет классов, который позволяет вам указать простое выражение, которое можно склеить. В примере кода я назвал его
GetOptions
. Это обертка для всех возможных вариантов запроса select. Он содержит коллекцию предложений where, группу по выражению и все остальное. Наши предложения where довольно сложны, но вы, очевидно, можете легко сделать более простую версию.Простейшей версией этой системы будет передача части запроса WHERE в виде строки непосредственно в модель.
Я извиняюсь за этот довольно сложный ответ. Я попытался обобщить нашу структуру как можно быстрее и яснее. Если у вас есть дополнительные вопросы, не стесняйтесь их задавать, и я обновлю свой ответ.
РЕДАКТИРОВАТЬ: Кроме того, если вы действительно не хотите загружать некоторые поля сразу, вы можете указать опцию отложенной загрузки в вашем отображении ORM. Поскольку все поля в конечном итоге загружаются через
getField
метод, вы можете загрузить некоторые поля в последнюю минуту при вызове этого метода. Это не очень большая проблема в PHP, но я бы не рекомендовал для других систем.источник
Это несколько разных решений, которые я видел. У каждого из них есть свои плюсы и минусы, но решать вам.
Проблема № 1: слишком много полей
Это важный аспект, особенно если учесть сканирование только по индексу . Я вижу два решения для решения этой проблемы. Вы можете обновить свои функции, добавив необязательный параметр массива, который будет содержать список возвращаемых столбцов. Если этот параметр пуст, вы бы вернули все столбцы в запросе. Это может быть немного странно; основываясь на параметре, вы можете получить объект или массив. Вы также можете дублировать все свои функции, чтобы у вас было две разные функции, выполняющие один и тот же запрос, но одна возвращает массив столбцов, а другая возвращает объект.
Проблема № 2: слишком много методов
Я кратко работал с Propel ORM год назад, и это основано на том, что я помню из этого опыта. Propel имеет возможность генерировать свою структуру классов на основе существующей схемы базы данных. Создает два объекта для каждой таблицы. Первый объект - это длинный список функций доступа, аналогичный тому, который вы в данный момент перечислили;
findByAttribute($attribute_value)
, Следующий объект наследуется от этого первого объекта. Вы можете обновить этот дочерний объект, чтобы встроить более сложные функции получения.Другое решение будет использовать
__call()
для отображения неопределенных функций на что-то действенное. Ваш__call
метод сможет проанализировать findById и findByName в разных запросах.Надеюсь, это поможет хотя бы кое-чему.
источник
Я предлагаю https://packagist.org/packages/prettus/l5-repository в качестве поставщика для реализации репозиториев / критериев и т.д ... в Laravel5: D
источник
Я согласен с @ ryan1234, что вы должны передавать полные объекты в коде и использовать общие методы запросов для получения этих объектов.
Для внешнего / конечного использования мне очень нравится метод GraphQL.
источник
Моя интуиция говорит мне, что для этого может потребоваться интерфейс, который реализует методы, оптимизированные для запросов, наряду с общими методами. Запросы, чувствительные к производительности, должны иметь целевые методы, в то время как нечастые или облегченные запросы обрабатываются универсальным обработчиком, возможно, за счет того, что контроллер выполняет немного больше манипуляций.
Универсальные методы позволили бы реализовать любой запрос и, таким образом, предотвратили бы прерывание изменений в течение переходного периода. Целевые методы позволяют оптимизировать вызов, когда это имеет смысл, и его можно применять к нескольким поставщикам услуг.
Этот подход был бы похож на аппаратные реализации, выполняющие определенные оптимизированные задачи, в то время как программные реализации выполняют легкую работу или гибкую реализацию.
источник
Я думаю, что GraphQL является хорошим кандидатом в таком случае, чтобы обеспечить крупномасштабный язык запросов без увеличения сложности хранилищ данных.
Однако есть и другое решение, если вы сейчас не хотите переходить на graphQL. Используя DTO, где объект используется для передачи данных между процессами, в данном случае между службой / контроллером и хранилищем.
Элегантный ответ уже приведен выше, однако я попытаюсь привести еще один пример, который, на мой взгляд, проще и может послужить отправной точкой для нового проекта.
Как показано в коде, нам потребуется всего 4 метода для операций CRUD.
find
метод будет использоваться для включения и чтения, передавая объект аргумента. Базовые сервисы могут создавать определенный объект запроса на основе строки запроса URL или на основе определенных параметров.Объект запроса (
SomeQueryDto
) также может реализовывать определенный интерфейс, если это необходимо. и его легко расширить позже, не добавляя сложности.Пример использования:
источник