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

December 12, 2007

В прошлый раз я обещал начать рассказ о паттерне Data Mapper и поддерживающей его библиотеке propel. Эта библиотека будет последней, которую мы рассмотрим в рамках серии посвященной доступу к БД из php. Естественно, что заслуживающих внимание библиотек еще очень много, но все базовые идеи (паттерны), которые лежат в их основе, мы уже прошли, разобрали наиболее ярких представителей каждого из подходов, так что пора завершать.

Если вы пишите на php код в стиле ООП, то перед вами стоит проблема отображения хранящейся в базе данных информации (реляционные структуры) на объектные структуры. И причем отображение, не столь примитивное, как мы видели в adodb (active record), когда одна таблица отображалась на один класс. Так что все объекты данного класса являлись представителями записей некоторой таблицы и … и все. В реальности наша база это не просто набор таблиц, но и отношений между ними. Таблицы связаны между собой отношениями один-к-одному, один-ко-многим, многие-ко-многим. Эти связи не оказывали никакого влияния на объекты adodb – и это плохо. С другой стороны, когда мы пишем php-код, то используем наследование, коллекции, мы создаем сложную логику, контролирующую правила по которым информация устроена, и снова возникает разрыв между базой данных и использующим ее php-кодом. Решений класса Data Mapper играют роль “прозрачных” посредников между базой и php-кодом, их назначение - загрузка из базы иерархий объектов, так чтобы наш код мог работать с данными, не думая о том, откуда они взялись. А после того как набор объектов был модифицирован, то посредник выполняет их сохранение в базе, “раскладывая” иерархию на отдельные записи в наборе таблиц.

В последние два-три года ярко проявилась тенденция к переносу в мир php решений, апробированных в других языках (прежде всего java). Ускорению этого процесса способствовала и улучшенная объектная модель php5, ставшая гораздо ближе к своему “большому брату” java. Так phpDocumentator можно считать “потомком” javadoc – системы подготовки документации из java, основанной на извлечении из файлов с исходными текстами комментариев особого вида. Известная в java система написания сценариев (управляющих компилированием, развертыванием, тестированием) ant дала для php систему phing с таким же назначением и сходным синтаксисом. Система автоматизации тестирования java-кода junit стал толчком для аналогичного решения в мире php – phpunit.И, наконец, java проект torque породил propel. Propel доступен только для php5 и входит в состав ряда известных php framework-ов, например, symfony. Propel может работать не только с одним mysql, но и со следующими СУБД: mssql, oracle, postgres, sqlite. Это обеспечивается за счет creole и PDO – библиотеки абстрактного доступа к БД (помните, adodb также представляла функции абстракции от конкретной СУБД). Прежде всего, на сайте http://propel.phpdb.org/ вам необходимо скачать архив с библиотекой (я буду рассказывать о версии 1.3) и распаковать его. Для работы нам потребуется база данных с набором взаимосвязанных таблиц. Я создал четыре таблицы: users (сотрудники), departments (отделы), professions (профессии), users_2_professions (связь между сотрудниками и профессии). Поля таблиц и связи между ними показаны на рис.1 и рис. 2.





Вкратце, сотрудники принадлежат одному из отделов (связь один-ко-многим). У сотрудников есть профессия, но очевидно, что у одного сотрудника может быть несколько профессий, также как и некоторая профессия может быть у множества людей. Следовательно, связь между этими двумя таблицами – многие-ко-многим и для реализации ее нам потребуется промежуточная таблица users_2_departments. В ее состав я также ввел поле skill – уровень владения заданным сотрудником своей профессией. Остальные поля таблиц очевидны и в комментариях не нуждаются.

Перед тем, как я начну показ “шаг-за-шагом” как использовать propel следует закрыть пару идеологических вопросов. Почему ORM используются так редко? Я сразу отбрасываю в сторону варианты ответа вроде “это так сложно”, “я не понял, как эта штука работает”, “у нас на фирме так не принято” и т.д. Это проблемы не ORM и propel в частности, а ленивых программистов, не желающих постоянно совершенствовать свои навыки, а может ни разу в жизни не писавших ничего сложнее “hello world”. Когда критикуют ORM/DataMapper говорят о двух ключевых недостатках: скорость работы и повышенные затраты памяти, а также проблемы с написанием действительно сложных запросов на отбор информации.

