{{LiveInternetCount: empty}}
Джино: хостинг и веб-сервисы

Система Orphus
{{LiveInternetLogo: empty}}
Russian version
Добавить на Del.icio.us
English version
Добавить на Digg.com

 dkLab | Конструктор | DbSimple: лаконичная работа с различными СУБД | Подробности 

Карта сайта :: Форум «Лаборатории» :: Проект «Денвер»
Проект «Orphus» :: Куроводство: наблы :: Конструктор


2006-03-03
Обсудить на форуме

Принять участие в разработке библиотеки/утилиты можно на GitHub.

Лирическое отступление 
Внимание! Текущая версия библиотеки DbSimple — 2.x. Отличия от версии 1.x: еще более упрощенный интерфейс (совместим с DbSimple 1.x не полностью!); поддержка условных макроподстановок ({}-блоков) в SQL-запросах; кэширование запросов.

В PHP встроено множество функций для работы с самыми разнообразными СУБД. Однако у четвертой версии языка есть большой недостаток: все эти функции различаются между собой как по числу принимаемых параметров, так и по идеологии работы с базой данных. Например, функция mysql_query() (для MySQL) принимает идентификатор соединения с БД во втором параметре, а аналогичный вызов ibase_query() (для InterBase/FireBird) — в первом. При использовании «прямых» обращений к таким функциям в большинстве случаев можно даже и забыть о том, чтобы перенести сайт на другую СУБД, пусть даже диалекты SQL у старой и новой базы почти совпадают и удовлетворяют стандарту SQL'92.

Для «выравнивания» различий и сведения всех обращений к различным СУБД в единый интерфейс существует довольно большое число библиотек. Самые популярные из них, пожалуй, — библиотека PEAR DB и ADOdb; PHP версии 5 также поддерживает PDO. Все библиотеки умеют работать с более чем десятком различных СУБД, при этом интерфейс выполнения запросов во всех случаях остается одним и тем же.

Что нужно скрипту от СУБД?

Чайник 

Вы можете спросить: зачем еще одна библиотека, раз их и так написано множество? Штука в том, что скриптам нужны далеко не все операции, предоставляемые PHP (или, например, библиотеками PEAR и ADOdb) для работы с любой СУБД. С другой стороны, по-настоящему необходимые операции (см. ниже) должны быть реализованы так, чтобы ими было удобно пользоваться, а этого как раз имеющиеся библиотеки лишены.

Ниже я перечислю тот минимум, который, по моему мнению, необходим скриптам для «комфортной» работы с произвольной СУБД. (Звездочками отмечены возможности, которые либо отсутствуют в PEAR DB, ADOdb и PDO, либо же реализованы неудовлетворительно.)

  • Интерфейс должен быть максимально простым, ненавязчивым и неизбыточным (*). Это очень важное требование, потому что работа с SQL порой занимает львиную долю кода PHP-скриптов, и от того, насколько она будет «прозрачной», зависит энтузиазм программиста.
  • Поддержка placeholder-ов: для вставки данных в строку SQL-запроса используются специальные маркеры, например, "?", а сами данные передаются позже: query('SELECT * FROM tbl WHERE id=?', $id).
  • Абстрагирование от параметров подключения к СУБД: подключение к СУБД использует единообразную строку коннекта (DSN — Data Source Name): connect("mysql://login:password@database?options").
  • Логирование запросов к СУБД (*):
    • какие запросы с какими параметрами (значениями placeholder-ов) выполнялись в ходе всей работы скрипта;
    • какие результаты вернули запросы (для небольших однострочных и скалярных выборок — явно, для многомерных — число строк результата);
    • какая строка программы вызвала тот или иной SQL-запрос;
    • сколько времени заняло выполнение запроса.
  • Механизм обработки ошибок в SQL-запросах (*): хотя бы с применением callback-вызова обработчика ошибок, которому передается исчерпывающая информация о контексте запроса.
  • Функции простейшей выборки из базы с применением placeholder-ов:
    • mixed select(string $query [,$arg1...]): выборка двумерного массива (список строк);
    • hash selectRow(string $query [,$arg1...]): выборка однострочного результата запроса (одна строка);
    • array selectCol(string $query [,$arg1...]): выборка одноколоночного результата запроса (один столбец);
    • scalar selectCell(string $query [,$arg1...]): выборка скалярного результата запроса (одна ячейка)
    • mixed selectPage(int &$total, string $query [,$arg1...]): выборка ограниченного двумерного массива с занесением общего числа записей в переменную;
    • mixed query(string $query [,$arg1...]): вызов не-SELECT запроса; для автоинкрементных полей (MySQL) возвращает ID вставленной записи.
  • Простейшее форматирования двумерной выборки (*):
    • в виде ассоциативного массива (возможно, многомерного) с ключами, взятыми из определенного столбца выборки;
    • в виде дерева, когда идентификатор элемента берется из одного столбца, а идентификатор его родителя — из другого.
  • Списковые и ассоциативные placholder-ы (*): чтобы можно было писать что-то типа query('SELECT * FROM tbl WHERE id IN(?a)', array(1,2,3)), а также короткие UPDATE-выражения, в которых все данные берутся из ассоциативного массива.
  • Поддержка транзакций: даже если СУБД не поддерживает транзакции, интерфейс должен быть единым.
  • Поддержка объектных BLOB-полей (*): если в какой-то BLOB-ячейке хранится запись размером, предположим, 500М, должна быть возможность извлечь ее оттуда по кускам, считывая участок за участком в цикле.

Недостатки PEAR DB и ADOdb

Обратите внимание, сколько в приведенном списке пунктов, поддержка которых в PEAR DB и ADOdb, как мне кажется, находится на неудовлетворительном уровне. В то же время, эти две библиотеки поддерживают еще множество возможностей, которые в данный список не вошли в виду их малой практической целесообразности и большой избыточности:

  • громоздкая сортировка ошибок СУБД по их типам;
  • различные форматы результата выборки, отличные от ассоциативных массивов (например, выборка в виде объектов PHP);
  • вынесение в интерфейс операций prepare и execute (это излишество, ибо функции выборки, заметив, что запросы поступают однотипные, и сами могут решить, что им стоит сделать 1 раз prepare, а потом — 100 раз execute);
  • разделение в интерфейсе операций «выполнить запрос» и «получить результат запроса»;
  • отдельная обработка SELECT-запросов с LIMIT (MySQL) или FIRST ? SKIP ? (InterBase/FireBird);
  • работа с последовательностями (поддерживается далеко не всеми СУБД, а эмуляция весьма "кривая").

