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

November 20, 2007

Сегодня мы продолжим и завершим знакомство с библиотекой dbSimple. Мне осталось закончить начатый в прошлой статье рассказ о кэшировании, показать пару фокусов с преобразованием результата запроса в привычные для php массивы. Также я скажу пару слов о memcached.

Напоминаю, что в прошлый раз мы остановились на создании запросов с помощью dbSimple (dklab.ru). Мы писали текст запроса содержащий специальные символы – placeholder-ы, вместо которых при выполнении запроса подставлялись реальные данные, с учетом типов данных, и выполнялось экранирование спец. символов. Результат выполнения запроса возвращался в виде массива. Это был обычный массив (для индексации использовались цифры), в свою очередь каждый элемент массива представлял собой запись таблицы БД и был А-массивом (в качестве ключей использовались имена полей). Подобная схема была очень приятна, по сравнению с привычной для php mysql/postgres/… расширений. Ранее данные возвращались в виде ссылки на некоторый ресурс, и нам приходилось реализовывать простой, но рутинный код по преобразованию данных в массивы или А-массивы. Еще более приятно использовать функцию dbSimple называемую “выборка ассоциативного массива”. Вы также как и раньше посылаете запрос sql-на сервер, но результат выполнения запроса помещается не внутрь обычного массива, а ассоциативного. И вы можете указать какое поле или формула будет использована как ключ этого массива. Например, в коде ниже предполагается, что есть таблица товаров с полем article_id, который и будет ключом массива-результата. В тексте запроса sql я пометил это поле как ARRAY_KEY.

Также обратите внимание на строку, в которой я посылаю на сервер команду “set names cp1251”. Это специфическая для mysql команда, необходимая для того, чтобы выбирать текст в правильной кодировке (windows-1251, кириллица).
  1. include_once ('koterov/lib/DbSimple/Generic.php');
  2.  
  3. // подключение к серверу от имени root c пустым паролем, база называется rewriteshop'
  4. $db = DbSimple_Generic::connect('mysql://root:@localhost/rewriteshop');
  5.  
  6. $db->query ('set names cp1251');
  7. $rows_arts = $db->select( 'SELECT article AS ARRAY_KEY, shop.* FROM shop');
  8.  
  9. print_r($rows_arts);// печатаем всю выбранную информацию
  10.  
  11. foreach ($rows_arts as $article=>$row) {
  12.   print 'article price: ' . $row['price'] . '<br>';
  13.   print 'article info: ' . $row['info'] . '<br>';
  14. }
  15.  
  16. print 'price for milk = ' . $rows_arts['milk']['price'];
  17. // печатаем цену на товар с заданным именем
  18. print 'error = ' . $rows_arts['milk']['ARRAY_KEY']; 
  19. //поле помеченное как ARRAY_KEY является виртуальным и в массив с данными не помещается
Будьте внимательны, у меня в таблице есть несколько товаров с названием “milk”, отличающихся датой производства ведь dbSimple создает ассоциативный массив. И если у вас для нескольких записей совпадет значение, указанное в поле, помеченном как ARRAY_KEY, то будут утеряны все записи, кроме последней (см. рис. 1).



Вы можете пометить любое количество полей как ARRAY_KEY_* (вместо “*” подставляется цифра). Затем эти поля будут отсортированы по возрастанию номеров и результат будет помещен в многомерный ассоциативный массив. Так для следующего запроса результат будет выбран так (см. рис. 2):


  1. SELECT sect AS ARRAY_KEY_1,article AS ARRAY_KEY_2,date_of AS ARRAY_KEY_3, shop.* FROM shop
Еще одна возможность, это указать в качестве второго ключа А-массива значение NULL.
  1. SELECT sect AS ARRAY_KEY_1,NULL AS ARRAY_KEY_2, shop.* FROM shop
Тогда dbSimple создаст массив, подобный указанному на рис. 3.



И последняя возможность dbSimple в преобразовании отобранной информации пригодится, если вы храните иерархические данные в таблице. Например, таблица вида: id_human, fio, id_mother, id_father. Здесь реализовано рекурсивное отношение, когда поля id_father и id_mother принимают значения, хранящиеся в этой же самой таблице в поле id_human.
  1. SELECT id_human AS ARRAY_KEY, id_father AS PARENT_KEY, humans.* FROM humans
В этом случае dbSimple найдет все записи (людей), у которых нет родителей, т.е. которые являются “корнями” деревьев. Затем в массив, содержащий информацию из полей записи, будет добавлен еще один элемент childNodes. В который будет помещен список всех записей, принадлежащих данному родителю (тех, у кого поле id_father равно полю id_human родительской записи). И так повторяется до тех пор, пока дерево не будет построено целиком (см. рис. 4).