Затраты ресурсов для DataMapper действительно гораздо больше чем для ActiveRecord и вот почему. В нашем примере с отделами и сотрудниками задано отношение один-ко-многим от отдела к списку людей работающих в нем. Когда я делаю запрос ищущий отдел, например, по его названию, то из базы будет отобрана не только одна единственная запись, а также все записи людей, которые работают в этом отделе. Ведь в общем случае у объекта “отдел” должен быть какой-то метод, вроде “получить_сотрудников()” возвращающий список таковых. В свою очередь у сотрудника есть сведения об профессиях которыми он владеет, так что в состав объекта “сотрудник” следует ввести метод “получить_список_профессий”. Объект “профессия”, в свою очередь, будет зависеть от чего-то еще и так до бесконечности. Фактически желая выбрать одну запись/объект из базы, мы вытянем чуть ли не половину хранящейся в БД информации – и это не нормально. Поэтому придумали концепцию lazy loading – ее идея в том, что объекты, подчиненные родительскому, загружаются не одновременно с созданием объекта-родителя, а только при необходимости. Так в нашем примере лишь, когда у объекта “отдел” был вызван метод “получить_сотрудников”, лишь тогда DataMapper должен прозрачно и незаметно обратиться к СУБД и вытянуть из нее подчиненные записи с перечнем работников. Рядом с lazy loading стоит проблема кэширования. Как быть если есть два запроса, которые отбирают иерархии объектов имеющие общие части? Следует ли кэшировать эти общие части или выбрать их заново? А как быть, если эти запросы посылаются из двух потоков или двух одновременно работающих процессов? А как часто следует обнулять кэш и загружать данные из СУБД? Простого ответа нет ни на один из этих вопросов. Одна из наиболее трудно отлавливаемых проблем в DataMapper – это либо слишком агрессивное кэширование, либо наоборот повышенная нагрузка на сервер, лишние выборки за счет слабого кэширования.

Второй недостаток ORM/DataMapper возникает тогда, когда необходимо “найти сотрудников, зарплата которых больше чем средняя по отделу, но только тех, кто не ездил в командировку прошлым летом”. Язык SQL специально разрабатывался для того, чтобы иметь возможность делать подобные выборки. Средства ORM такой функциональности не имеют. И даже если существует некий “конструктор” sql-запросов, то синтаксис его громоздок и неудобочитаем, особенно когда нам потребуются функциональность подзапросов. Часто говорят, что SQL - это промышленный стандарт, тогда как внутренние языки запросов, используемые в ORM, таковыми не являются. На самом деле здесь кривят душой, ведь SQL – похож на “голого короля” из всем известной сказки. Стандарт-то есть, но программные продукты (конкретные сервера СУБД) поддерживают данный стандарт только в небольшой части возможностей (самые общие и простые вещи), затем начинаются различия. Так что с этой точки зрения ORM могут играть роль средства “отвязаться” от зависимости от конкретной СУБД и смены ее в ходе развития проекта (например, по мере масштабирования проекта было принято решение перейти с mysql на oracle). Не нужно бояться смешивать ORM и запросы на SQL, нужно только вынести весь код, отправляющий такие запросы, в отдельный пакет/библиотеку/файл иначе проект станет не управляемым.

Одним словом, “серебряной пули” снова не оказалось, но использование средств ORM позволяет писать сложный код быстрее, и легче его перерабатывать (refactoring). Так в реальности мы не можем спланировать вид СУБД в начале разработки всего проекта, так чтобы она не подвергалась изменениям. В реальности база данных постоянно перерабатывается и, если мы не используем ORM, то возникает трудность согласования кода php работающего с данными с одной стороны и модели данных с другой. Например мы добавили в таблицу некоторое поле, переименовали, поменяли тип и забыли сделать это в классах, массивах, функциях работающих с ним. Для ORM характерно наличие средств автоматизирующих процесс генерации на основании базы данных набора классов, либо наоборот извлечение из объявлений классов информации о правилах хранения этих объектов, с последующим автоматическим созданием набора таблиц, а также автоматическим наполнением воссозданной базы данных имитацией информации с последующим тестированием нагрузки, которую создает ваше приложение. Итак, ORM дает возможность централизовать изменения. Мы тратим больше сил на начальном этапе разработки с тем, чтобы в последующем писать код быстрее.

