Правильний дизайн шаблону репозиторію в PHP?


291

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

Випуск №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для наполегливості. Зауважте, мої сховища все одно надаватимуть метод читання, але просто для створення об'єкта, а не для відображення. Детальніше про це пізніше.

R (Прочитати) не так просто. Тут немає моделей, лише цінуються об'єкти . Використовуйте масиви, якщо хочете . Ці об'єкти можуть представляти одну модель або суміш багатьох моделей, що-небудь справді. Вони самі по собі не дуже цікаві, але як вони генеруються. Я використовую те, що дзвоню Query Objects.

Код:

Модель користувача

Почнемо просто з нашої базової моделі користувача. Зауважте, що розширення ORM або бази даних взагалі немає. Просто чиста модель слава. Додайте свої геттери, сеттери, валідацію, що завгодно.

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

Інтерфейс сховища

Перш ніж створити моє сховище користувачів, я хочу створити свій інтерфейс сховища. Це визначить "контракт", якого повинні дотримуватися сховища, щоб використовувати мій контролер. Пам'ятайте, мій контролер не буде знати, де фактично зберігаються дані.

Зауважте, що мої сховища будуть містити лише ці три методи. save()Метод відповідає за як створення і оновлення користувачів, просто в залежності від того, чи має об'єкт користувача в набір ID.

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


4
@PeeHaa Знову ж таки, щоб приклади були простими. Дуже часто залишати фрагменти коду з прикладу, якщо вони не стосуються конкретної теми. Насправді я перейшов би у свої залежності.
Джонатан

4
Цікаво, що ви розділили створене, оновлене та видалене з читання. Думаю, варто згадати Сегрегацію відповідальності за запити команд (CQRS), яка формально робить саме це. martinfowler.com/bliki/CQRS.html
Адам,

2
@Jonathan Минуло півтора року, як ти відповів на власне запитання. Мені було цікаво, чи ви все ще задоволені своєю відповіддю і чи це зараз ваше основне рішення для більшості ваших проектів? Останні кілька тижнів я читав багато в сховищах і бачив, що у багатьох людей є власна інтерпретація того, як це потрібно реалізувати. Ви називаєте це запитом об'єктів, але це вже існуючий шаблон? Я думаю, я бачив, як він використовується в інших мовах.
Boedy

1
@Jonathan: Як ви обробляєте запити, які повинні доопрацювати користувача, не "ID", а, наприклад, "ім'я користувача" або ще складніші запити з більш ніж однією умовою?
Gizzmo

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

48

На основі мого досвіду, ось кілька відповідей на ваші запитання:

Питання: Як ми маємо справу з тим, щоб повернути нам непотрібні поля?

Відповідь: З мого досвіду, це дійсно зводиться до роботи з повною організацією та спеціальними запитами.

Повна сутність - це щось на зразок 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 спеціальних дзвінків набагато складніше, ніж це декілька сутностей.

З: У мене буде занадто багато методів у моєму сховищі.

Відповідь: Я насправді цього не бачив, крім консолідації дзвінків. Виклики методу у вашому сховищі дійсно відображають функції у вашій програмі. Чим більше функцій, тим більше конкретних дзвінків. Ви можете натиснути на функції та спробувати об'єднати подібні дзвінки в один.

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

Іноді мені доводиться казати собі: "Ну це треба було десь віддавати! Срібних куль немає".


Дякую за дуже ретельну відповідь. Ви зараз задумалися. Моє велике занепокоєння в тому, що все, що я читаю, не робить SELECT *, а лише вибирайте потрібні поля. Наприклад, дивіться це питання . Що стосується всіх тих рекламних запитів, про які ви говорите, я, безумовно, розумію, звідки ви беретесь. Зараз у мене дуже велике додаток, яке має багато з них. Це було моє "Ну це треба було десь дати!" Момент, я вибрав максимальну продуктивність. Однак зараз я маю справу з ЛОТОМ різних запитів.
Джонатан

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

1
Як примітка до використання "select *". Я робив це в минулому, і це справно працювало - поки ми не потрапили в поля varchar (max). Ті вбили наші запити. Тож якщо у вас є таблиці з вкладками, невеликими текстовими полями тощо, це не так вже й погано. Відчуває себе неприродно, але програмне забезпечення йде таким шляхом. Те, що було погано, раптом добре і навпаки.
ryan1234

1
Підхід R-CUD насправді є CQRS
MikeSW

2
@ ryan1234 "Складність наприкінці дня має десь існувати". Дякую за це. Змушує мене почуватись краще.
johnny

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);
    }
}

Ideea в тому , що в загальних Selectorцілях , Filterале в реалізації SqlSelectorцілей SqlFilter; SqlSelectorFilterAdapterадаптує родові Filterдо бетону SqlFilter.

Клієнтський код створює Filterоб'єкти (це загальні фільтри), але в конкретній реалізації селектора ці фільтри перетворюються у фільтри SQL.

