Ответ 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
более одного раза)
Совсем не плохой пакет.