Для своей работы propel требует выполнить подготовительную работу – создать конфигурационные файлы, в которых описывается то, как таблицы СУБД между собой соотносятся, а также информацию об их полях. Для того чтобы избежать этой несложной, но нудной работы (особенно неприятно забыть подправить конфиг при частых переработках модели данных) в состав propel был введен специальный инструмент propel-gen. Надо сказать, что стиль разработки от существующей базы к классам php не единственно возможный. Так есть вариант, когда вы пишите код php, создаете иерархию классов и на основании которых выполняется генерация БД и таблиц описывающих эти классы. Для начала я создал пустой каталог propelgen, в который поместил файл с настройками соединения с СУБД “build.properties”, и вот его вид:
 propel.project = firmaproject
 # драйвер используемый для подключения к СУБД
 propel.database = mysql
 # строка DSN подключения с СУБД в стиле creole
 propel.database.creole.url = mysql://root@localhost/firma01
Теперь я должен запустить утилиту генерации файла схемы СУБД. Для удобства работы с ней я решил добавить в переменную среды окружения PATH путь к утилите:
 set path=%path%;H:\propelhome\propel-1.3.0beta2\generator\bin\
Также я добавил путь к интерпретатору php, чтобы можно было запускать из командной строки на выполнение файлы php (это необходимо для корректной работы sphing и propel), например, так:
 php имя_файла.php
Тут я столкнулся с небольшой трудностью, которую хочу предупредить у вас. Дело в том, что для работы propel нужен sphing версии 2. У меня на компьютере изначально был установлен XAMPP. XAMPP – известная “упаковка” нужных для веб-разработчика компонент (если вы знакомы с Denver или wamp – то это их аналог): веб-сервер apache+php+mysql+ftpserver+mailserver + много-много разной всячины (библиотек, утилит). В поставке xampp шел phing версии 1.4, который не умеет корректно работать с propel. Так что мне придется загрузить свежую версию phing и самый простой способ это сделать – использовать стандартную для php систему управления библиотеками – pear. Для этого я зашел в каталог, где у меня установлен php, там же находится файл pear.bat и набрал следующие команды:
 pear channel-discover pear.phing.info
 pear install phing/phing
Также для работы propel потребуется библиотека creole, ее я тоже устанавливаю с помощью pear:
 pear channel-discover pear.phpdb.org
 pear install phpdb/creole
 pear install phpdb/jargon
Затем я зашел в каталоге H:\propelhome\gopropel и набрал в командной строке:
 propel-gen ./ creole
В результате был запущен скрипт, выполнивший генерацию файла schema.xml помещенного в текущий каталог. Этот файл содержит объявления того, какие таблицы есть в базе данных, какие поля у этих таблиц и как они между собой связаны. Весь текст этого файла я приводить естественно не буду, но и умолчать об основных тегах нельзя.
  1. <database name="firma01">
этот тег является корневым и содержит имя базы данных
  1. <table name="departments" idMethod="native">
Этот тег задает таблицу, указывает имя таблицы, а также метод генерации идентификаторов. У меня все таблицы mysql используют в качестве первичного ключа поле типа целое число с модификатором auto_increment – значение этого поля (UserID, DepartmentID, ProfessionID) являются постоянно возрастающими числами (1,2,…) и назначаются автоматически самим mysql-сервером, а не программистом.
  1. <column name="DepartmentID" type="INTEGER" required="true" autoIncrement="true" primaryKey="true">
Этот тег определяет собой поле таблицы. Указывается имя поля, его тип, признак того обязательно ли данное поле для заполнения, а также опциональные атрибуты того, что поле является авто-счетчиком и на основе его построен первичный ключ таблицы. Объявление других полей выглядит попроще (всего лишь задано имя, тип данных и их размер):
  1. <column name="DepartmentName" type="VARCHAR" size="100">
  2. </source >
  3. Внутри тега table отвечающего за создание таблицы сотрудников (users) находится интересный тег:
  4. <source lang="xml">
  5. <foreign-key foreignTable="departments" onDelete="CASCADE" onUpdate="CASCADE">
  6.   <reference local="DepartmentID" foreign="DepartmentID"/>
  7. </foreign-key>