Лирическое отступление 
К счастью, используя PEAR DB и ADOdb, можно написать собственную «обертку», которая будет реализовывать всю функциональность из приведенного выше списка. Вероятно, в будущем она и будет написана для DbSimple.

Сведя все воедино, можно сказать, что основная масса популярных библиотек абстракции от СУБД имеют три недостатка.

  1. Весьма большой объем PHP-кода, который нужно подключать к скрипту. Например, минимальный набор файлов PEAR DB для работы с MySQL занимает порядка 150 КБ (5000 строк кода), для ADOdb — 200 КБ и 7000 строк. Правда, это все включая комментарии, которых в коде очень много, но все равно.
  2. Единый интерфейс оказывается уж очень «многословным» и чрезмерно перегруженным ненужными (в большинстве случаев) функциями, в которых немудрено запутаться. Наоборот, некоторые возможности, которые применяются в скриптах очень часто (см. ниже), ими напрямую не поддерживаются. Иными словами, определенный слой абстракции данные библиотеки поддерживают, но это не совсем тот слой, с которыми бы «хотели» работать скрипты: он слишком низкоуровневый.
  3. Отладочные функции находятся в зачаточном состоянии, а также работают не на том слое абстракции, на котором их бы «хотели видеть» скрипты. Например, самая естественная отладочная информация — какие запросы с какими параметрами выполнялись в ходе всей работы скрипта, какие результаты они вернули (хотя бы число строк), где именно в PHP-коде вызывались и сколько времени заняли. Для всего этого поддержка в PEAR DB и ADODB отсутствует.

Библиотека DbSimple

Вашему вниманию предоставляется новая библиотека работы с СУБД — DbSimple. Вот ее основные характеристики.

  • Исключительно простой и лаконичный интерфейс, который очень удобно использовать в скриптах.
  • Условные макроподстановки в теле SQL-запросах ({}-блоки), позволяющие делать динамически изменяемыми даже очень сложные запросы без ущерба читабельности кода.
  • Выполнены все требования списка, приведенного выше. Чуть ниже будут даны подробные примеры по каждому из пунктов.
  • Библиотека весьма компактна: например, объем кода для работы с MySQL — 40 КБ (1300 строк); в основном, как водится, комментарии.
  • В настоящий момент поддерживаются три популярные СУБД: MySQL, PostgreSQL и InterBase/FireBird. Поддержка остальных СУБД может быть добавлена без каких-либо проблем. Возможно даже добавление универсальной поддержки для PEAR DB, ADOdb или PDO, однако это, конечно, подорвет компактность библиотеки, но без ущерба остальным достоинствам.
  • Код библиотеки оформлен в соответствии с PEAR Coding Standards.

Чайник 

Обращаю особое внимание на то, что DbSimple намеренно не занимается «выравниваниием диалектов» SQL в различных СУБД (что частично пытаются делать PEAR DB и ADOdb). Она лишь позволяет обращаться к ним через единый стандартизированный и очень удобный интерфейс. Что такое «выравнивание диалектов»? Например, в MySQL запрос с ограничением выглядит как "SELECT ... LIMIT m, n", в PostgreSQL — как "SELECT ... OFFSET x LIMIT y", а в FireBird — "SELECT FIRST a SKIP b ...". Это и есть «разные диалекты». Вы должны сами решить, какую СУБД используете, и соответствующим образом составлять запросы; DbSimple вам в этом не поможет (по крайней мере, это не тот слой абстракции, на котором осуществляется выравнивание диалектов). Также DbSimple не является слоем получения стандартизированных метаданных БД (информации о структуре таблиц, полей, индексов и т. д.).

DSN-подключение к БД

В простейшем случае код подключения к БД будет выглядеть примерно так:

Листинг 1
// Подключаем библиотеку.
require_once "DbSimple/Generic.php";
// Устанавливаем соединение.
$DB = DbSimple_Generic::connect("mysql://Логин:Пароль@Хост/База");
// Дальше работаем с соединением (или текущей транзакцией) $DB. 
// Например: $DB->select(...).

Формат DSN-строки тот же самый, что используется в PEAR DB. Для других СУБД DSN-строка может выглядеть более сложно. Спецификация параметров, следующих после "?" в DSN, определяется конкретной библиотекой поддержки той или иной СУБД. Вот пример для InterBase/FireBird: ibase://Логин:Пароль@Хост/База?ROLE=...&CHARSET=win1251&BUFFERS=0&DIALECT=3.

Статическая функция DbSimple_Generic::connect работает так:

  1. Разбирает DSN-строку, выделяя имя СУБД.
  2. Пытается подключить файл ИмяСУБД.php, находящейся в пределах директории библиотеки DbSimple.
  3. Создает объект класса DbSimple_ИмяСУБД, который и возвращается.

Например, в нашем случае в $DB будет записан объект класса DbSimple_Mysql.

Лирическое отступление 
В случае, если возникнет ошибка подключения, никакое сообщение выведено не будет, и работа программы не завершится. Ошибка будет сгенерирована только при назначении функции-обработчика ошибок. Это может показаться неудобным, однако следующий раздел расставит все на свои места.

Обработка ошибок

Нечего даже и думать работать с базой, пока вы не убедитесь, что у вас есть удобный инструмент для обработки ошибок. Ошибки могут возникать на самых разных местах, таких как: подключение к БД, выполнение SQL-запросов, старт транзакции и т. д. Все эти ситуации обрабатываются библиотекой DbSimple единообразно — при помощи запуска пользовательского обработчика ошибок (error handler):

Листинг 2
<?php ## Подключение к БД.
require_once "../../lib/config.php";
require_once "DbSimple/Generic.php";

// Подключаемся к БД.
$DATABASE = DbSimple_Generic::connect('mysql://test:test@localhost1/non-existed-db');

// Устанавливаем обработчик ошибок.
$DATABASE->setErrorHandler('databaseErrorHandler');

// Код обработчика ошибок SQL.
function databaseErrorHandler($message, $info)
{
    // Если использовалась @, ничего не делать.
    if (!error_reporting()) return;
    // Выводим подробную информацию об ошибке.
    echo "SQL Error: $message<br><pre>"; 
    print_r($info);
    echo "</pre>";
    exit();
}
?>

