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

December 3, 2007

В прошлый раз я начал рассказ о паттернах Active Record и Row Data Gateway. Сегодня мы продолжим и завершим рассмотрение возможностей библиотеки adodb (ставшей стандартом де-факто и применяемой в разработке множества известных и не очень веб-приложений: postnuke, xaraya, moodle). Библиотеке, которая позволяет нам писать код быстрее и с меньшим числом ошибок. Также я уделю внимание вопросу оценки производительности вашего sql-кода.

Прошлую статью я закончил простеньким примером кода использующий возможности adodb. Мы создали класс php, поля которого соответствовали колонкам таблицы mysql. Класс должен был наследоваться от встроенного в adodb класса ADOdb_Active_Record. На имя класса накладывается ограничение – оно должно быть построено на основе имени таблицы БД. Так для таблицы users, имя класса должно быть user. Это не всегда удобно, например, когда имена таблиц предваряются опциональным префиксом. В этом случае, укажите в качестве параметра конструктора имя таблицы. Или же можно установить значение специальной переменной $_table в составе объекта класса равной имени таблицы. Так три приведенных ниже способа создания новой записи являются равноценными:
  1. include_once('../adodb/adodb.inc.php');// подключаем библиотеку adodb
  2. require_once('../adodb/adodb-active-record.inc.php');
  3.  
  4. $db = NewADOConnection('mysql://root:@localhost/smf');
  5.  
  6. // привязываем ActiveRecord подсистему к конкретному соединению с БД
  7. ADOdb_Active_Record::SetDatabaseAdapter($db);
  8.  
  9. // Вариант 1 - имя таблицы строится на основании имени класса
  10. class User extends ADOdb_Active_Record {
  11. }
  12. // в качестве параметра конструктора передается имя таблицы
  13. $obj_1 = new User();
  14. $obj_1->fio = 'Billi';
  15. $obj_1->Save ();
  16.  
  17. // Вариант 2 - имя таблицы задается как параметр конструктора
  18. class Person extends ADOdb_Active_Record {
  19. }
  20. $obj_2 = new Person('users');
  21. $obj_2->fio = 'Billi';
  22. $obj_2->Save ();
  23.  
  24. // Вариант 3 - имя таблицы задается как значение поля класса _table
  25. class Human extends ADOdb_Active_Record {
  26.  var $_table = 'users';
  27. }
  28. $obj_3 = new Human('users');
  29. $obj_3->fio = 'Billi';
  30. $obj_3->Save ();
При создании объекта adodb выполняет незаметный для вас запрос к СУБД и узнает то, какие поля есть в таблице привязанной к заданному классу, так, далее я вывожу массив с именами полей:
  1. print_r ($obj_3->GetAttributeNames ());
Естественно, что эта информация запрашивается только один раз для всех объектов единого класса, так чтобы не нагружать СУБД повторяющимися запросами к метаинформации. Для корректной работы adodb необходимо, чтобы в таблице был определен первичный ключ. Если это не так (честно говоря, я просто не представляю себе, чем можно оправдать отсутствие в таблице первичного ключа PRIMARY KEY), то необходимо указать adodb то, какие поля будут играть роль первичного ключа – это задается вторым параметром конструктора объекта. Традиционно на роль PRIMARY KEY подходят уникальные индексы (UNIQUE). Ну а если и это вы не сделаете, то любые действия с adodb Active Record будут завершаться ошибкой.

После создания объекта вы указываете значения его атрибутов и вызываете метод Save. Save сам определяет нужно ли добавить запись в таблицу или изменить уже существующую, например, так:
  1. $obj_3 = new Human('users');
  2. $obj_3->fio  = 'Billi';
  3. $obj_3->Save ();// Добавляем запись
  4. $obj_3->fio  = 'Willi';
  5. $obj_3->Save ();// Обновляем запись
Если добавить запись не удалось, например, из-за ограничений на значения полей, то метод Save вернет false, в случае успеха – true. В случае модификации записи, возвращаемые значения могут принимать следующие значения: 0 – операция обновления не удалась, 1 – изменения были внесены и, наконец, -1 когда в отправке запроса к серверу нет необходимости – значения полей остались прежними. Для того чтобы узнать, почему именно изменения внести не удалось, используйте следующую запись:
  1. $if_ok = $obj_3->Save();
  2. if (!$if_ok)  
  3.   $err = $obj_3->ErrorMsg();