Не секрет, что каждый программист тратит значительную долю своего времени на поиск и устранение ошибок – то, что называется отладкой. Для того чтобы этот процесс не занимал слишком много сил и времени нам нужны специализированные инструменты позволяющие узнать: “что же там, у программы внутри, и что она делает на самом деле”. Инструментов много, иногда они встроенные в среду разработки, иногда интегрированы со средствами профилирования кода (когда нужно еще узнать, почему ваша программа так медленно работает, и куда делась вся свободная память?). Но самым долгоживущим и популярным средством является “журналы”. Журналом может быть файл или таблица в базе, в который каждый раз, при выполнении некоторой функции, помещается информация о том, что было вызвано (имя функции), с какими параметрами, и что эта функция вернула. Журналы являются, по сути, единственно действенным способом найти ошибку в случае, если вы территориально отдалены от заказчика, особенно если разрабатываемое приложение не является сетевым. Журналирование выполняемых действий - не такая простая задача: важно соблюсти разумный компромисс между количеством записываемых действий, их подробностью и производительностью системы.

DbSimple предоставляет вам возможность назначить специальную функцию – logger. Эта функция будет вызываться каждый раз, когда на сервер посылается запрос. Функция в свою очередь может сохранить сведения о деятельности библиотеки в файл-журнал для последующего анализа, как показано в следующем примере:
  1. // назначаем функцию журналирования 
  2. $db->setLogger('myLoggerToFile');
  3.  
  4. function myLoggerToFile($db, $sql){
  5.   // Определяем то, кто и откуда вызвал функцию выполнения запроса dbSimple
  6.   $called_from = $db->findLibraryCaller();
  7.   // выводим сведения о текущем времени, и о том кто вызвал функцию, также текст запроса
  8.   $log = '*' . date('d.m.y h:i:s') . " call info, file: ".@$called_from['file'].
  9. '; line: '.@$called_from['line'];
  10.   $log .= "\n---- sql: " . $sql . "\n";
  11.   // теперь сохраним информацию в файл
  12.   $h = fopen('log', 'a');
  13.   fwrite($h , $log);
  14.   fclose($h); 
  15. }
  16.  
  17. $rows_arts = $db->select( 
  18.  'SELECT id_human AS ARRAY_KEY, id_father AS PARENT_KEY, humans.* FROM humans' );
В результате работы данного скрипта в файл будет помещены следующие строки:
*05.11.07 12:30:10 call info, file: H:\docs\bazza\meshop.php; line: 26
---- sql: SELECT id_human AS ARRAY_KEY, id_father AS PARENT_KEY, humans.* FROM humans
*05.11.07 12:30:10 call info, file: H:\docs\bazza\meshop.php; line: 26
---- sql:   -- 1 ms; returned 6 row(s)
Вы видите что, один запрос на самом деле привел к двум вызовам функции myLoggerToFile. В первый раз библиотека dbSimple сообщила нам, какой запрос sql она хочет выполнить, а во второй вернула сведения о количестве записей, которые сформировал сервер, и времени выполнения запроса. Наверняка пригодится вам и функция getStatistics – она вернет итоговые сведения о количестве выполненных запросов и затраченном на это времени.

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

Размер BLOB-полей может оказаться катастрофическим для вашего веб-приложения. Например, если у вас есть таблица сотрудников с полем биография. Вы делаете выборку всех записей из этой таблицы и формируете на веб-странице их перечисление, для некоторых сотрудников выводите также и их биографию. В какой-то момент у вас в памяти хранится все содержимое таблицы БД и оно просто огромно. Мало того, что ваш скрипт работает медленно, так и учитывайте ограничения оперативной памяти. Для веб-приложений (точнее, для типового веб-хостинга, когда на одной машине выполняется сразу несколько десятков сайтов) характерно ограничение по размеру максимально выделенной О.П. Так для php этот лимит равен 8 мб, иногда 16. В любом случае, наши провайдеры не любят сайты размещенные на дешевых тарифах и потребляющие излишние ресурсы – вам вполне может придти письмо с просьбой разобраться или купить более дорогой хостинг. Неплохой прием работы с BLOB заключается в том, что данные берутся из таблицы только на короткое время – тогда когда они действительно нужны. Например, предыдущий пример с выводом на странице сведений о сотрудниках можно переделать так. Отобрать все записи сотрудников из таблицы, но без поля биография. Затем в цикле, перебирающем эти записи, только тогда когда найдется сотрудник, биография которого должна быть распечатана, следует послать еще один запрос к СУБД. Чтобы она вернула только одно поле BLOB с биографией и именно для этого сотрудника. После обработки поля нужно уничтожить переменную, в которой хранился BLOB, чтобы память как можно скорее снова стала свободной.