Обратите внимание на то, что мы выполняем $DB->setErrorHandler('databaseErrorHandler') сразу же после того, как установили подключение к БД (создали объект $DB). Как уже говорилось выше, если во время вызова connect() произошла ошибка, она нигде не отобразится, но последующий вызов setErrorHandler() ее обработает стандартным способом.

Пример ошибки подключения

Предположим, мы допустили опечатку и попытались присоединиться к хосту localhost1, а не localhost. В этом случае во время установки обработчика ошибок будет выведено следующее сообщение:

Листинг 3
SQL Error: Unknown MySQL Server Host 'localhost1' (11001) at .../connect.php line 17
Array
(
    [code] => 2005
    [message] => Unknown MySQL Server Host 'localhost1' (11001)
    [query] => mysql_connect()
    [context] => .../connect.php line 17
)

Чайник 

Конечно, при желании вы можете оформить сообщение об ошибке гораздо аккуратнее. Вы можете даже записать его в логи сервера, а не выводить на экран.

Как видите, ошибка подключения не была «потеряна». Отображается: код ошибки, подробное сообщение, ошибочный SQL-запрос (в нашем случае это не запрос, а вызов функции подключения), а также имя файла и номер строки, в которых была вызвана библиотека DbSimple.

Ошибки и исключения

Помимо вывода диагностики, какова должна быть реакция скрипта на возникновение ошибки в SQL-запросе? Существует как минимум два варианта:

  1. Вернуть какое-нибудь недопустимое значение вместо результирующих данных. Так поступает PEAR DB: если запрос завершается неудачно, возвращается признак ошибки — объект класса PEAR_Error, и вы должны проверять наличие этого признака явно.
  2. Немедленно завершить работу программы.

Первый способ неудобен тем, что расставлять повсюду в скрипте проверки весьма утомительно. Возникает соблазн этого не делать, а значит, потенциальная возможность «упустить» ошибку. Моя практика показала, что в подавляющем большинстве случаев самое логичное, что можно сделать при возникновении SQL-ошибки, — это немедленно завершить работу скрипта. Ведь код, выполняющий SQL-запрос, ожидает получить его результат и чаще всего не готов к тому, что «кина не будет».

Но, конечно, бывают и ситуации, когда ошибку необходимо «молча проглотить». Самый распространенный пример: нам нужно обновить некоторую запись в таблице, если она там уже существует, или добавить новую — если ее там нет. С использованием DbSimple и того обработчика ошибок, который мы написали выше, это будет выглядеть примерно так:

Листинг 4
// Обратите внимание на "@"!
if (!@$DB->query('UPDATE tbl SET field=? WHERE id=1', $field)) {
  // Здесь идет реакция на ошибку, если она возникла. 
  // Контекст ошибки можно получить через $DB->error.
  $DB->query('INSERT INTO tbl(id, field) VALUES(1, ?)', $field)
}

Взгляните еще раз на код callback-функции databaseErrorHandler() выше. Применение инструкции if (!error_reporting()) return заставляет обработчик не завершать работу скрипта (и не выводить диагностику), если вызову SQL-запроса предшествовал оператор PHP @. Таким образом, мы имеем возможность задействовать оба варианта реакции на ошибку, причем по умолчанию используется тот вариант, который оказывается самым логичным в большинстве случаев (второй).

Лирическое отступление 
Конечно, идеальным решением было бы не завершать скрипт по exit(), а генерировать исключение инструкцией throw, которое потом будет перехвачено в блоке catch. Однако исключения поддерживаются только в PHP5. Если вы используете PHP5, вы можете доработать функцию-обработчик ошибки так, чтобы она генерировала исключение вместо завершения работы всей программы. Подробнее об исключениях читайте в документации PHP.

Основные placeholder-ы

Чтобы избежать самой популярной среди скриптописателей проблемы с безопасностью — SQL Injection, существует хороший способ: использовать в запросах placeholder-ы и переложить обработку «небезопасных» данных на плечи библиотеки работы с СУБД (см. статью на эту тему). Иными словами, там, где «плохой» программист пишет «дырявый» код:

Листинг 5
$DB->select("SELECT * FROM tbl WHERE a='$a' AND b='$b'");

«хороший» применит placeholder-ы:

Листинг 6
$DB->select('SELECT * FROM tbl WHERE a=? AND b=?', $a, $b);

и, тем самым, гарантированно избавит себя от дыр вида SQL Injection. Основными для DbSimple являются placeholder-ы ? (вставка строки) и ?a (вставка списка или массива). Они используются в подавляющем большинстве случаев.

Строковой (бинарный): ?

