Как создать свободный интерфейс запросов?

Я знаю, как связать методы класса (с помощью "return $this" и всех), но то, что я пытаюсь сделать, это связать их по-умному, взгляните на это:

$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');

Что я мог понять из этого примера кода, так это то, что первые 3 метода (select, where, limit) создают запрос, который будет выполнен, и последний (заказ) приходит, чтобы закончить оператор, а затем выполняет его и отбрасывает результат, правильно?

Но это не так, потому что я могу легко отказаться от любого из этих методов (кроме "выбрать", конечно) или, что более важно, изменить свой порядок, и ничто не пойдет не так! Это означает, что метод "select" обрабатывает работу, не так ли? Затем, как другие 3 метода добавляют/влияют на запрос после того, как метод "select" уже был вызван!??

Ответ 1

Как реализовать составные запросы: вид 10k футов

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

Пример кода

$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');

Что мы видим здесь?

  • Существует некоторый тип, который $db является экземпляром, который предоставляет по меньшей мере метод select. Обратите внимание: если вы хотите полностью переупорядочить вызовы, этот тип должен выставлять методы со всеми возможными сигнатурами, которые могут принимать участие в цепочке вызовов.
  • Каждый из прикованных методов возвращает экземпляр того, что предоставляет методы, все соответствующие сигнатуры; это может быть или не быть тем же типом, что и $db.
  • После того, как был собран "план запроса", нам нужно вызвать какой-то метод для его фактического выполнения и вернуть результаты (процесс, который я собираюсь назвать материализацией запроса). Этот метод может быть только последним в цепочке вызовов по очевидным причинам, но в этом случае последний метод order, что кажется неправильным: мы хотим, чтобы его можно было перенести ранее в цепочке. Помните об этом.

Поэтому мы можем разрушить то, что происходит в трех разных шагах.

Шаг 1: Отключение

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

interface QueryPlanInterface
{
    public function select(...);
    public function limit(...);
    // etc
}

class QueryPlan implements QueryPlanInterface
{
    private $variable_that_points_to_data_store;
    private $variables_to_hold_query_description;

    public function select(...)
    {
        $this->encodeSelectInformation(...);
        return $this;
    }

    // and so on for the rest of the methods; all of them return $this
}

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

Означает ли это, что $db имеет тип QueryPlan? На первый взгляд вы можете сказать "да", но при ближайшем рассмотрении проблемы начинают возникать из-за такой договоренности. Самая большая проблема - это устаревшее состояние:

// What would this code do?
$db->limit(2);

// ...a little later...
$albums = $db->select('albums');

Сколько альбомов будет извлечено? Поскольку мы не "reset", план запроса должен быть 2. Но это совершенно не очевидно из последней строки, которая читается совсем по-другому. Это плохое расположение, которое может привести к ненужным ошибкам.

Итак, как решить эту проблему? Один из вариантов был бы для select до reset плана запроса, но это имеет противоположную проблему: $db->limit(1)->select('albums') теперь выбирает все альбомы. Это не выглядит приятным.

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

class DatabaseTable
{
    public function query()
    {
        return new QueryPlan(...); // pass in data store-related information
    }
}

который решает все эти проблемы, но требует, чтобы вы всегда записывали ->query() спереди:

$db->query()->limit(1)->select('albums');

Что делать, если вы не хотите иметь этот дополнительный звонок? В этом случае класс DatabaseTable должен реализовать QueryPlanInterface, с той разницей, что реализация будет создавать новый QueryPlan каждый раз:

class DatabaseTable implements QueryPlanInterface
{

    public function select(...)
    {
        $q = new QueryPlan();
        return $q->select(...);
    }

    public function limit(...)
    {
        $q = new QueryPlan();
        return $q->limit(...);
    }

    // and so on for the rest of the methods
}

Теперь вы можете написать $db->limit(1)->select('albums') без проблем; расположение можно описать как "каждый раз, когда вы пишете $db->something(...), вы начинаете составлять новый запрос, который не зависит от всех предыдущих и будущих".

Шаг 2: цепочка

Это самая простая часть; мы уже видели, как методы QueryPlan всегда return $this, чтобы включить цепочку.

Шаг 3: Материализация

Нам еще нужно сказать "ОК, я сочиняю, получаю результаты". Для этой цели можно использовать специальный метод:

interface QueryPlanInterface
{
    // ...other methods as above...
    public function get(); // this executes the query and returns the results
}

Это позволяет вам писать

$anAlbum = $db->limit(1)->select('albums')->get();

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

Ответ: да и нет. Да в том, что это действительно возможно, но нет в том смысле, что семантика происходящего изменится.

У PHP нет средства, которое позволяет автоматически "вызывать" метод, поэтому должно быть что-то, что инициирует материализацию, даже если это что-то не похоже на вызов метода с первого взгляда. Но что? Ну, подумайте о том, что может быть наиболее распространенным вариантом использования:

$albums = $db->select('albums'); // no materialization yet
foreach ($albums as $album) {
    // ...
}

Можно ли это сделать? Конечно, пока QueryPlanInterface расширяет IteratorAggregate:

interface QueryPlanInterface extends IteratorAggregate
{
    // ...other methods as above...
    public function getIterator();
}

Идея здесь состоит в том, что foreach вызывает вызов getIterator, который, в свою очередь, создаст экземпляр еще одного класса, в который вводится вся информация, скомпилированная реализацией QueryPlanInterface. Этот класс будет выполнять фактический запрос на месте и материализовать результаты по запросу во время итерации.

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

Наконец, этот трюк foreach выглядит аккуратно, но как насчет другого распространенного варианта использования (получение результатов запроса в массив)? Мы сделали это громоздким?

Не очень, спасибо iterator_to_array:

$albums = iterator_to_array($db->select('albums'));

Заключение

Требуется ли много кода для написания? Наверняка. У нас есть DatabaseTable, QueryPlanInterface, QueryPlan, а также QueryPlanIterator, которые мы описали, но не показаны. Кроме того, все кодированное состояние, в котором эти агрегаты классов, вероятно, должны храниться в экземплярах еще большего количества классов.

Стоит ли это того? Вполне вероятно. Это потому, что это решение предлагает:

  • привлекательный свободный интерфейс (цепочки вызовов) с четкой семантикой (каждый раз, когда вы начинаете, вы начинаете описывать новый запрос независимо от других)
  • развязка интерфейса запроса из хранилища данных (каждый экземпляр QueryPlan хранит дескриптор в абстрактном хранилище данных, поэтому вы можете теоретически запросить что-нибудь из реляционных баз данных в текстовые файлы с использованием того же синтаксиса)
  • (вы можете начать составлять QueryPlan сейчас и продолжать делать это в будущем, даже в другом методе)
  • повторно (вы можете материализовать каждый QueryPlan более одного раза)

Совсем не плохой пакет.

Ответ 2

Это действительно очень элегантное решение.

Вместо того, чтобы изобретать колесо, загляните в существующую структуру (ы).

Я предлагаю Laravel с использованием Eloquent ORM. Вы сможете сделать это и многое другое.

Ответ 3

Вам, вероятно, понадобится метод, который ударяет по фактическому запросу, в то время как методы, такие как select и order_by, просто сохраняют информацию до этой точки.

Вы можете сделать это неявным, но если вы реализуете интерфейс Iterator и запускаете запрос в первый раз rewind или current получил hit (think foreach) или Countable, поэтому количество результатов может быть вызвано вызовом count() с объектом. Я лично не хотел бы использовать библиотеку, построенную таким образом, я бы гораздо лучше оценил явный вызов, чтобы я мог видеть, где запускались запросы.