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


292

Предисловие: Я пытаюсь использовать шаблон репозитория в архитектуре 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 *подход select all fields ( ). Однако в моих приложениях я всегда пытаюсь ограничить количество полей, которые я получаю, поскольку это часто увеличивает накладные расходы и замедляет работу. Для тех, кто использует этот шаблон, как вы справляетесь с этим?

Проблема № 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()), что, безусловно, приводит к серьезным проблемам с производительностью, если вы извлекаете данные из базы данных.

Помогите?

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

Ответы:


208

Я подумал, что смогу ответить на свой вопрос. Далее следует лишь один из способов решения проблем 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, поэтому чаще всего я просто ссылаюсь на эти модели непосредственно во всем приложении. Однако в ситуациях, когда у меня есть более сложные запросы, я буду использовать объекты запросов, чтобы сделать их более пригодными для повторного использования. Я также должен отметить, что я всегда внедряю свои модели в свои методы, что облегчает имитацию в моих тестах.


4
@PeeHaa Опять же, чтобы примеры были простыми. Очень часто не показывать фрагменты кода в качестве примера, если они не относятся конкретно к данной теме. На самом деле я бы передавал свои зависимости.
Джонатан

4
Интересно, что вы разделили создание, обновление и удаление из чтения. Подумал, что стоит упомянуть об отделении ответственности за командные запросы (CQRS), которое формально делает именно это. martinfowler.com/bliki/CQRS.html
Адам

2
@Jonathan Прошло полтора года с тех пор, как ты ответил на свой вопрос. Мне было интересно, довольны ли вы своим ответом и является ли это вашим основным решением для большинства ваших проектов? Последние несколько недель я много читал в репозиториях и видел, как у многих людей есть своя собственная интерпретация того, как это должно быть реализовано. Вы называете это объектами запроса, но это уже существующий паттерн? Я думаю, что видел, как это используется на других языках.
Боэди

1
@Jonathan: Как вы обрабатываете запросы, которые должны указывать пользователю не «ID», а, например, «username» или даже более сложные запросы с более чем одним условием?
Gizzmo

1
@Gizzmo Используя объекты запросов, вы можете передавать дополнительные параметры, чтобы помочь с более сложными запросами. Например, вы можете сделать это в конструкторе: new Query\ComplexUserLookup($username, $anotherCondition). Или сделать это с помощью методов установки $query->setUsername($username);. Вы можете действительно спроектировать это, однако это имеет смысл для вашего конкретного приложения, и я думаю, что объекты запросов оставляют здесь большую гибкость.
Джонатан

48

Исходя из моего опыта, вот несколько ответов на ваши вопросы:

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

A: Из моего опыта это действительно сводится к работе с полными сущностями, а не со специальными запросами.

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

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

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

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

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

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

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

Q: У меня будет слишком много методов в моем хранилище.

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

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

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


Спасибо за очень тщательный ответ. Вы заставили меня думать сейчас. Моя большая проблема в том, что все, что я читаю, не говорит SELECT *, а только выбирает те поля, которые вам нужны. Например, посмотрите этот вопрос . Что касается всех тех рекламных запросов, о которых вы говорите, я, конечно, понимаю, откуда вы. У меня сейчас очень большое приложение, в котором их много. Это было мое "Ну, это должно было дать где-то!" момент, я выбрал максимальную производительность. Тем не менее, сейчас я имею дело с большим количеством различных запросов.
Джонатан

1
Одна последующая мысль. Я видел рекомендацию использовать подход R-CUD. Поскольку readsчасто возникают проблемы с производительностью, вы можете использовать для них более индивидуальный подход к запросам, который не превращается в реальные бизнес-объекты. Тогда, для create, updateи delete, использовать ORM, который работает с целыми объектами. Есть мысли об этом подходе?
Джонатан

1
Как примечание для использования "выберите *". Я делал это в прошлом, и это работало нормально - пока мы не поразили поля varchar (max). Те убили наши запросы. Так что если у вас есть таблицы с полями, маленькими текстовыми полями и т. Д., Это не так уж плохо Кажется неестественным, но программное обеспечение идет этим путем. То, что было плохо, вдруг хорошо и наоборот.
ryan1234

1
Подход R-
CUD на

2
@ ryan1234 «Сложность в конце дня должна где-то существовать». Спасибо тебе за это. Заставляет меня чувствовать себя лучше.
Джонни

20

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

  • 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 Это упрощение моего реального кода


«Репозиторий - загружает, вставляет, обновляет и удаляет сущности» - вот что могут делать «
сервисные слои

5

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

№ 1 и 2

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

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

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

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

# 3

Интерфейсы не должны быть жесткими и быстрыми требованиями. Что-то может реализовать интерфейс и добавить к нему. Чего он не может сделать, так это не реализовать требуемую функцию этого интерфейса. Вы также можете расширить интерфейсы, такие как классы, чтобы сохранить вещи СУХОЙ.

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


1
Что мне не нравится в этом методе, так это то, что если бы у вас был MongoUserRepository, он и ваш DbUserRepository возвращали бы разные объекты. Db возвращает Eloquent \ Model, а Mongo что-то свое. Конечно, лучшая реализация состоит в том, чтобы оба хранилища возвращали экземпляры / коллекции отдельного класса Entity \ User. Таким образом, вы не ошибочно полагаетесь на методы DB Eloquent \ Model, когда переключаетесь на использование MongoRepository
danharper

1
Я определенно согласен с вами в этом. Что бы я, вероятно, сделал, чтобы избежать этого, никогда не используйте эти методы вне класса Eloquent require. Таким образом, функция get, вероятно, должна быть закрытой и использоваться только внутри класса, так как, как вы указали, будет возвращать то, чего не могли другие репозитории.
Будет

3

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ: Кроме того, если вы действительно не хотите загружать некоторые поля сразу, вы можете указать опцию отложенной загрузки в вашем отображении ORM. Поскольку все поля в конечном итоге загружаются через getFieldметод, вы можете загрузить некоторые поля в последнюю минуту при вызове этого метода. Это не очень большая проблема в PHP, но я бы не рекомендовал для других систем.


3

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

Проблема № 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), Следующий объект наследуется от этого первого объекта. Вы можете обновить этот дочерний объект, чтобы встроить более сложные функции получения.

Другое решение будет использовать __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]
    }
}

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



0

Я согласен с @ ryan1234, что вы должны передавать полные объекты в коде и использовать общие методы запросов для получения этих объектов.

Model::where(['attr1' => 'val1'])->get();

Для внешнего / конечного использования мне очень нравится метод GraphQL.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

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

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

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

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

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


0

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

Однако есть и другое решение, если вы сейчас не хотите переходить на graphQL. Используя DTO, где объект используется для передачи данных между процессами, в данном случае между службой / контроллером и хранилищем.

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

Как показано в коде, нам потребуется всего 4 метода для операций CRUD. findметод будет использоваться для включения и чтения, передавая объект аргумента. Базовые сервисы могут создавать определенный объект запроса на основе строки запроса URL или на основе определенных параметров.

Объект запроса ( 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]/', '_$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);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.