Друпал предоставляет свои средства для доступа к базе данных. Это, во-первых, позволяет не зависеть от конкретного типа СУБД, а во-вторых, защититься от SQL инъекций. Самая первая функция, о которой следует узнать при работе с базой — db_query().
Начну, пожалуй, с примера, в стиле которого пишут почти все начинающие друпаллеры:
<?php
/**
* Пример 1 - небезопасный
* Пример должен отобразить список заголовков нод типа $type (например, поступающего из поля формы)
*/
$result = db_query("SELECT nid, title FROM node WHERE type = '$type'");
$items = array();
while ($row = db_fetch_object($result)) {
$items[] = l($row->title, "node/{$row->nid}");
}
return theme('item_list', $items);
?>
В этом примере сразу несколько вещей в корне неправильны.
Псевдонимы названий таблиц
Название таблиц следует заключать в фигурные скобки, а также присваивать им псевдонимы, которые рекомендуется всегда использовать при обращении к колонкам. Измененный вызов будет выглядеть так:
<?php
$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '$type'");
?>
Что нам это даст? Это обеспечит простоту обработки таблиц с префиксами. То есть, если у вас все таблицы в базе называются "pr_node", "pr_users" и т.д., Друпал автоматически будет подставлять корректные префиксы к таблицам, заключенным в скобки. Указание псевдонимов при этом избавит от надобности использовать фигурные скобки больше одного раза.
Фильтрация аргументов
Отсутствует фильтрация аргументов запроса. Это прямой путь к SQL инъекции. Если в $type
окажется значение story' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1/*
, то весь запрос будет уже таким:
что позволит мошеннику завладеть айдишниками сессий, и в свою очередь, при создании корректной куки сессии, получить прямой админский доступ к сайту.
Защититься от этого довольно просто, используя параметризацию запроса. При формировании запроса, Друпал использует синтаксис функции sprintf. В строке запроса вставляются заглушки, которые заменяются параметрами, которые идут отдельно. При этом параметры проходят проверку и экранирование, так что вы можете забыть об инъекциях, используя данный подход. Вот некоторые примеры:
<?php
db_query("SELECT n.nid FROM {node} n WHERE n.nid > %d", $nid);
db_query("SELECT n.nid FROM {node} n WHERE n.type = '%s'", $type);
db_query("SELECT n.nid FROM {node} n WHERE n.nid > %d AND n.type = '%s'", $nid, $type);
db_query("SELECT n.nid FROM {node} n WHERE n.type = '%s' AND n.nid > %d", $type, $nid);
?>
Список заменителей:
- %d - для целых чисел (integers)
- %f - для чисел с плавающей запятой, т.е. дробных (floats)
- %s - для строк (однако, обратите внимание, что в запросе, вокруг строки выставляются кавычки)
- %b - двоичные данные (не нужно оборачивать в кавычки)
- %% - заменяется на % (например, для
LIKE %monkey%
)
Для конструкций IN (... , ... , ...)
, используйте функцию db_placeholders(), которая создаст нужную последовательность заменителей, по заданному массиву параметров, например:
<?php
$nids = array(1, 5, 449);
db_query('SELECT * FROM {node} n WHERE n.nid IN ('. db_placeholders($nids) .')', $nids);
?>
Если вы используете модуль Devel, у вас есть очень простой способ получения конечных запросов для отладочных целей. Просто вызовите функцию
db_queryd()
с точно такими же параметрами, как вы вызываетеdb_query()
.
Теперь, наш запрос будет выглядеть так:
<?php
$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", $type);
?>
Ранжирование результатов запроса
Наш пример на большом сайте выведет большущий список нодов. Что если нам можно ограничиться всего первым десятком? Первым позывом будет использовать SQL конструкцию LIMIT, например
и вроде бы все хорошо, но на Postgree SQL этот код приведет к ошибке, так как с этим сервером управления, вам нужно использовать конструкцию OFFSET 0 LIMIT 10
. А еще на каком-нибудь Оракле, синтаксис опять другой. Что же делать?
Ответ — использовать db_query_range() для лимитирования количества результатов запроса. Его использование аналогично db_query, за исключением того, что в после всех аргументов, вам нужно указать два параметра — номер первой строки, и количество результатов. Наш запрос преобразится в следующее:
<?php
// выведет первых 10 результатов
$result = db_query_range("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", $type, 0, 10);
?>
И на последок, если вам ко всему еще нужен постраничный вывод, используйте функцию pager_query(). Она отличается от db_query_range()
наличием всего одного необязательного параметра, о котором вы можете почитать на странице документации. С этой функцией вывод листалки страниц прост как дважды два:
<?php
/**
* Пример 2 - безопасный, с листалкой
*/
// изменяем сам запрос
$result = pager_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", $type, 10, 0);
// обратите внимание на смену очередности параметров. Здесь 10 это кол-во результатов,
// а 0 - номер стратовой страницы, а не результата, как было в db_query_range
$items = array();
while ($row = db_fetch_object($result)) {
$items[] = l($row->title, "node/{$row->nid}");
}
$output = theme('item_list', $items);
// добавляем листалку
$output .= theme('pager');
return $output;
?>
Как видите, всего две строчки изменений. Всю рутину по подхватыванию текущей страницы, обработке и т.д. полностью берет на себя Друпал.
Возможность изменения запроса модулями
Довольно часто имеет смысл предоставить другим модулям возможность повлиять на ваш запрос. В Друпале это реализуется связкой функции db_rewrite_sql(), и реализациями хука hook_db_rewrite_sql() в модулях. Наш запрос будет выглядеть так:
<?php
$result = pager_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", 'n', 'nid'), $type, 10, 0);
?>
а вот и пример реализации хука, для того, чтобы у вас было представление, что происходит:
<?php
// Модуль отсеет все ноды, авторы которых сутки не были на сайте
function my_module_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
switch ($primary_field) {
case 'nid':
if ($primary_table == 'n') {
$return['join'] = "LEFT JOIN {users} u ON $primary_table.uid = u.uid";
$return['where'] = 'u.login > '. time() - 60 * 60 * 24;
}
return $return;
break;
}
}
?>
Возвращенные из хука 'join' элементы, будут прикреплены к нашему запросу, 'where' — добавлены к списку условий, и наш запрос после обработки будет таким:
После этого, он, собственно, поступит в pager_query()
и будет обработан как обычно.
Финальный код примера
<?php
/**
* Пример 3 - безопасный, с листалкой и возможностью перезаписи запроса
*/
// добавляем db_rewrite_sql
$result = pager_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", 'n', 'nid'), $type, 10, 0);
$items = array();
while ($row = db_fetch_object($result)) {
$items[] = l($row->title, "node/{$row->nid}");
}
$output = theme('item_list', $items);
$output .= theme('pager');
return $output;
?>
Полезные ссылки
Для поддержки на Хабре (черт, в плюсе от главной, докиньте же голосов
Комментарии
Постарался описать по максимуму обо всем часто используемом. Теперь можно смело тыкать нубов в ссылку
+1.
Александр, вы скоро наберёте материал на издание своей книги-)
Это точно. Очень просто и понятно все расписано. Сейчас же займусь пейджером для запросов.
Спасибище огромное!
Реально полезно и доступно написано, а главное акцент хороший выбрали - "безопасный код".
Если будете издавать книгу, то я первый в числе покупателей
Вплотную занимаемся обороной?
Хочу сказать пару слов по поводу пейджера.
<?php
pager_query($query, $limit = 10, $element = 0, $count_query = NULL)
?>
Параметр $count_query должен в себе содержать отдельный запрос к базе, который даст общее количество строк результата запроса.
Здесь есть два аспекта. Если этот параметр не указывать, то это подходит только для простых запросов, которые не включают в себя множественные джоинты таблиц.
Но это всего лишь пол беды.
Вторая половина, это то, что создается дополнительный запрос к базе на предмет подсчета результата (я все оптимизацией болею)(нервно хихитает).
Где то я читал, что есть специальная команда, которая выводит общее количество строк результата запроса без сканирования таблиц. Сейчас найти ссылочку не могу. Чуть позже размещу эту инфу.
Да, я конечно же в курсе, просто не стал уводить слишком от первоначальной темы, кому надо, тот найдет уже сам А книга да, в планах есть. Готовтесь к семерке.
Вячеслав, вы вероятней всего говорите об FOUND_ROWS()
Вот ваш код
$count_query = "SELECT FOUND_ROWS()";
$result = pager_query($select_query, $type, 10, 0, $count_query);
Хотя, используя это, вы и получаете выигрыш в прорисовке первой странички, но в общем будет проигрыш, так как судя по нескольким комментам с друпал.орга, здесь не будет работать мускуловский кеш запросов (который бы закешировал нормальный COUNT запрос полсе первого раза). Кроме того, вы естественно, теряете в переносимости кода.
спасибо, как раз думал о том, что пора начинать пеуджинг использовать
хотелось бы еще увидеть подобную статью про программирование своих фильтров
Так зачем же ждать статью? Просто берите filter.module и препарируйте Это более захватывающе чем статья, честное слово
Друпал, к сожалению, будет некорректно работать с функцией found_rows(), т.к. в [ru-api=pager_query]pager_query()[/ru-api] сначала выполняется $count_query запрос, а только потом — основной
zhylik, да, вы правы, не все так ажурно. Как бы там ни было, даже если сделать аналог pager_query() с нужной очередностью, мое замечание на счет производительности никуда не денеться.
Спасибо за отличную статью!
Безопасным код будет, только если правильно работать с базами данных. Друпал работает с базами неправильно, к сожалению - это и наследство старого MySQL, и некоторые другие факторы.
Вопрос: можно ли как -то работать с базами, указывая запрос в форме
SELECT nid, title FROM node WHERE type = ?
Теория и практика говорит, что это самый правильный и безопасный способ, но может ли Drupal его поддерживать?
В смысле? Без алиасов?
Спасибо за полезный пост. А кстати - когда выйдет 7-ка ?
Далеко не всегда один запрос работает быстрее чем тот же поделенный на два простых.
Связано это чаще всего с тем каким образом mysql пытается оптимизировать запрос и что именно сейчас у него лежит в кеше.
Типичный пример
SELECT COUNT(*) FROM message WHERE uid = $user_id
и
SELECT COUNT(uid) FROM message WHERE uid = $user_id
Первый запрос работает в разы быстрее чем второй. Потому как для работы первого достаточно пройтись по индексу uid и посчитать кол во записей. А индексы у нас упорядочены и в большинстве случаев лежат в кеше.
Потому бывают ситуации когда 20! запросов могу работать быстрее чем один получающий тот же результат что и исходные 20.
Второй аспект,
Формат таблиц myisam
Если сложный селект в себе использует часто обновляемые таблицы то Вы можете получить серьезный проигрыш в производительности нежели разбив тот же селект на два более простых.
И так далее. Так что еще раз повторяю, Один запрос далеко не всегда быстрее чем даже 20 более простых.
Demimurych, наверное, имелось в виду, что 2-ой запрос быстрее?
Наверное да-)
Нет. Не ошибся.
Именно первый запрос быстрее. Именно SELECT COUNT(*) FROM message WHERE uid = $user_id
Потому что этот запрос не приводит к выборе записи из базы. В нем задействован ТОЛЬКО индекс uid.
В запросе же
SELECT COUNT(uid) FROM message WHERE uid = $user_id mysql кроме идекса так же выбирает и всю запись из таблицы для того что бы извлечь от туда uid.
И это если я ничего не забыл, еще не учитывая того что в mysql select count(*) специльно оптимизирован.
но он выбирает все поля.
Странно...
http://dev.mysql.com/doc/refman/5.0/en/group-by-functions.html#function_...
COUNT(*) is optimized to return very quickly if the SELECT retrieves from one table, no other columns are retrieved, and there is no WHERE clause. For example:
mysql> SELECT COUNT(*) FROM student;
В данном случае * не означает выборку всех полей.
Так в мануале, явно указаны условия такой быстроты:
и
т.е. нет условия ГДЕ, как вашем примере:
______
/me потопал на mysql.com
Цитату с mysql я вам привел для демонстраци того что такое count(*) а так же и для того что именно для count(*) есть персональная оптимизация для myisam таблиц. Да работающая в определенных условиях но работающая ОЧЕНЬ быстро.
То есть сказанное не отменяет того о чем я говорил. А дополняет.
во всем остальном Вам поможет EXPLAIN и/или собственные замеры на какой нить более менее приличной(по обьему) базе.
поймите главное, при использовании count(*) mysql нет необходимости выбирать записи из базы данных. Ей достаточно просто посчитать индекс.
Который чаще всего лежит в кеше.
В отличии от count(uid) которое повлечет за собой выборку записи из базы, которая очень вероятно может в кеше не оказаться, а следовательно приведет к дисковым операциям, которые как Вы понимаете самые медленные.
Ок, я понял вас.
<?php
//так не работает
$result = pager_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", 'image', 10, 0 );
//так работает
$result = pager_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = 'image'", 10, 0 );
?>
хм.. ничего не понимаю
<?php
pager_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", 11, 0, NULL, 'image' );
?>
оказалось так
Спасибо за статью!