« Доступ к базам данных из php. Часть 6 | Доступ к базам данных из php. Часть 8 » |
Доступ к базам данных из php. Часть 7
В прошлый раз я обещал начать рассказ о паттерне 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 помещенного в текущий каталог. Этот файл содержит объявления того, какие таблицы есть в базе данных, какие поля у этих таблиц и как они между собой связаны. Весь текст этого файла я приводить естественно не буду, но и умолчать об основных тегах нельзя.
<database name="firma01">
<table name="departments" idMethod="native">
<column name="DepartmentID" type="INTEGER" required="true" autoIncrement="true" primaryKey="true">
<column name="DepartmentName" type="VARCHAR" size="100">
</source >
Внутри тега table отвечающего за создание таблицы сотрудников (users) находится интересный тег:
<source lang="xml">
<foreign-key foreignTable="departments" onDelete="CASCADE" onUpdate="CASCADE">
<reference local="DepartmentID" foreign="DepartmentID"/>
</foreign-key>
При запуске сценария 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 со следующим содержимым:
<?xml version="1.0" encoding="utf-8"?>
<config>
<log>
<ident>propel-firmaproject</ident>
<level>7</level>
</log>
<propel>
<datasources default="firmaproject">
<datasource id="firmaproject">
<adapter>mysql</adapter>
<connection>
<dsn>mysql://root@localhost/firma01</dsn>
</connection>
</datasource>
</datasources>
</propel>
</config>
Примечание: здесь я допустил небольшую ошибку скопировав в файл примера runtime-conf.xml строку подключения в стиле creole, а не в стиле pdo (в следующей статье я поправлюсь, но умолчать об своей ошибке я не могу.)В этом файле хранятся сведения о подключении с СУБД. Зачем? Ведь мы уже указывали параметры подключения в файле build.properties? Дело в том, что файл build.properties нужен для работы propel на стадии генерации схемы БД или наборов классов, а для собственно работы с библиотекой propel, нужно создать специальный файл с конфигурацией подключения. Этот файл создается при запуске цели “propel-gen ./”и помещается по следующему пути “build/conf/firmaproject-conf.php”.
На сегодня хватит. Мы полностью рассмотрели подготовительные действия перед началом собственно использования propel. В следующей статье серии я продолжу рассказ о propel. Нам осталось рассмотреть приемы поиска и отбора информации, методы работы с журналами, поговорить о производительности propel-основанных программ.
« Доступ к базам данных из php. Часть 6 | Доступ к базам данных из php. Часть 8 » |