Это самый простой вид placeholder-а, используемый в подавляющем большинстве случаев. Вставляемое на его место значение обрамляется апострофами ('), при этом все символы, которые СУБД считает служебными, экранируются в соответствии с правилами этой СУБД (например, в MySQL перед апострофами вставляется \, а в FireBird - апострофы удваиваются).

Листинг 7
$DB->select('SELECT * FROM tbl WHERE a=?', "test'string");
// MySQL: SELECT * FROM tbl WHERE a='test\'string'
// FireBird: SELECT * FROM tbl WHERE a='test''string'

Лирическое отступление 
Правила экранирования задаются для каждой СУБД отдельно. За это отвечает виртуальный метод escape().

Правильнее было бы назвать данный placeholder не строковым, а бинарным, т.к. с его помощью в БД можно вставлять произвольные бинарные данные (в том числе — картинки, исполняемые файлы и т. д.).

Если значение подставляемого параметра равно null, вместо обрамления его апострофами вставляется ключевое слово SQL NULL. Это же верно и для всех остальных типов placeholder-ов.

Листинг 8
$DB->query('UPDATE tbl SET a=?', null);
// MySQL: UPDATE tbl SET a=NULL

Списковый/ассоциативный: ?a

Данный вид placeholder-ов удобно использовать для составления IN-выражений в SQL — при условии, что в программе имеется список с перечисленными значениями:

Листинг 9
$ids = array(1, 101, 303);
$DB->select('SELECT name FROM tbl WHERE id IN(?a)', $ids);
// SELECT name FROM tbl WHERE id IN(1, 101, 303)

Помните, что в случае передачи пустого списка вы получите ошибочный SQL-запрос:

Листинг 10
$ids = array();
$DB->select('SELECT name FROM tbl WHERE id IN(?a)', $ids);
// SELECT name FROM tbl WHERE id IN() - ОШИБКА!

Если в качестве параметра передан ассоциативный массив (ключи массива не целочисленные, а строковые), DbSimple заменяет ?a набором пар ключ=значение. Это удобно использовать в UPDATE-запросах:

Листинг 11
$row = array(
  'id'   => 10,
  'date' => "2006-03-02"
);
$DB->query('UPDATE tbl SET ?a', $row);
// MySQL: UPDATE tbl SET `id`='10', `date`='2006-03-02'

Дополнительные placeholder-ы

Для удобства работы DbSimple поддерживает еще целый набор видов placeholder-ов, которые будут описаны далее. Они используются значительно реже и, как правило, позволяют просто сократить письмо. Если вы думаете, что все это слишком сложно для понимания, — не используйте дополнительные placeholder-ы.

Префиксный: ?_

Часто все имена таблиц, используемых в программе, имеют один и тот же префикс. Например, в форуме phpBB этот префикс, как правило, равен phpbb_, и таблицы называются phpbb_users, phpbb_sessions и т. д. Это делается для того, чтобы в одной базе данных можно было хранить сразу несколько наборов таблиц для разных форумов, избегая конфликтов имен.

Префиксный placeholder заменяется на некоторое фиксированное значение, ранее установленное для объекта базы данных:

Листинг 12
define(TABLE_PREFIX, 'phpbb_'); // с подчерком!
$DB->setIdentPrefix(TABLE_PREFIX); 
...
$DB->select('SELECT * FROM ?_users');
// SELECT * FROM phpbb_users

// Сравните:
$DB->select('SELECT * FROM '.TABLE_PREFIX.'_users');

Чайник 

Если быть точным, ?_ не является обычным placeholder-ом, т.к. для него очередное значение не извлекается из списка параметров, а берется из другого источника. Это скорее удобная макроподстановка.

Идентификаторный: ?#

Ключевые слова SQL, такие как date, int и т. д., не могут использоваться в качестве имен полей и таблиц. Например, у вас не получится создать в таблице столбец с именем date. Тем не менее, многие СУБД предлагают способы, позволяющие все же ссылаться на подобные объекты. Имена идентификаторов следует окружить теми или иными ограничителями:

  • MySQL использует обратные апострофы (backticks): table.`date`.
  • FireBird заставляет применять кавычки: table."date".
  • Microsoft SQL Server использует квадратные скобки: table.[date].

Идентификаторный placeholder заставляет СУБД воспринимать значение как идентификатор:

Листинг 13
$DB->select('SELECT ?# FROM tbl', 'date');
// MySQL: SELECT `date` FROM tbl
// FireBird: SELECT "date" FROM tbl

$DB->select('SELECT ID AS ?# FROM tbl', 'this is ID');
// MySQL: SELECT ID AS `this is ID` FROM tbl
// FireBird: SELECT ID AS "this is ID" FROM tbl

Конечно, использование идентификаторного placeholder-а полностью защищает от уязвимостей вида SQL Injection. Передаваемый параметр обрамляется «идентификаторными кавычками», а если они встречаются в нем самом, то экранируются специфичным для СУБД образом.

Лирическое отступление 
Правила экранирования определятюся тем же самым виртуальным методом escape(). Для экранирования в стиле идентификаторов второй параметр полагается равным true.

Идентификаторно-списковый: ?#

Уже знакомый нам идентификаторный placeholder ?# может принимать в качестве значения не только строку, но также и массив (список значений). Это очень удобно для формирования INSERT-запросов в соответствии со стандартом SQL'92:

Листинг 14
$row = array('id' => 101, 'name' => 'Rabbit', 'age' => 30);
$DB->query('INSERT INTO table(?#) VALUES(?a)', array_keys($row), array_values($row));

В зависимости от того, передаете вы placeholder-у ?# массив или строку, он "развернется" в список идентификаторов или в единственный идентификатор соответственно.

Чайник 

Вы можете спросить, почему же для вставки списка значений используется отдельный placeholder ?a, а для списка идентификаторов — тот же самый ?#. Ответ достаточно прост: применение ? для обработки и скаляра, и списка небезопасно, т.к. данные могут быть получены из формы и представлены злоумышленником в виде массива. Например, можно передать id[]=1&id[]=2 вместо id=1 в QUERY_STRING и "сломать" запрос $DB->select('SELECT * FROM table WHERE id=?', $_GET['id']). В то же время, "фальсифицировать" передачу списка идентификаторов вместо единственного значения практически невозможно.

Целочисленный: ?d

Переданный параметр преобразуется в целое цисло и вставляется без обрамления апострофами. В случае ошибки конвертирования вставляется 0.

Чайник 

Может возникнуть вопрос, зачем нужны целочисленные placeholder-ы, если СУБД и так умеют преобразовывать строки в числа? Например, MySQL конвертирует '10' в 10 при вставке в числовое поле. Оказывается, это верно не для всех существующих в мире СУБД. Кроме того, предложение FIRST ? SKIP ? FireBird (или LIMIT ?, ? MySQL) требует обязательной подстановки чисел, а не строк.

Вещественный (дробный): ?f

Вещественный placeholder можно использовать для передачи дробных (вещественных) чисел в СУБД. В зависимости от локальных настроек для разделения компонент PHP использует либо точку, либо запятую, в то время как стандарт SQL требует обязательного использования точки безотносительно к локальным настройкам. Чтобы не связываться с локальными настройками, просто применяйте дробный placeholder.

Ссылочный: ?n

В большинстве таблиц, с которыми приходится работать, присутствует целочисленный столбец с именем IDпервичный ключ данной таблицы. На этот столбец устанавливают уникальный индекс и "навешивают" auto_increment (MySQL) или триггеры-генераторы (остальные СУБД), чтобы при вставке очередной записи в таблицу она автоматически получала уникальный номер (как правило, отличный от нуля).

На первичный ключ очень удобно ссылаться из другой (или из той же самой) таблицы, такие ссылки называют внешними ключами. Например, у нас может быть таблица forest с полями (ID, PARENT_ID, NAME), определяющая множество деревьев. Чтобы не нарушать ссылочной целостности, корневой элемент каждого дерева должен иметь PARENT_ID=NULL.

Предположим, мы пишем скрипт, который вставляет в forest новую запись. ID родительского узла передается так: http://example.com/tree.php?parent=123. Что делать, когда нам нужно передать NULL в качестве идентификатора родителя?

Листинг 15
$DB->query(
  'INSERT INTO forest(PARENT_ID, NAME) VALUES(?, ?)', 
  ($_GET['parent']? $_GET['parent'] : null), $name
);

Теперь можно передать в GET-параметр parent значение "" или 0 для вставки NULL.

Ссылочный placeholder позволяет немного упростить письмо:

Листинг 16
$DB->query(
  'INSERT INTO forest(PARENT_ID, NAME) VALUES(?n, ?)', 
  $_GET['parent'], $name
);

Подставляемое значение преобразуется в целое число. Если это число равно нулю, то вместо него подставляется NULL, иначе — оно само.

Лирическое отступление 
Ссылочный placeholder крайне удобно применять для "выправления" баз данных MySQL типа MyISAM, если для "узлов-сирот" применялся не NULL в качестве родителя, а обычный 0. К сожалению, такое часто встречается на практике, т.к. MyISAM, в отличие от BDB и InnoDB, не поддерживает ссылочную целостность, а написать PARENT_ID=? вместо PARENT_ID<=>? бывает весьма заманчиво. (См. документацию MySQL на операторы IS_NULL и <=> в отношении NULL-значений.)

«Родные» placeholder-ы базы

Некоторые СУБД (например, InterBase/FireBird) сами поддерживают placeholder-ы. Правда, их набор обычно ограничивается одним-единственным видом: скалярным placeholder-ом "?", применяемым как для строк, так и для целых и дробных чисел:

Листинг 17
ibase_query($link, 'SELECT * FROM tbl WHERE id=?', 100);

Естественно, быстродействие от применения "родных" placeholder-ов базы только увеличивается (нет необходимости преобразовывать данные в строковой SQL-запрос), поэтому DbSimple, где это возможно, применяет встроенные placeholder-ы. Например, их поддержка реализована в модуле работы с InterBase.

Чайник 

Отдельную сложность при работе с "родными" placeholder-ами составляет логирование запросов к СУБД, также поддерживаемое DbSimple. Действительно, в журнал должны идти запросы с уже проставленными параметрами. Поэтому DbSimple при включенном логировании "расширяет" placeholder-ы самостоятельно.

В качестве "родных" placeholder-ов могут использоваться только строковой (?) и численный (?d и ?f) placeholder-ы. Все остальные типы (в том числе списковый, идентификаторный и т. д.) по-прежнему обрабатываются самой библиотекой DbSimple.

Выполнение запросов к БД

Библиотека DbSimple имеет в своем имени слово simple ("простой") потому, что она максимально упрощает процедуру выполнения запросов к базе данных.

Выборка всего результата: select()

Данный метод является самым простым и универсальным. С его помощью вы можете выполнить запрос к базе данных и (если это SELECT-запрос) получить в двумерный массив все строки результата операции.

Листинг 18
$rows = $DB->select(
  'SELECT * FROM ?_users WHERE username LIKE ?', 'к%'
);
foreach ($rows as $numRow=>$row) {
  foreach ($row as $colName=>$cellValue) {
    echo "$numRow: $colName = $cellValue<br>";
  }
}

Если вам не нужны все строки результата, ограничьте выборку стаднартными средствами SQL — например, предложением LIMIT в MySQL:

Листинг 19
$rows = $DB->select('SELECT * FROM ?_users LIMIT 10');
foreach ($rows as $numRow=>$row) {
  ...
}

Лирическое отступление 
Обратите внимание, что промежуточный слой абстракции "объект-результат" не используется, а выборка в любом случае производится полностью, от первой записи до последней. Это сделано совсем не случайно: я имею твердую уверенность, что данный слой абстракции в скриптах совершенно излишен и только запутывает программу. Хотите ограничить выборку или устроить выборочную навигацию по результату (seeking) — используйте для этого средства SQL, а не PHP.

Выборка ассоциативного массива

Результат выборки, который попал в $rows (см. предыдущий пример), является списком массивов. Иными словами, ключи $rows — целые числа, большие либо равные нулю и идущие по порядку.

В ряде случаев оказывается удобным индексировать результат не целыми числами, а ассоциативными значениями, взятыми в одном из столбцов выборки. Например, если мы выбираем пользователей, в качестве ключа может быть использован их primary key в базе (идентификатор).

Чтобы DbSimple сформировал ассоциативный массив, а не список, используйте для столбца фиксированное имя ARRAY_KEY:

Листинг 20
$rows = $DB->select(
  'SELECT user_id AS ARRAY_KEY, * FROM ?_users'
);
foreach ($rows as $userId=>$userData) {
  ...
}
echo $rows[$_REQUEST['uid']]['username'];
echo $rows[$_REQUEST['uid']]['ARRAY_KEY']; // ошибка: нет поля

По наличию столбца с именем ARRAY_KEY библиотека определит, какой формат данных вы ожидаете получить, и произведет соответствующие преобразования. Т.к. этот столбец является служебным, он сам в результат выборки не попадет (см. последнюю строчку примера).

Выборка многомерного массива

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

Листинг 21
$messagesByTopics = $DB->select('
    SELECT 
        message_topic_id AS ARRAY_KEY_1,
        message_id AS ARRAY_KEY_2,
        message_subject, message_text
    FROM 
        ?_message
');

На выходе получится двумерный ассоциативный массив: $messagesByForumsAndTopics[topicId][messageId] = messageData. Поле message_topic_id, объявленное как ARRAY_KEY_1, стало первым индексом массива, а поле message_id (ARRAY_KEY_2) — вторым.

Существует и специальный вариант данного синтаксиса, позволяющий формировать индексы массивов в возрастающем порядке, а не на основе величины, полученной из БД:

Листинг 22
$usersByCity = $DB->select('
    SELECT 
        city_id AS ARRAY_KEY_1, 
        NULL AS ARRAY_KEY_2,
        user_name, user_email
    FROM 
        ?_user
');

В данном примере будет получен массив списков пользователей $usersByCity[cityId][] = userData. Т.е. каждый элемент массива, соответствующий некоторому городу, содержит обычный список записей с данными пользователей. Мы достигли получения обычного списка, передав NULL в качестве ARRAY_KEY_2.

Лирическое отступление 
Вообще, специальными являются все поля вида ARRAY_KEY* (здесь "*" означает "любой текст"). Перед формированием индексов ассоциативного массива эти поля сортируются в алфавитном порядке, так что ARRAY_KEY_1 всегда будет более "ранним" индексом, чем ARRAY_KEY_2.

Выборка связанного дерева

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

Листинг 23
$forest = $DB->select('
  SELECT 
    user_id   AS ARRAY_KEY, 
    parent_id AS PARENT_KEY,
    * 
  FROM ?_users
');

Строго говоря, на выходе в $forest мы получаем не дерево, а лес — набор однокоренных деревьев. Дело в том, что в результатах выборки могут присутствовать сразу несколько элементов, не имеющих родителей. Все такие элементы объявляются вершинами дерева, а их "дети" строятся по правилу: PARENT_KEY "ребенка" равен ARRAY_KEY "родителя".

У каждого элемента результирующего массива, помимо его собственных данных (в нашем случае это * — все поля записи), имеется запись с ключом childNodes. В ней-то и содержится массив всех "детей" текущего элемента (или пустой массив, если это листовая вершина).

Выборка строки: selectRow()

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

Часто бывают случаи, когда выборка гарантировано состоит из одной записи. Предположим, у нас есть ID некоторого объекта, и мы хотим получить данные его полей. Можно для этого воспользоваться методом select(), а потом взять первую строку результата, однако удобнее будет применить метод selectRow():

Листинг 24
$row = $DB->selectRow(
  'SELECT * FROM ?_users WHERE user_id=?', $uid
);
// Теперь в $row - массив вида имяПоля => значениеПоля.

Выборка ячейки: selectCell()

Иногда нам нужны данные в еще более простом формате, чем выдает selectRow(). Например, мы хотим получить имя пользователя, зная его ID, и при этом совершенно не интересуемся всеми остальными его полями. Метод selectCell() подходит здесь как нельзя лучше:

Листинг 25
$userName = $DB->selectCell(
  'SELECT username FROM ?_users WHERE user_id=?', $uid
);
// В $userName - строка, имя пользователя.

Выборка столбца: selectCol()

Последний вид форматирования результатов выборки — получение одного столбца. Метод selectCol() трактует результат как массив-столбец и возвращает данные в виде списка:

Листинг 26
$cities = $DB->selectCol('SELECT city_name FROM ?_cities');
// В $cities - список имен всех городов.

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

Листинг 27
$citiesById = $DB->selectCol(
  'SELECT city_id AS ARRAY_KEY, city_name FROM ?_cities'
);
foreach ($citiesById as $id=>$name) { ... }

Чайник 

Кстати говоря, результат, полученный в предыдущем примере, можно "один-в-один" использовать вместе с библиотекой HTML_FormPersister, сформировав из него выпадающий список городов (SELECT).

Выборка страницы: selectPage()

Организация страничного навигатора по некоторому результату выборки может оказаться настоящей головной болью, если не знать, как оптимальнее всего подойти к этому вопросу. Помимо выполнения запроса на получение строк очередной страницы нужно дополнительно подсчитывать общее число записей. Иными словами, нам нужен один запрос с предложением LIMIT, и один — с выборкой COUNT(*).

Метод selectPage() сводит эти две операции к одному вызову. С его помощью вы передаете СУБД запрос с необходимыми вам LIMIT-ограничениями, а библиотека дополнительно производит еще и COUNT-запрос:

Листинг 28
$rows = $DB->selectPage(
  $totalRows,
  'SELECT * FROM ?_users LIMIT ?d, ?d',
  $from, $pageSize
);
// Теперь:
// - в $rows: данные очередной страницы
// - в $totalRows: общее число записей в полной выборке

Обратите особое внимание, что первый параметр метода является ссылочным: в переменную, указанную на его месте, записывается общее число попадающих под запрос записей, без учета LIMIT-предложения.

Лирическое отступление 
Метод selectPage() можно использовать только для «простых» SQL-запросов, не содержащих предложение UNION. В противном случае результат не определен.

Выполнение обновлений: query()

Как видите, до сих пор повествование "крутилось" вокруг различных вариаций метода select(), предназначенного для организации выборок из базы данных (SELECT). Но ведь существуют еще и команды вставки (INSERT) и обновления (UPDATE) данных. Как быть с ними?

Библиотека DbSimple поддерживает метод query(), который удобно использовать именно для подобных запросов. А теперь — сюрприз: query() является ни чем иным, как полным синонимом для пресловутого select(). А называется он по-другому, чтобы не пришлось думать: как это — INSERT в select().

Метод query() (а значит, и select() тоже!) возвращает различные значения для INSERT и UPDATE-запросов:

  • В UPDATE-запросах возвращается число строк, задействованных в обновлении. Оно вполне может быть нулевым, что не является признаком ошибки.
  • В INSERT-запросах возвращается значение автоинкрементного поля (если оно имелось в таблице). Естественно, это работает только в СУБД, поддерживающих auto_increment (например, в MySQL).

Вот несколько примеров:

Листинг 29
// Обновляем запись.
$DB->query(
  'UPDATE ?_users SET ?a WHERE user_id=?',
  $userData, $userData['user_id']
);
// Вставляем запись (MySQL).
$userId = $DB->query('INSERT INTO ?_users SET ?a', $userData);

Обработка ошибок в запросах

Выше было сказано, что при возникновении ошибки запроса вызывается зарегистрированный ранее обработчик ошибок. Он, как правило, завершает работу скрипта и выдает исчерпывающую информацию о контексте вызова запроса. В большинстве случаев это поведение — самое разумное, однако для некоторых запросов может понадобиться временно отключить стандартный механизм и обработать ошибку вручную, непосредственно в коде программы.

Библиотека DbSimple позволяет позапросно отключать обработку ошибок, используя для этого стандартную нотацию PHP — оператор @. Иными словами, поставив @ перед вызовом любого из методов DbSimple, вы заставите этот метод вернуть null в случае возникновения проблем. Далее вы можете извлечь контекст выполнения запроса из свойства error (краткое описание ошибки — из errmsg) и поступить с этой информацией так, как вам нужно.

Чайник 

Конечно, не следует злоупотреблять данной возможностью. Используйте ее только в случае, если вы точно уверены, что обрабатываете ошибку вручную непосредственно после выполнения запроса.

Типичный пример: мы хотим вставить некоторую запись в таблицу, но, если она там уже имеется (нарушение уникальности), не завершать работу программы, а выводить пользователю аккуратное предупреждение в форме. Это можно сделать так:

Листинг 30
// Предположим, по полю username имеется уникальный индекс.
if (!@$DB->query('INSERT INTO ?_users SET username=?', $name)) {
  echo 'Такой пользователь уже существует, попробуйте другое имя';
}

Макроподстановки в SQL-запросах

Каждый, кто писал скрипты со сложными запросами к СУБД, знает, какие проблемы начинаются, если запрос требуется составлять динамически. Например, если нам требуется добавить в выражение WHERE некоторое ограничение, если пользователь поставил галочку в форме, и не выполнять его в противном случае. Традиционно в таких случаях применяют динамическое составление SQL-запросов, формируя их в виде строки:

Листинг 31
$sql = '
    SELECT *
    FROM goods
    WHERE category_id = ?
';
if (!empty($_POST['activated_at'])) {
    $sql .= ' AND activated_at IS NOT NULL';
}
$sql .= " ORDER BY price";
$rows = $DB->select($sql, $categoryId);

Теперь представьте, что на activated_at наложено более сложное условие, учитывающее также и его величину:

Листинг 32
$query = array($categoryId);
$sql = '
    SELECT *
    FROM goods
    WHERE category_id = ?
';
if (!empty($_POST['activated_at'])) {
    $sql .= ' AND activated_at > ?';
    $placeholders[] = $_POST['activated_at'];
}
$sql .= " ORDER BY price";
array_unshift($query, $sql);
$rows = call_user_func_array(array(&$DB, 'select'), $query);

В примере выше мы используем всего одно динамическое поле, но на практике их может быть значительно больше. В результате читабельность кода резко снижается, не говоря уж о читабельности генерируемых SQL-запросов...

К счастью, данная проблема относится к классу беспроигрышно-разрешимых. А именно, имеется такой синтаксис, который позволяет создавать динамические SQL-запросы без какого-либо снижения читабельности кода! Он используется в DbSimple:

Листинг 33
$rows = $DB->select('
        SELECT *
        FROM goods
        WHERE 
            category_id = ?
          { AND activated_at > ? }
    ',
    $categoryId,
    (empty($_POST['activated_at'])? DBSIMPLE_SKIP : $_POST['activated_at'])
);

Обратите внимание на блок, обрамленный фигурными скобками ({}-блок). Нетрудно догадаться, как он работает: если хотя бы один placeholder, используемый в этом блоке, имеет специальное значение DBSIMPLE_SKIP, то весь блок удаляется из запроса, в противном случае удаляются только обрамляющие фигурные скобки (точнее, они заменяются на пробелы, чтобы сформированный SQL-запрос хорошо читался).

Чайник 

В настоящий момент значение DBSIMPLE_SKIP определяется в библиотеке как log(0). Т.к. логарифма нуля в природе не существует, константа принимает значение "недопустимое значение: логарифм нуля" (оказывается, есть в PHP такое значение для числа с плавающей точкой), и вероятность того, что кому-то потребуется вставить его в БД, падает до нуля (да оно и не вставится для большинства СУБД).

Начав однажды пользоваться {}-макросами, через некоторое время перестаешь понимать, как же обходился без них раньше. Вот еще примеры запросов с макроподстановками:

Листинг 34
$rows = $DB->select('
        SELECT *
        FROM 
            goods g
          { JOIN category c ON c.id = g.category_id AND c.name = ? }
    ',
    (empty($_POST['cat_name'])? DBSIMPLE_SKIP : $_POST['cat_name'])
);

Листинг 35
$rows = $DB->select('
        SELECT *
        FROM 
            goods g
          { JOIN category c ON c.id = g.category_id AND 1 = ? }
        WHERE 
            1 = 1
          { AND c.name = ? }
    ',
    (empty($_POST['cat_name'])? DBSIMPLE_SKIP : 1),
    (empty($_POST['cat_name'])? DBSIMPLE_SKIP : $_POST['cat_name'])
);

В последнем примере применены два интересных приема.

  • Первый {}-блок содержит placeholder, единственным назначением которого является указание, следует пропустить тело блока или нет. Если он равен 1, то выполняется «бесполезное» условие 1 = 1, и блок остается. Если же он равен DBSIMPLE_SKIP, то блок удаляется.
  • Второй {}-блок тоже используется довольно интересно. Он начинается с ключевого слова AND, поэтому мы вынуждены написать перед ним «бесполезное» и всегда истинное выражение 1 = 1, чтобы не получить синтаксическую ошибку. (Кстати, для OR-выражения надо было бы писать 1 = 0.)

Отсюда мораль: не всегда бесполезные на первый взгляд условия действительно не имеют смысла. Иногда их очень удобно использовать совместно с условными блоками.

Оптимизация prepare+execute

В скриптах, вставляющих много записей в таблицу, запросы обновления обычно выполняются в цикле. Они имеют одну и ту же структуру, различаясь лишь значениями параметров (placeholder-ов). Чтобы СУБД не приходилось каждый раз разбирать синтаксис запроса (транслировать во внутреннее представления), применяется идеология prepare+execute. С использованием гипотетического синтаксиса это выглядит так:

Листинг 36
// Подготавливаем "скелет" запроса и транслируем его.
$sth = prepare('INSERT INTO tbl(field) VALUES(?)');
// В цикле выполняем уже оттранслированный запрос.
foreach ($array as $item) {
  // Подставляем различные значения параметров.
  $sth->execute($item);
}

Практически все библиотеки абстракции от СУБД (в том числе ADODB и PEAR DB) поддерживают независимые операции prepare() и execute(), работающие примерно так же, как описано выше. Однократное выполнение prepare с последующим многократным execute позволяет существенно улучшить производительность, однако дается это за счет традиционного усложнения синтаксиса запросов.

Рад сообщить, что в DbSimple данная дилемма решена беспроигрышно: библиотека имеет только одну операцию выполнения запроса query(), однако, несмотря на это, поддерживает многократный execute без prepare. В итоге мы получаем прирост производительности без изменения интерфейса — хороший пример правильного выбора слоя абстракции для реализации этой возможности.

Чайник 

Вынесение слоя абстракции prepare+execute за рамки DbSimple (как это делают все остальные библиотеки) не представляется имеющим какой-либо смысл, т.к. это все легко можно автоматизировать. Что DbSimple и делает.

Итак, если выполняется серия запросов с одинаковой структурой, но разными значениями placeholder-ов, DbSimple автоматически делает одну операцию prepare и много операций execute. Естественно, это работает только для СУБД, умеющих работать по схеме prepare+execute (например, в InterBase/FireBird). Здесь особенно полезна поддержка "родных" placeholder-ов базы, сильно ускоряющая работу: в большинстве случаев накладных расходов вообще нет.

Пример теперь можно переписать так:

Листинг 37
foreach ($array as $item) {
  // Подставляем различные значения параметров.
  $DB->query('INSERT INTO tbl(field) VALUES(?)', $item);
}

Операция prepare будет выполнена только при первом получении запроса "INSERT INTO tbl(field) VALUES(?)", а во всех остальных случаях запустится только execute, экономя время.

Логирование запросов

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

DbSimple позволяет назначить функцию, которая будет вызываться при выполнении каждого запроса:

Листинг 38
$DB->setLogger('myLogger');
function myLogger($db, $sql)
{
  // Находим контекст вызова этого запроса.
  $caller = $db->findLibraryCaller();
  $tip = "at ".@$caller['file'].' line '.@$caller['line'];
  // Печатаем запрос (конечно, Debug_HackerConsole лучше).
  echo "<xmp title=\"$tip\">"; 
  print_r($sql); 
  echo "</xmp>";
}

Этой функции передается полный текст запроса, в котором placeholder-ы уже заменены на свои значения (это касается также и "родных" placeholder-ов базы). Контекст (файл и номер строки), в котором был запущен данный запрос, вы можете получить при помощи метода findLibraryCaller().

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

Листинг 39
$rows = $DB->select('SELECT * FROM U_GET_PARAM_LIST');

будет выполнена серия из двух вызовов обработчика:

Листинг 40
SELECT * FROM U_GET_PARAM_LIST;
  --- 13 ms = 4+3+6; returned 30 row(s);

Здесь "SELECT ..." — это первый вызов функции логирования, а "--- 13 ms .." — второй вызов. Вы можете заметить, что, фактически, во втором случае никакой запрос не выполнялся: т.к. текст представляет собой "голый" комментарий, выполнять просто нечего. Такого рода "запросы" библиотека генерирует при соединении с базой данных, при запуске новой транзакции, работе с BLOB-объектами и т. д. Они все носят чисто информационный характер и в базу не идут.

Итак, информационные записи содержат:

  • Время выполнения запроса.
  • В случае возникновения ошибки СУБД — текст этой ошибки с кратким указанием контекста вызова.
  • Для SELECT-запросов: число строк результата. Если строка всего одна, и данных не слишком много, она будет включена прямо в информационную запись.
  • Для UPDATE- и INSERT-запросов: признак успешности выполнения.

Лирическое отступление 
Если функция логирования не установлена, библиотека работает гораздо быстрее: ведь ей нет необходимости "вручную" разворачивать "родные" placeholder-ы, собирать статистическую информацию, а также определять контекст вызова запроса. Таким образом, на хостинге, где отладка не нужна, просто не назначайте функцию-логгер.

Транзакции

Транзакции поддерживаются методами $DB->transaction(), commit() и rollback(). У каждого соединения в любой момент времени может существовать только одна текущая транзакция.

Запросы с атрибутами

Каждый запрос может быть снабжен одним или несколькими атрибутами, являющимися некоторыми указаниями для DbSimple. Они оформляются в виде SQL-комментариев, идущих перед телом запроса, и имеют формат:

Листинг 41
— AttributeName: AttributeValue

Атрибут BLOB_OBJ: объектные BLOB-поля

Если BLOB-ы очень большие, можно работать с ними как с объектами, выполняя read и write "кусками".

Для того, чтобы получить blob-поля в виде объектов, а не в виде строки, используйте синтаксис:

Листинг 42
$row = $DB->selectRow('
    — BLOB_OBJ: true
    SELECT * FROM table WHERE id=123
');

В результате в $row['blob_field'] окажется не строка, равная содержимому blob-а, а объект DbSimple_*_Blob, у которого есть метод read().

Атрибут CACHE: кэширование запросов

Лирическое отступление 
Внимание: данная функциональность находится в разработке и в настоящее время может работать нестабильно. Используйте ее с осторожностью.

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

Листинг 43
$row = $DB->select('
    — CACHE: 10h 20m 30s
    SELECT * FROM table WHERE id=123
');

Здесь "10h 20m 30s" - промежуток времени, в течение которого запрос будет браться из кэша. (Если указано только число, то оно трактуется как промежуток времени в секундах.)

Зависимость от источников данных

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

Листинг 44
$row = $DB->select('
    — CACHE: 10h 20m 30s, forum.modified, topic.modified
    SELECT * 
    FROM forum JOIN topic ON topic.forum_id=forum.id 
    WHERE id=123
');

Здесь предполагается, что в таблице forum имеется столбец с именем modified, хранящий дату последнего изменения записи (аналогично и с таблицей topic). Как только в указанных таблицах появляется новая запись, библиотека это обнаруживает, делая запрос SELECT MAX(forum.modified) FROM forum, и очищает кэш.

Чайник 

Конечно, чтобы инвалидация работала правильно, вы должны перечислить все таблицы, от которых зависит запрос. Кроме того, убедитесь, что с полем modified в БД связан индекс, иначе запрос на получение самой новой записи может работать очень долго.

Работа с кэш-хранилищем

При работе с кэшем используются две общие операции: запись данных в кэш и чтение данных из кэша. Сама библиотека DbSimple не содержит методов, которые их реализуют. Это и понятно: в каждом конкретном приложении используется своя собственная система кэширования.

Пожалуй, самым оптимальным вариантом можно считать применение memcached, т.к. он поддерживает автоматическую очистку старых записей кэша на основе частоты их использования. Однако, если memcached недоступен, то подойдет и любая другая реализация (например, на файлах).

Для подключения кэш-функции к библиотеке используется следующий синтаксис:

Листинг 45
// Connect to database.
$DB = DbSimple_Generic::connect($dsn);
// Set caching function.
$DB->setCacher('myCacher');
// Define caching function.
function myCacher($key, $value)
{
    // Если $value !== null, то следует записать его в кэш с ключом $key.
    // Если $value === null, то следует вернуть значение кэша с ключом $key.
}

Резюме

Библиотека DbSimple является удобным инструментом для тех, кто пишет скрипты с использованием большого разнообразия сложных SQL-запросов. Ее основная черта — простота и лаконичность повседневного применения. А вот и другие два плюса библиотеки: расширенная поддержка placeholder-ов и условные макроблоки.







Дмитрий Котеров, Лаборатория dk. ©1999-2016
GZip
Добавить на Del.icio.us   Добавить на Digg.com   Добавить на reddit.com
{{Counters: empty}}