Если к тому же значением свойства debug объекта соединения с БД будет true, то текст сообщения об ошибке будет напечатан на экран браузера вместе со стеком вызовов приведших к ошибке (см. рис. 1).



Естественно, что adodb должен содержать удобные средства для поиска информации в таблице. Например, я хочу найти запись о человеке, у которой значение поля fio равно “Billi”, и изменить дату рождения. Есть два метода умеющих искать записи: Load и Find. В качестве параметра Load передается строка, содержащая условие WHERE, например, так:
  1. $obj_4 = new Human('users');
  2. $obj_4->Load('fio = "billi"');
  3. // второй вариант с placeholders
  4. $obj_5 = new Human('users');
  5. $obj_5->Load('fio = ?' , array ('Willi"and\'Ron'));
Второй способ, когда строка запроса содержит placeholder-ы (знаки “?”), вместо которых подставляются значения из массива переданного как второй параметр методу Load более удобен и избавляет от скучной возни с кавычками и экранированием спец.символов. Очевидно, что строка-условие может быть сколь угодно сложным, например, таким:
  1. if ($obj_5->Load('fio like ? and weight between ? and ?' , array ('%Willi%', 500, 2000)))
  2.  print_r($obj_5);
  3. else 
  4.  print 'cannot Load record';
Если найти запись по условию не удалось, то метод Load возвращает false. Если было найдено несколько записей удовлетворяющих условию, то внутрь объекта obj_5 будет загружено содержимое первой попавшейся записи. Если же вам, наоборот, нужно найти множество записей по некоторому условию, то используйте метод Find. Параметры метода идентичны описанному выше методу Load:
  1. $list_of = $obj_5->Find('fio like ?' , array ('%Billi%'));
Транзакции. Если в базах вы не новичок, то должны знать, уметь пользоваться и любить транзакции или Logical Unit Of Work. Всякий раз, когда нужно выполнить набор взаимосвязанных модификаций таблиц, следует послать команду “начало транзакции”, затем вы выполняете действия, и, наконец, подводите итог: если все шаги завершились успешно, то транзакция закрепляется, и изменения сохраняются в базе данных. Иначе изменения “откатываются” или отменяются сразу для всех действий внутри транзакции. Не секрет, что не все СУБД поддерживают транзакции, и даже mysql не исключение. Так для таблицы типа myisam попытка откатить внесенные в ходе транзакции изменения будет неудачна, а для таблиц innodb все получится. Интересно, как работает с транзакциями adodb? Являются ли встроенные в него методы RollbackTrans, StartTrans, CompleteTrans простыми “перевызывалками” стандартных средства СУБД, или же изменения отслеживаются и откатываются самой adodb без зависимости от типа СУБД и особенностей ее таблиц? Я попробовал запустить следующий код:
  1. $db->StartTrans ();
  2. // начали транзакцию
  3.  
  4. $obj_4 = new Human('users');
  5. // меняем значения полей
  6. $obj_4->Load('fio = "billi"');
  7. $obj_4->sex = 'm';
  8. $obj_4->weight = 120;
  9. $obj_4->Save ();
  10.  
  11. // откатываем изменения
  12. $db->RollbackTrans ();
И получил сообщение об ошибке
 Transactions not supported in 'mysql' driver. Use 'mysqlt' or 'mysqli' driver
Послушно заменив в строке подключения к СУБД тип драйвера с mysql на mysqli

$db = NewADOConnection('mysqli://root:@localhost/smf');

Я заметил, что adodb просто-напросто отправляет на сервер команды BEGIN, COMMIT, ROLLBACK при вызове методов StartTrans, CompleteTrans, RollbackTrans. Соответственно, если СУБД не поддерживает транзакции, то и ADODB вам ничем не поможет.

Реализацию active record внутри adodb не стоит воспринимать как очередную “серебряную пулю”. В действительности стиль работы active record бывает слишком “примитивным” по сравнению с отправкой классических sql-запросов. Например, для того чтобы всем сотрудникам некоторого отдела увеличить зарплату на 20% на sql достаточно записать следующую строку:
  1. UPDATE users SET salary = salary * 1.2 WHERE otdel = ‘marketing’;