Інші реалізації селектора, як-от InMemorySelector, перетворюються з Filterна InMemoryFilterвикористання їх конкретних InMemorySelectorFilterAdapter; Отже, кожна реалізація селектора має власний адаптер фільтра.

Використовуючи цю стратегію, мій клієнтський код (на рівні бізнесу) не хвилює конкретного сховища чи реалізації селектора.

/** @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 Це спрощення мого реального коду


"Репозиторій - завантажує, вставляє, оновлює та видаляє об'єкти". Це те, що можуть зробити "сервісний рівень", "DAO", "BLL"
Yousha Aleayoub

5

Я додам трохи про це, оскільки зараз я намагаюся все це зрозуміти сам.

№1 і 2

Це ідеальне місце для вашого ORM, щоб зробити важкий підйом. Якщо ви використовуєте модель, яка реалізує якусь ORM, ви можете просто використовувати її методи, щоб подбати про ці речі. Зробіть власні замовленняBy функції, які реалізують красномовні методи, якщо вам потрібно. Наприклад, використовуючи красномовний:

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

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

Те, що ви, здається, шукаєте, це ОРМ. Немає причин, що ваш Репозиторій не може базуватися на ньому. Це вимагає від користувачів красномовного розширення, але я особисто не вважаю це проблемою.

Якщо ви все-таки хочете уникнути ORM, вам доведеться "закатати свій", щоб отримати те, що ви шукаєте.

№3

Інтерфейси не повинні бути жорсткими та швидкими вимогами. Щось може реалізувати інтерфейс і додати його. Що він не може зробити - це не реалізувати необхідну функцію цього інтерфейсу. Ви також можете розширити інтерфейси, як класи, щоб зберегти речі ВСУХОМ.

Це сказало, що я тільки починаю розуміти, але ці розуміння мені допомогли.


1
Мені не подобається у цьому методі, що якби у вас був MongoUserRepository, він і ваш DbUserRepository повертали різні об'єкти. Db повертає красномовні \ моделі, а Монго щось своє. Безумовно, краща реалізація полягає в тому, щоб обидва сховища повертали екземпляри / колекції окремого класу Entity \ User. Таким чином, ви не помиляєтесь на методи кращих
баз

1
Я б точно погодився з вами на цьому. Що я, мабуть, зробив би, щоб цього не було, - це ніколи не використовувати ці методи поза красномовним класом, що вимагає. Тож функція get, ймовірно, повинна бути приватною і використовуватись лише у класі, оскільки вона, як ви вказали, повертає щось, чого не могли б інші сховища.
Буде

3

Я можу лише прокоментувати те, як ми (у моїй компанії) маємо справу з цим. Перш за все продуктивність не надто велика проблема для нас, але чистий / належний код - це.

Перш за все ми визначаємо такі моделі, як, UserModelщо використовує ORM для створення UserEntityоб'єктів. Коли a 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 містить відображення і використовує це для введення всіх полів для цієї сутності в сутність. Однак для іноземних полів завантажуються лише ідентифікатори цих об'єктів. У цьому випадку OrderModelcreate створює OrderEntityлише ідентифікатори посилань. Коли PersistentEntity::getFieldвиклик OrderEntityсуб'єкта господарювання вказує, що його модель ледаче завантажує всі поля в OrderEntitys. Усі OrderEntitys, пов’язані з одним UserEntity, трактуються як один набір результатів і будуть завантажені відразу.

Магія тут полягає в тому, що наша модель та ORM вводять всі дані в сутність, і ці сутності просто забезпечують функції обгортки для загального getFieldметоду, який надає компанія PersistentEntity. Підводячи підсумок, ми завжди завантажуємо всі поля, але поля, що посилаються на іноземну сутність, завантажуються при необхідності. Просто завантаження купу полів насправді не є проблемою продуктивності. Завантаження всіх можливих іноземних організацій, однак, було б величезним зниженням продуктивності.

Тепер на завантаження певного набору користувачів на основі пункту де. Ми надаємо об'єктно-орієнтований пакет класів, який дозволяє вказати простий вираз, який можна склеїти. У коді прикладу я його назвав GetOptions. Це обгортка для всіх можливих варіантів вибору запиту. Він містить збірник де пункти, група за пунктом та все інше. Наші пункти, де пропозиції досить складні, але ви, очевидно, можете легко спростити версію.

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

Найпростішою версією цієї системи було б передати частину запиту WHERE у вигляді рядка безпосередньо моделі.

Мені шкода за цю досить складну відповідь. Я намагався узагальнити наші рамки якомога швидше і зрозуміліше. Якщо у вас є якісь додаткові запитання, не соромтесь їх задати, і я оновлю свою відповідь.

EDIT: Крім того, якщо ви дійсно не хочете відразу завантажувати деякі поля, ви можете вказати варіант ледачого завантаження у вашому відображенні 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.