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

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

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

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

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

Как реализовать составные запросы: просмотр 10 тыс. Футов

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

Пример кода

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

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

  1. Существует некоторый тип, который $db является экземпляром, который предоставляет по крайней мере метод select . Обратите внимание: если вы хотите полностью переупорядочить вызовы, этот тип должен выставлять методы со всеми возможными сигнатурами, которые могут принимать участие в цепочке вызовов.
  2. Каждый из прикованных методов возвращает экземпляр того, что предоставляет методы, все соответствующие сигнатуры; это может быть или не быть тем же типом, что и $db .
  3. После того, как был собран «план запроса», нам нужно вызвать некоторый метод, чтобы фактически выполнить его и вернуть результаты (процесс, который я собираюсь назвать материализацией запроса). Этот метод может быть только последним в цепочке вызовов по очевидным причинам, но в этом случае последний метод – это 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'); 

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

Итак, как решить эту проблему? Один из вариантов был бы для select сброса плана запроса, но в этом проблема противоположна: $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 более одного раза)

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

Это требует очень элегантного решения.

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

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

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

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