В случае adodb придется получить вытянуть из базы с помощью метода Find массив записей в оперативную память и обработать каждую из них в цикле, примерно, так:
  1. $list_of = $obj_5->Find('otdel = ?' , array ('management'));
  2.  
  3. for ($i = 0; $i < count($list_of); $i++){
  4.   $list_of [$i]->salary = $list_of [$i]->salary * 1.2;
  5.   $list_of [$i]->Save (); 
  6. }
Во втором случае, код становится не только более громоздким, но и главное, очень медленным. В идеале нам нужны такие средства, которые позволяли бы не просто работать с СУБД двумя стилями (стиль php и стиль sql) – это не так уж и сложно и adodb позволяет писать подобный код. А главное – обеспечить синхронизацию между объектами php и информацией в таблице. При изменении значений в таблице объекты должны отследить изменения и подгрузить новые сведения внутрь своих полей – а вот это очень-очень сложно.

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

Давайте посмотрим, какие средства предусмотрены в составе adodb для мониторинга за выполняемыми запросами?

Так что, только в составе adodb есть средства для мониторинга за состоянием mysql? Нет, конечно, есть множество специализированных средств для решения этой задачи. В простейшем случае на сайте mysql.com вы можете скачать утилиту “MySQL Administrator”. Частью ее возможностей (кроме управления правами доступа, возможностей управлять backup-ами БД, удобных, действительно, удобных и понятных средств редактирования конфигурационных файлов mysql) является средства мониторинга. На закладе Health вы видите график загруженности mysql: количество обслуживаемых запросов в единицу времени, количество активных соединений и трафик. Выводятся также и сведения об эффективности работы в mysql КЭШа запросов.



Единственная сложность в том, что часто хостинги блокируют соединения с mysql с посторонних машин сети (не принадлежащих хостеру). Кроме того имеет смысл оценивать производительность в комплексе т.е. с учетом затрат которые вносит само adodb, а не только “чистое” время выполнения запроса на сервере. Как ожидалось, Adodb не осталась в стороне от вопроса мониторинга. Так в ее состав входит компонент, формирующий html-страницу для оценки основных параметров производительности. Все запросы, которые обрабатываются adodb, могут быть сохранены в специальный журнал – таблицу с именем adodb_logsql. Эта таблица будет автоматически создана, при первом вызове метода LogSQL (true) для объекта соединения. В таблице хранятся сведения о дате и времени выполнения запроса, собственно тексте запроса, значениях bind-переменных и времени выполнения запроса. Отделение текста запроса от подставляемых в него параметров очень важно т.к. дает возможность собирать находить одинаковые запросы и получить статистические сведения о том, какие из них заняли больше всего времени. После чего, вооружившись командой EXPLAIN, вы можете понять то, как выполняет запрос сервер, и подсказать ему лучший алгоритм, например, добавив нужные индексы. В следующем примере показано как включить механизм журналирования запросов. Единственная недоработка заключается в том, что adodb пытается записывать сведения в таблицу-журнал без предварительной проверки того, существует ли таблица. Поэтому если вы включили обработку ошибок, то первая же попытка сделать запись в журнал приведет к аварийному завершению всего скрипта.
  1. // отключаем обработку ошибок
  2. //include_once('../adodb/adodb-errorhandler.inc.php');
  3.  
  4. // подключаем модуль анализа производительности
  5. include_once('../adodb/adodb-perf.inc.php');
  6.  
  7. // данный прием позволяет переопределить имя таблицы, 
  8. // в которую будет сохраняться информация журнала
  9. adodb_perf::table('my_logsql_table');
  10.  
  11. // создаем подключение
  12. $db = NewADOConnection('mysqli://root:@localhost/smf');
  13.  
  14. $db->debug = true;
  15. // включаем режим журналирования и выводим на экран старое значение данного параметра
  16. print_r ($db->LogSQL (true));
  17.  
  18. //Теперь после накопления статистики мы можем написать очень короткий скрипт отображающий
  19. // html-интерфейс для доступа к накопленным сведениям.
  20.  
  21. $perf = NewPerfMonitor($db);// создаем объект визуализации счетчиков
  22. echo $perf->SuspiciousSQL();
  23. echo $perf->ExpensiveSQL();
Результат работы скрипта показан на рис. 3.