Как вы догадались, его назначение – указать на связь между двумя таблицами по общему полю DepartmentID. Указываются имена полей, которые будут использованы для связи, а также правила, по которому при удалении или изменении главной записи (отделы) будет выполнено автоматическое удаление или модификация соответствующих подчиненных записей (сотрудников).

При запуске сценария propel-gen возможно указать не только цель creole – выполняющую генерацию файла xml с описанием схемы для существующей СУБД, но и цели:
 propel-gen ./ sql
Эта цель выполнит создание файла schema.sql содержащего код для конкретной СУБД, который создаст таблицы. Для того, чтобы этот файл схемы выполнить, фактически создать таблицы в БД, используйте цель:
 propel-gen ./ insert-sql
Следующая цель:
 propel-gen ./ datadump
Выполняется для базы данных уже заполненной тестовой информацией и создает файл data.xml, внутрь которого копируются сведения из таблиц БД. Есть и парная для нее цель “propel-gen ./ datasql” генерирующая на основании файла data.xml файлы .sql с кодом вставляющим в таблицы БД информацию.

Однако мы отвлеклись, теперь мы должны создать на основании файла schema.xml набор файлов с кодом php-классов. Для этого я запускаю цель:
 propel-gen ./
в результате чего в папке с проектом появится подкаталог build/classes. Внутри которого будет подкаталог firmaproject (его название совпало с именем проекта заданного внутри файла build.properties), а уж внутри этого каталога будет создано множество файлов:
  • Departments.php
  • DepartmentsPeer.php
  • Professions.php
  • ProfessionsPeer.php
  • Users.php
  • Users2Professions.php
  • Users2ProfessionsPeer.php
  • UsersPeer.php

Как вы видите каждой таблице были поставлены в соответствие два файла: имя_таблицы.php и имя_таблицыPeer.php. Назначение первого из них – представлять собой отдельную запись из таблицы. Второй служит для выполнения операций (загрузка, поиск, сохранение) над данными. Эти классы пусты – они не содержат ни единого метода или свойства, но зато наследованы от автоматически сгенерированных классов реализующих нужную функциональность и размещенных внутри подкаталога map. Вы можете модифицировать эти классы, добавляя им набор нужных для вас полей или специальные методы. Не бойтесь изменять модель БД и выполнять перегенерацию классов целью “propel-gen ./”: ваши изменения не будут утеряны. Среди результатов работы генератора propel вы можете найти и подкаталог map. Его назначение – хранить файлы вида “имя_таблицыMapBuilder.php”. Каждый из этих файлов содержит класс хранящий сведения о том какие поля есть в таблицах БД, характеристики этих полей и ряд другой информации. Вам, скорее всего, ни разу не потребуется разбираться во “внутренностях” этих файлов. Теперь создайте в папке проекта файл runtime-conf.xml со следующим содержимым:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <config> 
  3.  <log>
  4.    <ident>propel-firmaproject</ident>
  5.    <level>7</level> 
  6.  </log>
  7.  <propel>
  8.  <datasources default="firmaproject">
  9.    <datasource id="firmaproject">
  10.      <adapter>mysql</adapter>
  11.      <connection>
  12.        <dsn>mysql://root@localhost/firma01</dsn>
  13.      </connection>
  14.    </datasource> 
  15.   </datasources>
  16.  </propel>
  17. </config>
 Примечание: здесь я допустил небольшую ошибку скопировав в файл примера runtime-conf.xml строку 
 подключения в стиле creole,  а не в стиле pdo (в следующей статье я поправлюсь, но умолчать
 об своей ошибке я не могу.)
В этом файле хранятся сведения о подключении с СУБД. Зачем? Ведь мы уже указывали параметры подключения в файле build.properties? Дело в том, что файл build.properties нужен для работы propel на стадии генерации схемы БД или наборов классов, а для собственно работы с библиотекой propel, нужно создать специальный файл с конфигурацией подключения. Этот файл создается при запуске цели “propel-gen ./”и помещается по следующему пути “build/conf/firmaproject-conf.php”.

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