Доступ к базам данных из php. Часть 3

November 12, 2007

Сегодня мы продолжим знакомство с паттернами доступа к данным и библиотеками php реализующими эти паттерны или просто помогающими делать нам меньше ошибок. В прошлый раз я начал рассказывать о библиотеке dbSimple (http://dklab.ru), сегодня мы продолжим ее рассмотрение и поднимем еще парочку интересных вопросов “качественного доступа к СУБД”.

Полагаю, что вы уже загрузили библиотеку по адресу http://dklab.ru/lib/DbSimple/demo.zip Первое что нужно – это подключить файл библиотеки “DbSimple/Generic.php”, затем используйте вызов функции DbSimple_Generic::connect для соединения с сервером СУБД. В качестве параметра функции передается строка содержащая DSN (data source name). Проще говоря, в этой строке указывается то, с какой СУБД вы хотите работать, имя сервера, имя пользователя и пароль для входа, также задается имя базы и опциональный набор параметров, управляющих специфическими возможностями СУБД (кодировка, используемый диалект SQL, …). Возможно, что операция соединения или какая-то из последующих команд отбора данных не удастся. Нам необходимо позаботиться об обработке ошибок, создать функцию, которая будет вызываться при возникновении сбоя, а внутри этой функции мы можем выводить сообщения об ошибках на экран браузера в удобочитаемом виде, либо записывать сообщение в системный журнал, в production-системе можно отправлять сообщения на электронную почту администратора сайта. В примере далее я создал функцию onErrHandler и привязал ее к соединению к БД. Несмотря на то, что соединение с СУБД выполняется раньше чем назначение функции обработчика, если все же соединение не удастся, то сразу после назначения функции-обработчика будет сгенерирована ошибка и наша функция сможет ее обработать (ошибка не потеряется).

После соединения мы должны отправлять серверу запросы на отбор информации или ее модификацию. В прошлой статье я рассказывал о проблеме sql-injection (как злые хакеры пытаются взломать наш sql-код) и проблеме “хочу массив”. В DbSimple реализован набор функций получающих в качестве sql-команды некоторую заготовку. В заготовке места, где должны быть размещены данные, взятые из отправленных веб-форм или переменные адресной строки, будут помечены специальными последовательностями символов (placeholders). А результат запроса возвращается в форме привычного всем массива, обычного или ассоциативного. Так функция select возвращает массив, каждый элемент которого хранит информацию об одной записи, в форме А-массива, у которого, в свою очередь, каждый индекс – это имя поля в таблице БД. Похожа на select и функция selectRow, ее отличие в том, что служит для выборки одной единственной записи, которая и возвращается в виде А-массива. Функция selectCol служит для выборки одноколоночного запроса, который возвращается в виде обычного индексированного массива. Функцию selectCell удобно применять в случае, если ваш запрос возвращает только одно число, например, сумму значений некоторого столбца или количество записей в таблице. Функция selectPage – служит для постраничной выборки записей. И, наконец, функция query, ее следует применять в том случае, когда вы хотите не отобрать данные из таблицы, а выполнить модификацию данных: удалить записи, обновить записи или вставить новую запись. Если вы отправляете запрос на вставку данных в таблицу, где содержится поле типа auto_increment (наилучший выбор поля на роль первичного ключа), то функция query вернет номер вставленной записи, в противном случае возвращается количество записей, которые были подвергнуты модификации.
  1. <?php
  2. include_once ('koterov/lib/DbSimple/Generic.php');
  3.  
  4. // подключение к серверу от имени root c пустым паролем, база называется smf
  5. $db = DbSimple_Generic::connect('mysql://root:@localhost/smf');
  6.  
  7. //назначаем объекту соединения функцию обработчик ошибок
  8. $db->setErrorHandler('onErrHandler');
  9.  
  10. //функция обработки ошибок
  11. function onErrHandler($message, $info){
  12.  if (! error_reporting ()){
  13.   // записываем в журнал сведения о некритической ошибке
  14.   return; 
  15.  }
  16.  print "Сбой работы с БД: $message<br /><pre>"; 
  17.  print_r($info); die ("</pre>");
  18. }
  19.  
  20. // а теперь используем подключение к БД
  21. print 'drop:' . $db->query ('drop table if exists users') .<br />;
  22. // после удаления таблицы создадим ее заново
  23. print 'create:' . $db->query ('create table users (id int auto_increment primary key,
  24.  fio varchar(50), sex enum("f","m") )') .<br />;  
  25.  
  26. // добавляем записи о людях
  27. print 'insert id = :'. $db->query ('insert into users (fio, sex) 
  28. values (?, ?)', $_REQUEST['fio'], $_REQUEST['sex']) .<br />;
  29. ?>
Последняя строка в примере как раз и демонстрирует что такое placeholder. Вместо символов “?” подставляются значения, переданные вторым и третьим параметром функции. Заметьте, что я не указал внутри VALUES ограничивающие строку кавычки. Также я не выполнял экранирование (см. прошлую часть, посвященную экранированию спец. символов) входных переменных – все это было сделано автоматически. Перед запуском скрипта я сделал небольшую подготовительную работу: поместил в каталог где находится скрипт файл .htaccess и написал в нем директиву запрещающую использование “адских кавычек”.
 php_flag magic_quotes_gpc off
Существует несколько разновидностей placeholder-ов, служащих для обозначения вставки в текст запроса различного вида информации. В примере выше я использовал простейший символ “?” – вставка строки. В том случае если вы хотите поместить в таблицу некоторое числовое значение, то рекомендуется использовать placeholder “?d” или “?f” – для целых и вещественных полей, соответственно:
  1. // добавим новое поле - вес человека
  2. $db->query ('alter table users add weight float');
  3. // обновим значение этого поля на 97.5 для всех записей в таблице
  4. $db->query ('update users set weight = ?f', 95.7);
  5.  
  6. // и еще одно поле но уже в виде целого числа
  7. $db->query ('alter table users add friends int');
  8. // обновим значение этого поля на 12 для всех записей в таблице
  9. $db->query ('update users set friends = ?d', 10);
Есть также модификатор “?n” – ссылочный. Но я про него ничего не буду рассказывать, только посоветую вам обратиться по следующему адресу http://dklab.ru/lib/DbSimple/manual.html#cont16 – там подробно описана сфера его применения.

Модификатор “?#” – пригодится вам в том случае, у вас есть таблицы или поля, названия которых совпадают с определенными ключевыми словами вашей СУБД. Хотя я настоятельно рекомендую никогда так поля не называть, но если это служилось, то поможет “?#”. Он поместит имена объектов СУБД внутрь специальных символов (для mysql это символ апострофа, для mssql символ квадратных скобок и т.д.).

Более интересен модификатор “?a” – списковый. Его назначение вставить внутрь строки запроса список элементов массива разделенных с помощью “,”. Если же массив является ассоциативным, то будут вставлены пары ключ=значение. Ниже два примера:
  1. // пример на передачу через ?a обычного массива, также заметьте, 
  2. // что символ ?# был заменен на список имен полей
  3. $db->query (
  4.    'insert into users (?#) values (?a)', 
  5.    array (fio, sex, weight, friends), 
  6.    array ('bill', 'm', 12.78, 5)
  7. );
  8.  
  9. // а теперь вместо ?a подставляется содержимое ассоциативного массива 
  10. //конструируя корректную запись 'weight' = '100'
  11. $db->query ('update users set ?a where id in (?a)', array ('weight' => 100), array (2,3));
Особую роль играет placeholder “?_”. Хорошей практикой в разработке приложений работающих с СУБД является создание для всех ваших таблиц в базе некоторого префикса. Например, для гостевой книги - “guest_”, для форума - “forum_”. Если вы так сделаете, то у клиента никогда не возникнет проблем установки и гостевой книги и форума в одну базу данных, особенно, если у этих двух приложений имена таблиц будут совпадать. Например, таблица messages – хранящая то ли сведения о сообщениях форума то ли записи в гостевой. При установке вашего продукта на сайт, клиент должен будет указать некоторый префикс и тем самым избежать конфликта имен: одна таблица будет называться forum_messages, а вторая – guest_messages – и никаких пересечений имен. Так весь код в примерах выше может быть переписан по правилу:
  1. $db->setIdentPrefix('mega_');// назначаем префикс – внимание, 
  2. // он должен заканчиваться на символ “_”
  3. print 'drop:' . $db->query ('drop table if exists ?_users') . '<br />';
  4. $db->query ('update ?_users set friends = ?d', 10);
Кроме показанной в примерах функции select, возвращающей массив записей, вот еще пара примеров на другие функции:
  1. $rez_rows = $db->selectPage($all_size, 'SELECT * FROM ?_users LIMIT ?d, ?d', $from, $page_size);
Здесь выполняется запрос с постраничной выборкой, используется специфическая для mysql конструкция LIMIT, после которой следуют числовые placeholder-ы для номера записи с которой идет выборка ($from) и количества отбираемых данных ($page_size). Внутри библиотеки DbSimple скрыта нехитрая манипуляция с SQL_CALC_FOUND_ROWS и FOUND_ROWS, благодаря чему в переменную $all_size будет помещено общее количество записей (на всех страницах). И это работает не только для mysql (указанные выше ключевые слова специфичны именно для mysql) но и для postgres и interbase. В поставке dbSimple идут драйвера для этих СУБД, скрывающие внутри себя эту некоторую эмуляцию данных функций. Еще раз напомню, что simpleDB не выравнивает код sql запросов, не добавляет отсутствующую функциональность.

А вот пример отбора одного значения:
  1. print 'count: ' . $db->selectCell ('select count(1) from ?_users') . '<br />';
Очень интересна возможность макроподстановок. Дело в том, что часто запрос sql в программе является не статическим, т.е. предопределенным на стадии разработки и не меняющимся, а динамическим и зависящим от набора входных данных. Скажем, у вас есть каталог товаров (цена, цвет, название, материал). Вы хотите сделать форму поиска товаров, в которой можно будет отметить то, по каким полям следует выполнять запрос. Т.е. часть команды WHERE будут опциональными, например, так:
  1. print_r ( 
  2.   $db->select('select * FROM ?_users WHERE (1 = 1) {and fio = ?}',
  3.   (empty($_REQUEST['ffio'])? DBSIMPLE_SKIP : $_REQUEST['ffio'])) 
  4. );
Данный запрос должен находить людей с фамилией заданной внутри веб-формы поиска как переменная ffio. Если же параметр не задан, то следует отобрать всех людей. Вариативную часть запроса “fio = ?” я поместил внутрь фигурных скобок. Это значит, что если вместо значения параметра “?” будет указана специальная константа DBSIMPLE_SKIP, то все, что заключено в фигурные скобки будет удалено из запроса. Не обошлось без маленького трюка: в условии присутствует строка 1 = 1. Зачем скажите вы это нужно делать, ведь всегда 1 = 1 и тем более какое значение это имеет к содержимому таблицы users? На самом деле это очень полезный фокус, тогда когда количество вариантов условий более одного, и вы боитесь, что ни один из них не будет задан. Без условия 1 =1, когда были бы исключены все остальные условия то получилась бы висящая конструкция запроса “select * from ?_users WHERE”.

Одна из возможностей dbSimple о которой я не могу не упомянуть – это кэширование. Давайте поговорим о том, что такое кэширование и зачем оно нужно?

Для типового сайта характерно значительное превышение количества просмотров информации перед числом изменений. Если ваш сайт активно посещается, то рано или поздно станет вопрос роста нагрузок на сервер. Начнется это с того, что ваш хостер пришлет письмо с предупреждением, мол, нагрузки превышают некоторый лимит и создают сложности для других сайтов размещенных на том же сервере, что и ваш (для наиболее привычного, и дешевого, виртуального хостинга характерно, что один физический сервер обслуживает множество виртуальных веб-серверов или сайтов). Если вы не примите мер по улучшению ситуации и снижению нагрузки, то ваш аккаунт будет приостановлен (такой пункт присутствовал в договорах всех хостеров, с которыми я сталкивался). Но прежде чем вы заплатите деньги за лучший хостинг, и может быть, выделенный сервер, следует подумать над оптимизацией алгоритмов. Перепишите и оптимизируйте код, используйте встроенные в вашу СУБД средства анализа выполнения запроса, чтобы найти узкие места. Еще один способ поднять производительность – кэширование. Оно может выполняться на различных уровнях и зачастую без нашего участия (встроенные возможности используемого вами “софта”). В общем случае, можно кэшировать информацию, отбираемую из базы данных, либо кэшировать конечный результат работы веб-страницы, отдельный вариант - кэширование шаблонов – но об этом позже. Очевидно, что второй вариант, когда страница формируется лишь первый раз, после внесения изменений, получившийся код html сохраняется в файл, и всякий раз, когда страница запрашивается во второй, третий, … сотый раз – мы берем этот файлик и отдаем его клиенту – самый быстрый и самый простой. Очевидный минус – в том, как узнать, когда информация была изменена и страницу необходимо перегенерировать. Это не так просто как кажется. Первый вариант – при любом изменении данных выполнять обновление всех сохраненных в КЭШе страниц. Это ужасно, и подходит только для сайтов с редко меняющейся информацией. Вариант два – выполнять перегенерацию лишь тех страниц, которые зависят от изменившейся информации. Здесь можно для всех записей в таблицах ввести поле “дата_последнего_изменения”, и перед генерацией страницы проверить осталась ли дата “старой” и если это не так, то кэш обновляется. Правда, что делать, если были добавлены новые данные или удалены старые? Например, каталог товаров, в котором появились новые позиции или удалили старые – простого ответа нет. Плюс, если страница разделена на логические зоны: меню, шапка, список новостей – то можно выполнять кэширование этих частей по отдельности. Естественно, что можно придумать сколь угодно хитрую стратегию отслеживания “поменялось ли что-то”, но чем более эта стратегия качественна, тем она сложнее и тем больше для ее работы будет нужно ресурсов, так что с какого-то шага кэширование станет “вредным”. В большинстве “народных” реализаций кэширования я видел подход, когда предполагается, что кэш действителен в течение некоторого времени. Например, каждые 5 часов кэш очищается. Это терпимо когда никаких изменений не произошло и кэш еще валиден, а вы его очищаете и заполняете заново. И нетерпимо, когда после изменения данных клиент должен ждать несколько часов, чтобы наконец-то увидеть изменения. Это не значит что подход основанный на времени “истечения срока годности информации” плох – каждой ситуации свое решение.

Кэширование тесно связано и с игрой в балансировку нагрузки. В создании страницы задействованы несколько участников. В простейшем случае, у вас есть веб-сервер, на котором исполняется код вашего сайта и сервер базы данных. Может быть, так, что второй из них испытывает повышенные нагрузки, и тогда имеет смысл разгрузить его за счет уменьшения количества sql-запросов. Сделали запрос – отобрали информацию и положили в память, или в файлик, чтобы ближайшее время (ох, и как узнать что информация еще “свежая”…) не посылать запросы к серверу БД, а брать сведения из нашего КЭШа. А возможно через пару недель ситуация с нагрузкой качнется в другую сторону и наши попытки кэшировать информацию на стороне веб-сервера будут уже вредными – эффективнее будет выбирать сведения из БД каждый раз заново.

Хороший сервер БД также думает о кэшировании: говорить о кэшировании результатов выполнения запросов (собственно, отбираемой информации) бесполезно, а вот кэширование планов запросов – вполне реализуемо и встречается часто. Что такое план запроса? Вот написали вы текст sql-запроса и отправили его серверу. Сервер должен потратить некоторое время на синтаксический анализ пришедшей команды, проверить, что в ней нет ошибок, проверить права доступа. А затем придумать, как он будет выполнять вашу команду на уровне элементарный действий с жестким диском и памятью. Очевидно, что таких способов много и они зависят от того какие есть индексы в таблицах, какое заполнение этих индексов, от особенностей используемых вами функций и операторов отбора. После анализа этих сведений сервер выбирает наиболее оптимальный план и этот план сохраняется в течении некоторого времени. Так что когда вы посылаете серверу команду повторно, то он быстренько находит в КЭШе команд уже подготовленный план выполнения запроса и использует его, не тратя время на разбор, проверку корректности и поиск наилучшего способа выполнить запрошенное вами действие.

Что такое кэширование шаблонов и сами шаблоны? Шаблоны развивают идею абстракции и послойного построения приложения. Первый слой – слой доступа к данным – отберет нужные сведения в виде массивов php или xml. Затем данные поступят на вход второму слою – слою визуализации – и будут вставлены внутрь шаблонов. Шаблон - это заготовка или “рыба” веб-страницы, с готовой версткой и помеченными местами, куда нужно в этом шаблоне поместить информацию, взятую с предыдущего слоя. На практике движки шаблонов гораздо сложнее, чем просто “найди в тексте страницы-шаблона слово XXX_FIO_AUTHOR и замени его на фамилию автора книги”. Нам потребуются условные конструкции, циклы, … многое другое, что усложняет синтаксис шаблонов и приводит к значительной потере одного из традиционных рекламных плюсов “только с нашим движком шаблонов даже неопытный (читай, вчерашний студент двоечник) сможет верстать самые сложные сайты”. Чтобы выйти из этой ситуации придумывают особый, упрощенный синтаксис языка шаблонов. Чтение файла шаблона и выполнение команд на его придуманном языке требует чувствительных затрат ресурсов, и уже сложилась традиция незаметно для дизайнера-html-кодера выполнять преобразование из упрощенного языка шаблонов в полноценный (главное работающий быстро) язык некоторого серверного расширения (тот же php). Так как шаблоны меняются достаточно редко, то очевидно, что и здесь имеет смысл использовать кэширование – выполнив преобразование файла шаблона в исполняющийся файл php можно использовать его многократно.

На этом рассказ про кэшировании шаблонов я прекращу. В следующий раз я расскажу о том, как умеет кэшировать dbSimple а также о совместном использовании dbSimple и memcached.