При вызове методов ExpensiveSQL и SuspiciousSQL можно указать в качестве параметра число – сколько записей самых “дорогостоящих” и “подозрительных” (запросы, чье среднее время выполнения достаточно велико) запросов отобразить на экране браузера. Также в составе PerfMonitor есть методы. HealthCheck – выводит сведения об размере КЭШа и частоте попадания в него, также число подключений и их предельное значение. Следующий метод InvalidSQL – печатает таблицу с перечислением тех запросов, которые не удалось выполнить из-за каких либо ошибок, например, нарушения ограничений на значения полей таблицы. Естественно, что это не все возможности adodb, но все же дальнейший рассказ о нем я прекращу. Дело в том, что adodb местами напоминает свалку, где среди множества полезных вещей встречаются и предметы с непонятным назначением и довольно дрянного качества. К их числу я отношу возможность создать веб-интерфейс с paging-ом содержимого таблицы. Также возможность экспорта информации в csv-формат. Не радует и функция rs2html – формирующая html-таблицу с содержимым таблицы БД. С другой стороны, может быть полезным механизм Pivot Tables – возможность создания перекрестных таблиц, если конечно его доведут до ума. Средства кэширования нуждаются в тщательной проработке, как и многое другое.

Напоследок, я приведу прототип организации работы с adodb activerecord, который я часто использую у себя в работе. Его назначение – создать незаметную прослойку между adodb и нашим кодом php так, чтобы вносимую в базу данных информацию проверять на предмет корректности и сложных отношений. Не всегда возможно реализовать контроль за информацией только средствами СУБД. Т.к. не у всех СУБД есть хранимые процедуры, и часто код ее слишком сложен, для проверки могут потребоваться данные не доступные из СУБД (конфигурационные файл). Можно попробовать пожертвовать скоростью работы приложения в стремлении повысить ее “подхватываемость”. Под этим странным термином я понимаю задачу не потерять темпы развития проекта при смене части команды.
  1. // создаем интерфейс валидатора, всякий код знающий о правилах накладываемых на информацию должен
  2. // реализовывать данный интерфейс 
  3.  
  4. interface ActiveValidator { 
  5.   public function  Validate ($rec_obj);
  6. }
  7.  
  8. class  Validate_Of_ADOdb_Active_Record extends  ADOdb_Active_Record {
  9.  
  10.  // статический массив объектов валидаторов
  11.  private static $validators = array ();
  12.    public static function addValidator (ActiveValidator $val){
  13.    self::$validators [] = $val;
  14.  }
  15.  
  16.  public function Save (){
  17.   for ($i = 0; $i < count(self::$validators); $i++)
  18.    if (! self::$validators [$i]->Validate ($this))
  19.      // если хотя бы один тест провален - запись не сохраняется
  20.      return false;
  21.    ADOdb_Active_Record::Save ();	
  22.  } 
  23.  
  24. } // --- end of class --- 
  25.  
  26. // объект валидатор основанный на длине некоторого поля
  27. class LengthValidator implements ActiveValidator {
  28.  private $max_len_of;
  29.  private $field_name;
  30.  
  31.  public function __construct ($max_len_of, $field_name){
  32.   $this->max_len_of = $max_len_of;
  33.   $this->field_name = $field_name;
  34.  }
  35.  
  36.  public function  Validate ($rec_obj){
  37.   $tmp = get_object_vars ($rec_obj);
  38.   return strlen($tmp[$this->field_name]) < $this->max_len_of;
  39.  } 
  40.  
  41. }  // ---- end of class ---
  42.  
  43. class User extends Validate_Of_ADOdb_Active_Record {
  44. }
  45.  
  46. // привязываем к класу User объект валидатор свойств
  47. User::addValidator(new LengthValidator (20, 'fio'));
  48. // значение поля fio должно не превосходить 20 символов
  49. $obj_1 = new User();
  50. $obj_1->fio  = 'Billi';
  51. $obj_1->Save ();
  52.  
  53. //здесь прозрачно для нас срабатывает валидация и объект не 
  54. //будет помещен в базу, если хотя бы один из присоединенных тестов будет провален.
Следующая статья серии будет посвящена Propel. Это известный php framework (является реализацией паттерна Data Mapper) служит для отображения хранящихся в таблицах БД записей на объекты php.