Всем привет!
Сегодня плагин WP-Recall был обновлен до версии 16.21.0 и в этой статье я хочу поделиться описанием одной обновки, которая стала доступна разработчикам.
Возможно, кто то помнит, что более 3-лет назад была опубликована статья Rcl_Query — удобный класс для построения запросов к БД от WP-Recall. Данный класс оказался действительно удобной вещицей, я и другие разработчики активно применяли его при написании запросов к БД в своих дополнениях и проектах, но как это часто бывает, по ходу работы, кроме уже озвученных плюсов стали видны и очевидные минусы выбранного подхода, с которыми необходимо было что то делать.
В результате, пришлось полностью пересмотреть порядок построения запроса через Rcl_Query, чтобы получить более гибкий инструмент, чем тот, что был. И ниже я расскажу что же было сделано и как с этим работать.
Основной проблемой при использовании прежней итерации класса Rcl_Query стала некая ограниченность для дальнейшего расширения, все-таки подход с передачей в основные методы класса какого то единого массива данных из которых и строился запрос упирался в возможности массива php. Внутри себя массив может иметь связь ключ-значение и некие вложенные массивы, поэтому для построения сложных запросов приходилось строить сложный массив со множеством вложенных массивов, запутаться в которых было совсем несложным делом. Мы решили уйти от построения монструозных массивов и начать более активно использовать отдельные методы.
Итак, давайте рассмотрим на примерах, как было и как стало.
Начнем с того, что регистрация самой таблицы с которой мы будем работать по сути не изменилась и выглядит как и прежде:
class Rcl_Groups_Query extends Rcl_Query { public $serialize = ['group_users']; //в этом свойстве можем указать перечень столбцов в которых данные сериализуются function __construct($as = false) { $table = array( 'name' => RCL_PREF ."groups", 'as' => $as? $as: 'rcl_groups', 'cols' => array( 'ID', 'admin_id', 'group_users', 'group_status', 'group_date' ) ); parent::__construct($table); } }
Заметим только, что при инициализации объекта query можно будет указать свойство $serialize - массив с перечнем наименований столбцов, в которых данные храняться в сериализованном виде, а также настоятельно рекомендуется иметь возможность указывать произвольный alias таблицы при инициализации класса, например:
$query = new Rcl_Groups_Query('groups_one');
Это очень поможет, когда возникнет необходимость строить сложный запрос содержащий несколько, возможно, вложенных запросов или придется заджойнить одну и туже таблицу несколько раз. Ниже остановлюсь подробнее.
Итак, теперь к главному, рассмотрим простой запрос по старой схеме
$query = new Rcl_Groups_Query(); $args = array( 'ID' => 76 ); $query->get_row($args);
По сути, в массиве мы указали условие выборки "WHERE ID = '76'". Новый подход предполагает использование метода where для этой задачи:
$query = new Rcl_Groups_Query(); $query->where(['ID' => 76])->get_row();
Данный подход позволит получить нам тот же самый результат, что прежний подход, но мы можем увидеть, что метод where возвращает измененный объект query из которого уже далее мы вызываем метод get_row() на получение данных. Теперь мы можем получить и работать напрямую с query-объектом запроса, менять его и использовать в разных местах скрипта. Например, можно сделать так:
$query = new Rcl_Groups_Query(); $query = $query->where(['group_status' => 'open']); $sql1 = $query->get_sql(); //получим сформированный sql-запрос $cnt1 = $query->get_count(); //посчитаем результат $data1 = $query->get_results(); //получим данные $query->where(['admin' => 10]); //изменим запрос $sql2 = $query->get_sql(); //получим измененный sql-запрос $data2 = $query->get_results(); //получим другие данные
Принципиальное отличие нового подхода состоит в возможности работать с промежуточным звеном - объектом query. Этот объект возвращает каждый метод класса за исключением методов на получение данных, которые начинаются на get_. Например, построим более сложный запрос на выборку, с использованием метода select():
$query = new Rcl_Groups_Query(); $data = $query->select(['ID', 'group_status']) ->where(['group_status' => 'open', 'admin_id' => 10]) ->get_results();
Так мы получили запрос указанием полей, которые бы хотели получить и указанием условия выборки where. На этом примере мы можем обратить внимание на то, что нам приходится инициализацию $query выносить в отдельную строку. Это удобно, если мы захотим использовать $query где то еще ниже, но совсем не обязательно. Мы можем оформить весь этот запрос по сути одной строкой, для вашего удобства буду лишь делать принудительные переносы перед методами:
$data = RQ::tbl(new Rcl_Groups_Query()) ->select(['ID', 'group_status']) ->where(['group_status' => 'open', 'admin_id' => 10]) ->get_results();
т.е. для одновременной инициализации и использования query-объекта мы можем использовать конструкцию RQ::tbl(), куда должны будем передать объект класса нужной таблицы, в данном случае, это RQ::tbl(new Rcl_Groups_Query()).
Основные отличия нового подхода от старого были озвучены, давайте подробнее остановимся на перечне методов, которые мы можем теперь использовать внутри класса Rcl_Query.
Метод select()
select($args); $args - массив параметров, задающий перечень требуемых к получению данных. Данные указываются в виде: ключ => значение Если ключ указан в виде строки, то он будет восприниматься как alias, например: select(['user_id']) - результат будет содержать массив user_id => значение select(['uid' => 'user_id']) - результат будет содержать массив uid => значение В качестве значения к ключу можно передавать query-объект, в этом случае, будет возвращен результат запроса сформированного из переданного query-объекта. ключ может быть указан как 'max', 'min', 'count', 'sum', в этом случае, необходимо передавать массив колонок, которые хотим посчитать, например: select(['count' => ['user_id', 'uid' => 'user_id']]) - результат возвратит посчитанное значение user_id, а также uid - alias для user_id
Пример передачи возможных параметров в метод select():
$query = RQ::tbl(new tableQueryOne()) ->select([ 'colname1', 'alias1' => 'colname2', 'alias2' => RQ::tbl(new tableQueryTwo()), 'count' => [ //can be: count, max, min, sum 'colname3', 'alias3' => 'colname4', ] ]);
Метод where()
where($args); $args - массив параметров для указания условий выборки результата. Данные указываются в виде: ключ => значение Ключ определяет колонку выборки и тип сравнения, а значение - условие, по которому будет производится выборка. Значение может быть массивом, строкой, числом, а также query-объектом. Могут формироваться следующие ключи: '%colname%' //значение - строка, число, query-объект '%colname%__in' //значение - массив, query-объект '%colname%__not_in' //значение - массив, query-объект '%colname%__between' //значение - массив двух значений '%colname%__like' //значение - строка, число '%colname%__to' //значение - число '%colname%__from' //значение - число '%colname%__is' //значение - строка
Метод orderby()
orderby($orderby, $order = false); $orderby - указание столбца по которому будет производиться выборка. Можно указывать простое наименование столбца, тогда в запросе будет указана таблица из текущего query-объекта, например: 'colname' будет приведено к виду 'table_as.colname', но можно указать столбец сразу с указанием alias нужной таблицы, это удобно, если формируется сложный запрос для нескольких таблиц. $orderby может быть также массивом, если есть необходимость производить сортировку по нескольким столбцам. Внутри массива в качестве ключа указывается наименование столбца, а в качестве значения - направление сортировки. $order - необязательный параметр, указывает направление сортировки, можно указывать только если $orderby - строка. Может быть DESC, ASC
Пример передачи возможных параметров в метод orderby():
->orderby('colname'); ->orderby('table.colname', 'DESC'); ->orderby([ 'colname1' => 'ASC', 'colname2' => 'DESC' ]);
Метод order()
order($order); $order - указывает направление сортировки. Может быть 'DESC', 'ASC', 'RAND()'. По-умолчанию, в запросе указывается 'DESC'
Метод number()
number($number); - Метод передает в запрос количество строк необходимых к выборке. $number - число, указывает кол-во строк к выборке. Например: ->number(20);
Метод offset()
offset($offset); - метод передает в запрос указание значения для пропуска значений при выборке $offset - число, указывает кол-во строк, которые будут пропущены при выборке Например: ->offset(20);
Метод limit()
limit($number, $offset = 0); - метод объединяющий методы number() и offset() Например: ->limit(10); - выборка 10 строк без пропуска ->limit(5, 10); - выборка 5 строк с пропуском первых 10
Метод groupby()
groupby($groupby); - метод передает в запрос указание столбца по которому будет группироваться результат $groupby - строка, указывает наименование столбца, по которому будет производиться группировка. Может быть указано простое наименование столбца, тогда в конечном запросе будет автоматически указан alias таблицы из текущего query-объекта. Если результаты выбираются из нескольких таблиц, то будет удобно сразу указать alias таблицы и имя столбца. Например: ->groupby('colname'); ->groupby('table_as.colname');
Метод date()
date( $col_name, $compare, $props ); - отдельный метод для указания условий выборки по дате $col_name - строка, определяет наименование столбца, в котором будет производиться выборка $compare - сравнение выборки, может быть '>', '<', '>=', '<=', '=', 'BEETWEN' $props - строка или массив Если $props - строка, то ожидается значение для выборки по указанному столбцу. Если $props - массив, то содержимое может быть: при $compare равному '=': 'year' - выборка по определенному году, 'month' - выборка по определенному месяцу, 'day' - выборка по определенному числу, 'last' - выборка за указанному количество последних DAY, MONTH, YEAR и тп 'older' - выборка старше указанного количества DAY, MONTH, YEAR и тп при $compare равному 'BEETWEN': $props содержит массив из двух дат - начальной и конечной в остальных случаях: $props содержит массив с ключом 'interval' и значением DAY, MONTH, YEAR и тп которое будет сравниваться со временем NOW() при различных значениях $compare.
Пример передачи возможных параметров в метод date():
выборка за последний месяц ->date('colname', '=', ['last' => '1 MONTH']); выборка результатов старше 7 дней ->date('colname', '=', ['older' => '7 DAY']); выборка за 2018 год ->date('colname', '=', ['year' => '2018']); выборка за определенное число ->date('colname', '=', '2020-02-15'); выборка за последние 3 часа ->date('colname', '>=', ['interval' => '3 HOUR']);
Метод join()
join($joinProps, $joinQuery); - метод для присоединения к запросу выборку по другой таблицы $joinProps - массив, параметры присоединения таблицы. Массив может содержать три значения: - наименование столбца левой таблицы - наименование столбца правой таблицы - указание типа объединения таблиц, необязательно. Может быть LEFT, RIGHT, INNER $joinQuery - query-объект таблицы, данные из которого соединяются с основным запросом.
Пример использования join для двух таблиц:
$data = RQ::tbl(new QueryTableOne()) ->select([ 'colname1', 'colname2' ]) ->where([ 'colname1' => 10, 'colname2__in' => ['value1', 'value2'] ]) ->join(['colname1', 'colname3'], RQ::tbl(new QueryTableTwo()) ->select([ 'colname3', 'colname4' ]) ->where([ 'colname4' => 'value3' ]) ) ->get_results();
Как можно заметить в результате работы данного запроса будут выбраны данные из двух таблиц, каждая из которых имеет свои условия выборки.
Методы получения данных
get_var($cache = false) - получение определенного значения, указанного в массиве метода select() get_col($cache = false) - получение массива значений определенного столбца, указанного в массиве метода select() get_row($cache = false) - получение данных определенной строки, согласно переданных условий get_results($cache = false) - получение массива данных по всей таблице, согласно переданных условий get_count($cache = false) - получение значения подсчета строк результата на основе уже сформированного query-объекта get_max($cache = false) - получение максимального значения внутри определенного столбца, указанного в массиве метода select() get_min($cache = false) - получение минимального значения внутри определенного столбца, указанного в массиве метода select() get_sum($cache = false) - получение суммы значений внутри определенного столбца Каждый из методов выше может принимать true или false для помещения полученного результата в кеш WP. get_sql() - получение сформированного запроса к БД на основе уже сформированного query-объекта
Работа с массивом аргументов
Все это конечно хорошо, скажете вы, но как же быть с формированием запроса на основе массива? Ведь по сути он удобен и необходим, например, для его передачи в функцию, внутри которой и строиться запрос, например как это было тут:
function rcl_get_feeds( $args = false ) { $feeds = new Rcl_Feed_Query(); return $feeds->get_results( $args ); }
Спешу успокоить, специально под эту задачу класс содержит метод parse(), который может принимать массив для указания условий выборки практически идентичный старому принципу. Выглядит это так:
function pfm_get_groups( $args = false ) { return RQ::tbl( new PrimeGroups() )->parse( $args )->get_results(); }
или
function pfm_count_groups( $args = false ) { return RQ::tbl( new PrimeGroups() )->parse( $args )->get_count(); }
Принимаемый массив $args соответствует порядку формирования массива из предыдущей статьи о классе Rcl_Query с той лишь разнице, что ключом для массива полей к выборке теперь будет не fields, а select, например:
array( 'admin_id' => 1, 'group_status' => 'closed', 'select' => array( 'ID', 'group_users', ), 'orderby' => 'group_users', 'order' => 'DESC', 'offset' => 5, 'number' => 3 )
Вот такой получился краткий экскурс в функциональные возможности нового Rcl_Query API.
К радости тех, кто использовал Rcl_Query по старому варианту после обновления все будет работать как прежде, но кто знает, что произойдет через полгода? Поэтому настоятельно рекомендуется все запросы перевести на новые рельсы.
Ну и на десерт, вот вам пример построения сложного запроса из реального проекта старым и новым способом https://pastebin.com/xsMZU1bJ. Как говориться: Почувствуй разницу)
Всем чистого кода!
И это я хотел сегодня отдохнуть, теперь буду разбираться 🙂
Безоговорочное спасибо!
Ну если сразу не разберешься - не беда, может появится повод по этому вопросу стрим провести)
О да, стрим хорошая мысль, но в будущем)
1. Можно ли будет теперь жоинить таблицы с условием WHERE table.col_name IS NULL? Например мне это в ачивках надо было - взять все достижения, которые юзер не получил.
2. Теперь можно юзать ORDER BY FIELD?
3. Я правильно понял select(['uid' => 'user_id']) это SELECT user_id as uid? А то логически кажется что это должно значить SELECT uid as user_id но видимо все наоборот
4. Для чего указывать столбцы с сериализоваными данными? ведь есть функция maybe_unserialize?
1. думаю, да, должно получиться, по крайней мере where позволяет указать условие:
'colname__is' => 'NULL'
2. сейчас метод orderby не поддерживает ORDER BY FIELD, но можно использовать метод orderby_string:
->orderby_string('ORDER BY FIELD(id,1,2)')
3. да, все верно, интуитивно хочется чтобы alias указывался в значении, но он указывается в ключе, это сделано для единого подхода при использовании подзапросов, так как там alias можно указать только в ключе, например:
'as_colname' => RT::tbl(new TableQuery())
4. всего лишь для удобства, если указать, то при получении данных они будут десериализованы автоматически
Как сделать select distinct?
Есть отдельный метод distinct(), работаешь также как с методом select()
Тут опечатка BEETWEN? Правильно же between
На гитхабе ничего не найдено по разпросу BEETWEN, значит да - просто тут опечатка