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

November 27, 2007

Сегодня мы продолжим знакомство с передовыми методиками доступа к базам данных. На очереди рассмотрение паттернов Active Record и Row Data Gateway. Также я расскажу о новой библиотеке adodb.

Чем отличается паттерны Active Record и Row Data Gateway от рассмотренного в прошлых статьях Table Data Gateway (шлюза таблицы данных). Шлюз таблицы говорил нам: “давайте, отделим и обособим sql-код обращающийся к БД от остальных частей программы, создадим библиотеку, в которой будет несколько функций, для того чтобы искать сведения в таблицах, функции для добавления, удаления и изменения данных, а затем будем везде в программе использовать только эти функции”. Паттерн Row Data Gateway предлагает каждой записи возвращаемой из выборки поставить в соответствие некоторый объект. Свойствами данного класса будут поля таблицы, методы класса будут служить для сохранения информации в объекте как новой записи, обновления существующей или удаления из таблицы БД той записи, представителем которой является данный объект. Интересной особенностью Row Data Gateway является то, что его объект может представлять собой не запись таблицы, а запись более сложной выборки (например, запрос из нескольких таблиц, содержащий статистические данные, вычисляемые поля …). Конечно, в таком случае код по обновлению информации становится достаточно сложным, но растут и возможности по планированию удобного интерфейса пользователя. Паттерн Active Record очень похож на Row Data Gateway, так, что их часто путают. Основное отличие в том, что объект, привязанный к записи таблицы в случае Row Data Gateway, содержит методы только для чтения информации из базы, сохранения и удаления. Но не содержит методов специфических для предметной модели, которую моделирует наша БД. Например, у вас есть перечень товаров на складе, каждый товар характеризуется количеством. Объекту-записи в случае Row Data Gateway нет никакого дела до того чему равно это поле “количество”. А объект Active Record должен знать и учитывать, что поле “количество” не может быть отрицательным и не может быть больше, например, ста тысяч, поскольку на складе просто нет больше свободного места. Вопрос о том, где хранить логику приложения на клиенте (в виде специальных методов вроде “проверитьКоличество”) или же перенести логику на сервер не имеет однозначного ответа. Я стараюсь, где только можно создавать хранимые процедуры (процедуры работы или проверки данных размещенные не внутри программы, а на сервере СУБД). Это дает плюсы в скорости работы, легкости обновления и внесения исправлений в одном месте – сервере, а не в тысяче разрозненных мест – программ-клиентов. С другой стороны, может быть, вы пишите небольшое приложение, работающее с СУБД, которая просто не поддерживает хранимые процедуры. Или же языковые средства написания хранимых процедур слишком примитивны, получающийся код проверки громоздок и неудобочитаем. Поэтому я с большей радостью приветствую сложившуюся тенденцию внедрять в ядро современных СУБД возможность писать код для хранимых процедур, триггеров не на косноязычном “tsql” или “pl/sql”, а на универсальных языках (java, c#, php). При условии, конечно, что потери скорости не слишком велики. Перенос логики на сторону клиента может быть и вынужденным в случаях, когда этап вноса изменений в СУБД сложен или отложен по времени, например, связь выполняется по низкоскоростным линиям, или изменения накапливаются в течении нескольких часов и только затем (пакетом) сохраняются на сервере БД – в этих случаях следует проверять вводимые данные на предмет корректности как можно скорее и до фактической отправки их на сервер.

Всем хороша рассмотренная в прошлый раз библиотека dbSimple. Удобно писать код отбора данных, проще защищать свое приложение от sql-injection, приятно работать с функциями отбора данных из БД, возвращающими привычные для php массивы. Вот только остается открытым вопрос о том, как быть, если нам нужны не только выборки данных, но и их модификация: нашли запись в таблице товаров по, например, цене, внесли изменения в поля записи и сохранили ее назад в БД. Для того чтобы изменить запись используется команда update:
  1. UPDATE имя_таблицы SET поле = новое_значение WHERE условие_отбора
Итак, для того чтобы иметь возможность для некоторой записи внести изменении нам нужно “условие отбора”. Традиционно для этого используется первичный ключ таблицы – поле или их наименьший набор позволяющий гарантированно однозначно отличить одну запись от другой. Значит везде, где планируется обновлять данные, мы должны сохранять сведения о том, какое поле – первичный ключ и чему оно равно для данной записи. Я хотел бы, чтобы каждой записи таблицы соответствовал свой объект. Я хочу, чтобы этот объект можно было передавать из одной функции в другую, хочу, чтобы объект можно было сохранить в файле или сессии. Честно говоря, здесь одно из слабых мест паттернов Row Data Gateway и Active Record – они слишком привязаны к структуре БД и подчиняются правилу: “одна таблица – один класс для работы с ней”. Частично эта проблема решается с помощью средств DataMapper/ORM (средств отображения таблиц-реляций на объектную модель), но об этом не сейчас. Сегодня же мы познакомимся с библиотекой adodb.

В большинстве популярных статей рассказ начинается со слов, что adodb – абстрактный класс для доступа к БД. А в качестве “затравки” приводят рассказ: “представьте, что вы сделали сайт под mysql, а на хостинге вдруг оказался postgres и только благодаря adodb вы не переписали с нуля весь код вашего сайта”. Свое негативное отношение к подобному “вдруг” и “выравниванию sql-кода” я высказал еще в первой статье серии. С другой стороны подобная абстракция от того, с какой СУБД мы работаем, необходима для ряда больших и сложных продуктов, например, “коробочных” систем управления сайтами. Если вы вложили силы и деньги во что-то большое, то, очевидно, вы хотите продать его наибольшему количеству клиентов, и сосредотачиваться на одном mysql не рационально. У вас теплится мысль продать этот продукт и для хостингов, где используется postgres и там, где firebird, oracle, mssql. Чем больше баз, под которыми ваш код будет работать, тем лучше. С другой стороны, вы не хотите нести дополнительные затраты по ведению нескольких различающихся версий программы для каждой из СУБД. В качестве первейшего возражения против adodb или подобной ему Pear:DB, приводят фактор снижения производительности. Согласен (каждый слой абстракции добавляет лишние траты памяти и ресурсов процессора), но, с другой стороны, ведь не все мы пишем сайты работающие под высокими нагрузками. А если пишите, то вы будете вынуждены использовать специальные “фишки” вашей СУБД, чтобы выиграть еще пару процентов скорости. Если использование adodb позволит увеличить вашу производительность и сделать за месяц не один, а два сайта – используйте adodb смело. А производительность? А кэширование вам на что? А зачем существуют продукты вроде zend optimizer или phpaccelerator? Кроме того, adodb можно найти практически в любом известном php framework-е или cms. Также adodb может работать под php4 и под php5. Так что если вы еще ни разу не сталкивались с adodb, то самое время с ним познакомиться. Начните с того, что скачайте с домашнего сайта проекта http://adodb.sourceforge.net/ архив с библиотекой и распакуйте его у себя на веб-сервере. Неплохо обзавестись и документацией посвященной этой библиотеке, английскую версию можно скачать на том же сайте, а если вам больше по душе русскоязычный текст, то добро пожаловать на http://kuzma.russofile.ru/ (перевод несколько косноязычен, но в целом хорош). Если вы внимательно читали мои прошлые статьи, посвященные simpleDB, то вы без труда разберетесь в том, как использовать и adodb. Дело в том, что библиотека может быть условно поделена на две части: первая из них посвящена уже привычным нам возможностям. Например, унифицированная система указания того, с какой СУБД мы хотим работать и необходимыми для подключения сведениями (имя, пароль, хост). Средства записи sql-запросов содержащих специальные placeholder-ы вместо которых подставляются значения с учетом экранирования спец. символов, добавления кавычек. Также средства позволяющие возвращать результат запроса в виде массивов php. Поэтому я не буду останавливаться на этой теме долго и просто приведу пару примеров. Прежде всего, подключите библиотеку и создайте объект подключения к СУБД, здесь обратите внимание на параметр, передаваемый внутрь ADONewConnection – имя используемой СУБД. Следующий шаг – подключение к серверу. Строка, подаваемая на вход функции Connect или PConnect, зависит от СУБД. Например, для mysql она будет выглядеть так:
  1. $conn->Connect(‘сервер’, 'пользователь', 'пароль, 'база данных’)
Более удобно при вызове NewADOConnection указать параметром строку DSN, содержащую сведения в следующем формате:
 $driver://$username:$password@hostname/$database?options[=value]
Например, для mysql эта строка DSN будет выглядеть так (пароль в примере пустой):
  1. $conn = &ADONewConnection('mysql://root:@localhost/smf');
Возможно, в ходе работы с adodb возникнут ошибки, следовательно, нам нужен способ для удобного отслеживания происходящих событий. Моделей обработки ошибок несколько и фактически выбор используемой схемы обработки ошибок определяется подключением одного из следующих файлов.
 adodb-errorhandler.inc.php
 adodb-errorpear.inc.php
 adodb-exceptions.inc.php
Прежде всего, если вы пишите под php5, то рекомендуется использовать исключения. Вы подключаете файл adodb-exceptions.inc.php и весь код по работе с СУБД помещаете внутрь секции try catch. Естественно, что выбрасываемый объект ADODB_Exception производен от стандартного для php родового класса всех исключений Exception и вы можете легко узнать что именно случилось и где. Например, так:
  1. include_once('../adodb/adodb.inc.php'); 
  2.  // подключаем библиотеку adodb
  3.  include_once('../adodb/adodb-exceptions.inc.php'); 
  4.  // подключаем схему обработки ошибок основанную на исключениях
  5.  
  6.  //$conn = &ADONewConnection('mysql'); 
  7.  // создаем подключение, обратите внимание на то, что в качестве параметра указывается mysql
  8.  //$conn->Connect('localhost', 'root', '', 'smf'); // выполняем соединение с СУБД
  9.  // а теперь второй способ подключения основанный на DSN
  10.  try{
  11.  $conn = &ADONewConnection('mysql://root:@localhost/smf1'); 
  12.  // создаем подключение, обратите внимание на то, что в качестве параметра указывается DSN
  13.  }
  14.  catch (exception $e){
  15.    print_r($e); // выводим все содержимое объекта исключения
  16.    adodb_backtrace($e->gettrace());// выводим trace вызовов функций приведших к ошибке
  17.    die ($e->getMessage ()); // вводим текст сообщения об ошибке и завершаем скрипт
  18. }
Возможен и другой способ обработки ошибок, когда вы подключаете файл adodb-errorhandler.inc.php. Внутри этого файла определена функция ADODB_Error_Handler, которая выводит текст сообщения об ошибке, а затем повторно “бросает” ошибку уровня E_USER_ERROR с помощью trigger_error. В свою очередь вы можете назначить собственную функцию обработки ошибок с помощью set_error_handler. Последний способ обработки ошибок – в стиле PEAR. После подключения файла adodb-errorpear.inc.php вы можете писать следующий код:
  1. $conn = &ADONewConnection('mysql://root:@localhost/smf1');
  2. if (! $conn){
  3.   $e = ADODB_Pear_Error(); 
  4.  die('<p>error-connect:'. $e->message . '</p>'); }
Полезным на стадии отладки кода является включить debug-режим:
  1. $conn->debug = true;
В этом случае на экран будут выводиться сообщения, какие команды были отправлены на сервер (см. рис. 1).



После подключения к серверу вы должны посылать команды на отбор или модификацию данных. Для этого служит функция Execute. Первым параметром функции идет строка sql, внутри которой помечены точки вставки данных с помощью “?” (точно как и в simpleDB), переданных как второй параметр.
  1. $recordSet = &$conn->Execute('select * from users where fio = ? or friends > ?', array('bill', 2));
Возможен вариант синтаксиса с именованными placeholder-ами, как в примере ниже (к сожалению, этот синтаксис не поддерживается mysql, только oracle и firebird):
  1. $recordSet = &$conn->Execute('select * from users where fio = :fio or friends > :friends', 
  2.    array(‘fio’ => 'bill', ‘friends’ => 2)
  3. );
Неприятно, что при указании списка параметров следует контролировать их тип данных, так если бы я для свойства friends указал бы значение в кавычках “2” , то и в посланной на сервер строке sql число два было бы заключено в кавычки.

Команда Execute позволяет не только посылать команды на отбор данных, но и на их модификацию, в этом случае приятно использовать механизм bulk binding. Под этим грозным названием скрывается довольно простая идея: передать как список подставляемых переменных несколько массивов, например, так:
  1. $arr = array(
  2. array('Bill',12),
  3. array('George', 4),
  4. array('Lisa', 7));
  5. $inserted = $conn->Execute('insert into users (fio,friends) values (?,?)',$arr);
После вставки данных бывает полезным определить, какой номер был автоматически сгенерирован и назначен полю с модификатором auto_increment или identity.
  1. print 'auto_increment id:' . $conn->Insert_ID ();
А если вы запустили запрос на модификацию или удаление записей, то, используя функцию Affected_Rows(), сможете определить количество записей, которые были обработаны таким запросом.

Если шел запрос на выборку данных, то отобранные записи будут помещены внутрь объекта ADORecordSet, методы которого перечислены далее:

EOF – здесь хранится признак того до конца всех найденных записей

Fields – массив в котором хранятся значения текущей записи. Вид массив определяется изменением конфигурационной переменной “$conn->setFetchMode(…)”. В качестве параметра указывается константы ADODB_FETCH_NUM или же ADODB_FETCH_ASSOC или же ADODB_FETCH_BOTH. Точь-в-точь как во встроенной библиотеке php/mysql указывается будут ли поля помещены в массив на основе их порядковых номеров, или же массив-запись будет ассоциативным с ключами равными именам полей, третий вариант – комбинация первых двух.

RecordCount – функция возвращает количество строк полученных в результате выполнения запроса.

MoveNext – приводит к переходу к следующей записи, так что в массив fields помещается следующая порция информации, здесь же меняется значения “флажка” EOF.

Далее приводится пример использования ADORecordSet:
  1. // отправили запрос на сервер
  2. $res = $conn->Execute("SELECT * FROM users");
  3.  
  4. while (!$res->EOF) {
  5.   // цикл до тех пор пока не кончатся отобранные записи
  6.   print "fio = ".$res->fields['fio']."\n";
  7.   // распечатыаем значения полей
  8.   print "friends = ".$res->fields['friends']."\n";
  9.   print "sex = ".$res->fields['sex'];
  10.   // не забываем переместиться к следующей записи
  11.   $res->MoveNext();
  12. }
Выборки данных можно кэшировать, например, так:
  1. $conn->cacheSecs = 3600*24;// устанавливаем срок хранения информации в КЭШе в 24 часа 
  2. $rs = $conn->CacheExecute('select * from table');
  3. // делаем запрос, только если информации нет в КЭШе то она и будет взята из СУБД
Как некоторая альтернатива dbSimple возможно сказать, что результат выполнения выборки данных следует вернуть не в виде объекта ADORecordSet, а с помощью двумерного массива содержащего все отобранные записи:
  1. $all = $conn->getAll(‘select * from users’);
На этом общее знакомство с adodb я считаю законченным, и переходим ко второй части его возможностей: activerecord. Первым делом подключите библиотеку adodb-active-record.inc.php, в которой находится класс ADOdb_Active_Record. Его назначение – выполнять прозрачное преобразование между объектами php и записями в таблице БД.

Итак, если у меня есть таблица со сведениями о людях:
  1. CREATE TABLE `users` ( 
  2.    `id` int NOT NULL AUTO_INCREMENT, 
  3.    `fio` varchar(50), 
  4.    `sex` enum('f','m'), 
  5.    `weight` float, 
  6.    `friends` int, 
  7.     PRIMARY KEY (`id`)
  8. )
То я хотел бы создать объект PHP со свойствами, названия которых такие же, как и поля этой таблицы (id, fio, sex, weight, friends). В состав объекта должны входить методы позволяющие искать в таблице запись по некоторому критерию, вносить в нее изменения, удалять запись. В следующем примере я создаю класс Users производный от ADOdb_Active_Record, это значит, что всякий экземпляр данного класса будет способен сохранять себя внутри таблицы, имя которой указано как параметр конструктора.
  1. include_once('../adodb/adodb.inc.php'); // подключаем библиотеку adodb
  2. include_once('../adodb/adodb-errorhandler.inc.php');
  3. require_once('adodb/adodb-active-record.inc.php');
  4.  
  5. $db = NewADOConnection('mysql://root:@localhost/smf');
  6. // привязываем ActiveRecord подсистему к конкретному соединению с БД
  7. ADOdb_Active_Record::SetDatabaseAdapter($db);
  8.  
  9. // создаем класс соотвествующий таблице Users
  10. class Users extends ADOdb_Active_Record{}
  11.  
  12. // в качестве параметра конструктора передается имя таблицы
  13. $vasya = new Users('users');
  14. $vasya->fio = 'Antony';// здесь мы назначаем свойства объекту
  15. $vasya->sex = 'm';
  16. $vasya->friends = '13';
  17. $vasya->weight = 80;
  18. $vasya->Save ();// сохраняем запись в БД
Для удобства написания кода в средах разработки подобных zend studio (удобство заключается в подсказках при наборе кода), можно использовать прием с объявлением полей класса с такими же именами, как и поля таблицы. Пример подсказки показан на рис. 2.



На сегодня хватит. В следующий раз я продолжу рассказ о возможностях adodb и паттерне activerecord.