В следующем примере в тексте sql-команды присутствует комментарий (предваряемый двойным дефисом), текст которого содержит специальную пометку “BLOB_OBJ: true”. Это значит, что поля BLOB будут возвращены не как строка текста, а в виде объекта. Этот объект содержит метод для чтения информации read(количество_байт_для_чтения). По правде говоря, данная функция зависит от возможностей СУБД и для mysql является простой “обманкой” – никакого выигрыша по ресурсам не будет. А вот postgres и interbase функцию чтения blob по частям поддерживают.
  1. $rows_arts = $db->select('-- BLOB_OBJ: true
  2. SELECT * FROM humans');
  3.  
  4. $blob_bio = $rows_arts [0]['bio'];// это поле BLOB
  5. print 'размер поля bio = ' . $blob_bio->length () . '<br />';
  6. print 'содержимое поля bio = ' . $blob_bio->read (100) . '<br />';
Теперь мы перейдем к кэшированию информации. Технически реализация кэширования в dbSimple построена на предположении, что информация в таблице отбираемая некоторым запросом будет неизменна в течении определенного времени. Величина этого времени задается в тексте sql-запроса, например, так:
  1. $rows_arts = $db->select('-- CACHE: 0h 1m 30s
  2. SELECT * FROM humans');
Обратите внимание, что здесь, как и в прошлом примере, после комментария с директивой использования КЭШа должна начаться новая строка. Сам же текст директивы очевиден: мы говорим, что информация должна храниться в КЭШе в течении полутора минут. Так после первого запуска скрипта я изменил значения ряда полей в таблице, но результат выборки не изменился – что и ожидалось. В примере не показано, но в общем случае мы должны указать, кто именно будет ответственен за кэширование, где будет храниться информация. В каждом приложении может быть применена своя реализация кэш-менеджера, в идеале он должен быть написан не на php, а на c|c++ чтобы выжать еще пару капель производительности. Однако даже если ничего больше не писать, то пример все равно будет работать. По-умолчанию вместе с dbSimple идет простенькая библиотека Cache/Lite.php. Она использует для хранения повторно-используемых результатов выборки временные файлы.

В некоторых ситуациях мы не можем ждать несколько минут или часов чтобы кэш стал недействительным. В этом случае мы должны указать еще один параметр для dbSimple – имена таблиц и имена полей этой таблицы, хранящие дату последней модификации записи. Тогда dbSimple может быстро проверить была ли выполнена модификация таблицы, сравнив значение этого поля с тем, каким оно было на момент создания КЭШа. Для базы данных mysql вы можете добавить такое поле timestamp к любой существующей таблицы с помощью команды:
  1. ALTER TABLE имя_таблицы ADD modified timestamp
Тогда сервер при любом изменении записи будет автоматически менять значение поля “modified” внося в него текущую дату/время. Следовательно, вы можете не бояться забыть указать новое значение этого поля в запросах на модификацию данных – о нем можно просто забыть. Запрос sql для dbsimple будет выглядеть так:
  1. $rows_arts = $db->select('-- CACHE: 10m, humans.modified
  2. SELECT * FROM humans');
Здесь кэш будет обновлен либо через 10 минут, либо как только были внесены изменения в таблицу humans. Если вы строите запрос на основании нескольких таблиц, то просто перечислите их через запятую, например, так:
  1. $db->select('-- CACHE: 1h, users.modified, payments.modified 
  2. SELECT *  FROM users, payments USING (user_id) ');
Можно и вообще не указывать время хранения КЭШа – только поля таблиц с timestamp, например, так:
  1. $db->select(' -- CACHE: humans.modified
  2. SELECT * FROM test');
По правде говоря, определить, что таблица была изменена на основании только поля timestamp не возможно, например если записи были удалены, то в поле modified это никак не отразится. Например, для mysql есть специфическая команда show table status. Она возвращает сведения о таблице, в том числе и дату ее последней модификации:
  1. SHOW TABLE STATUS [FROM db_name] [LIKE 'pattern']
Так что в случае необходимости вы сможете улучить кэш-реализацию dbSimple.

Вместо Cache/Lite может быть назначен любой другой менеджер кэширования. Наибольшей популярностью пользуется memcached. Это приложение, написанное на c|c++, которое устанавливается на сервере, запускается и работает как сервис-демон. Его назначение - это кэшировать в памяти часто-используемые объекты. Memcached исторически появился в недрах livejournal (ЖивогоЖурнала), когда его сервера не стали справляться с нагрузкой. А простое увеличение количества серверов не приводило к качественному улучшению ситуации, не говоря о том, что это были лишние затраты. Принцип работы memcached таков: обращаемся к серверу memcached за информацией. Если объект все еще в памяти, то вам его вернут. Если же объекта нет, то он вычисляется или берется из базы данных, отдается клиенту и обязательно кладется в memcached – на потом. Memcached управляет объектами на основании сложных алгоритмов, где учитывается степень “популярности”, так что не рекомендуется его применять когда информация изменяется редко – в этом случае более подойдет обычный файловый кэш. Обычно memcached работает на выделенном сервере, к которому подключается несколько веб-серверов с сайтами. Вы можете “поиграться” с memcached даже если у вас нет выделенного linux-сервера – доступен порт memcached для windows. Разработаны клиентские библиотеки для обращения к memcached из разных языков: php, perl, java, python, ruby.

На этом все. В следующий раз я начну рассказ о двух сходных паттернах доступа к данным: Active Record и Row Data Gateway. Также мы познакомимся с еще одной интересной библиотекой работы с БД из php – adodb.