Применение класса myXTree

Автор: content Понедельник, Апрель 9th, 2012 Нет комментариев

Рубрика: Язык PHP

Вступление

myXTree — это интерфейс к SQL дереву. Об SQL деревьях написано достаточно много (см. ссылки), поэтому достаточно только упомянуть, что в myXTree используется модель вложенных множеств с добавлением ещё одного параметра depth (глубина).

Класс достаточно прост в использовании, в том смысле, что не требует много кодирования. В качестве входных данных для класса используется DOM документ. Передаёте DOM документ классу, и он сохраняет его в SQL дереве. Для выборки данных из SQL дерева используются XPath выражения. На выходе класса опять таки получаете DOM документ.

Почему именно DOM документ? Потому что он тоже имеет древовидную структуру. Потому что есть несколько способов получить DOM документ. Это собственный API, с помощью которого можно построить структуру DOM. Это XML документ, пропарсив который, вы можете получить DOM. А также, результат трансформации XSLT процессора это тоже DOM документ. Таким образом, DOM документ является сердцем всей технологии XML. Поэтому было решено ввести поддержку DOM.

Однако, при простоте кодирования возникает опасность получить не тот результат который ожидался (это похоже на регулярные выражения — мало кода, большая функциональность, но пока добьёшся результата!). Для работы с классом необходимо знать язык XPath и хорошо понимать разницу между DOM документом и SQL деревом. Именно эти вопросы призвана осветить эта статья.

Добавление данных к SQL дереву

Это одна из самых простых операций. А если учесть что их всего четыре (добавление, изменение, выборка и удаление) то это целые 25% ;~).

Для начала нужно выполнить некоторые подготовительные операции.

<?php
// myDOM входит в состав пакета myXML
require_once('myDOM/myDOM.php');
// собственно сам myXTree
require_once('myXTree.php');
// и абстрактный доступ к базам данных из PEAR репозитория
require_once('DB.php');

$dom = new Document;
// далее документ $dom необходимо наполнить содержимым одним из упомянутых
// способом, например через DOM API
$dom->appendChild($dom->createElement('root'));
// и т.д.

// создаём подключение к базе данных
$db = DB::connect("mysql://$user:$pass@$host/$name");
// переменные $user, $pass и др. конечно же, подставить ваши

// и наконец создаём обьект класса myXTree
$xtree =& myXTree::create($db, $prefix);
PEAR::isError($xtree) and
trigger_error($xtree->getMessage(), E_USER_ERROR);
// параметр $prefix можно не указывать, это префикс имён таблиц базы данных
// результат, возвращаемый методом myXTree::create лучше проверить, вдруг база
// данных не является базой myXTree или не той версии
?>

Теперь всё готово для следующих операций.

Итак, добавление. Метод insert() принимает три параметра. Первый параметр $node — это объекты типа ELEMENT_NODE, TEXT_NODE, CDATA_SECTION_NODE и ATTRIBUTE_NODE. Другие типы из стандарта DOM в базе данных не сохраняются. Почему? Потому что пока не было необходимости. А во вторых — не всё получается сохранить (подробности позже).

Вторым параметром указывается объект, к которому необходимо добавить объект $node указанный в первом параметре. Так как только у ELEMENT_NODE могут быть дети, то соответственно, в параметре $parent можно передавать только эти объекты. Кроме того $parent уже должен существовать в базе данных, иначе будет выдано сообщение об ошибке. Возникает вопрос, где взять объект класса Element который находился бы в базе данных. Правильнее будет задать вопрос как создать объект класса Element и установить соответствие между этим объектом и записью в базе данных. Такая постановка вопроса — это суть работы класса myXTree. Ему постоянно приходится устанавливать соответствие между объектами DOM документа и записями в базе данных. И ему требуется от вас некоторая помощь (но об этом тоже позже). Если вы будете помнить об этом утверждении, то вам значительно легче будет работать с классом.

Так вот, myXTree сам создаст объект класса Element и установит соответствие между этим объектом и записью в базе данных. Он сделает это для вас, если вы его попросите, вызвав метод select(). Проще говоря, по XPath выражению вы выбираете необходимый вам узел (node) и получаете готовый объект, который и подставляете в параметре $parent.

Но тут возникает второй законный вопрос, где взять объект $parent если база данных ещё пуста. Ответ прост. Она не пуста. Там уже есть корень (root). Его всегда можно выбрать по XPath выражению «/», но делать этого не нужно. По умолчанию, если $parent не задан, $node добавляется в корень SQL дерева. И таких можно добавить достаточно много.

И, наконец, третий параметр — $deep (глубоко), указывает на то что необходимо добавить также всех потомков объекта $node (т.е. можно добавить только объект $node с атрибутами, не смотря на то что у него есть потомки).

Давайте, наконец, посмотрим, что у нас делается в примере.

<?php
// Ну вот. Как я и говорил, можно добавлять прямо в корень.
// В качестве первого параметра $node передаём объект $dom->documentElement, а
// не $dom, так как $dom имеет тип DOCUMENT_NODE.
$result = $xtree->insert(&$dom->documentElement, $parent = null, $deep = true);
PEAR::isError($result)) and
trigger_error($result->getMessage(), E_USER_ERROR);
// Обязательно проверим успех нашего действия.
?>

Конечно же, вместо $dom->documentElement можно было бы вставить любую часть документа DOM. Просто у нас в примере её нет.

Удаление данных

По логике вещей сейчас нужно было бы рассказать о выборке или обновлении, но удаление, это следующая, ещё более простая операция.

Метод delete() принимает только один параметр $node который необходимо удалить. Причём, если у $node в базе есть потомки, они тоже удаляются. Конечно же, объект должен существовать в базе, а получить его можно также, с помощью метода select().

<?php
// Удалим, например тот же $dom->documentElement. Причём, так как мы его только
// что вставляли, соответствие между ним и записью в базе данных уже установлено.
// Это я к тому, что не нужно выбирать его из базы методом select();
$result = $xtree->delete(&$dom->documentElement);
PEAR::isError($result) and
trigger_error($result->getMessage(), E_USER_ERROR);

Выборка данных

Здесь нам понадобится хорошее понимание языка XPath.

Для выборки данных SQL дерева по XPath выражению используется метод select(). Первый параметр, который принимает этот метод, это строка — выражение XPath. Второй параметр указывает на метод, которым необходимо обработать XPath выражение. Назначение этого параметра станет понятным позднее. Так же как и метод evaluate() для DOM документа, select() возвращает массив ссылок (references) на объекты Node. Но, кроме этого, метод select() воссоздаёт дерево объектов Node. Пример поможет лучше понять разницу.

Для начала необходимо уточнить разницу между терминами XML документ и DOM документ. XML документ — это определённым образом отформатированный текст. Таким образом, ниже приведенный пример — это XML документ. DOM документ — это древовидная структура объектов в памяти компьютера. Т.е. DOM документ используется для представления XML документа в памяти компьютера. Каждый объект, в структуре DOM документа имеет ссылки на соседние объекты. А именно previousSabling (предшествующий брат), nextSibling (последующий брат), firstChild (первый ребёнок), lastChild (последний ребёнок) и некоторые другие.

<root>
  <node-1-1>
    <node-1-2>
      Simple Text
    </node-1-2>
    <node-2-2>
      <node-1-3>
        Very Simple Text
      </node-1-3>
    </node-2-2>
  </node-1-1>
</root>

В этом примере предшествующим братом для узла node-2-2 будет узел node-1-2, а node-2-2 соответственно будет последующим братом для узла node-1-2. Первым ребёнком для узла node-1-1 будет узел node-1-2, а последним node-2-2. А текстовый узел «Simple Text» будет первым и последним ребёнком для узла node-1-2. Именно таким способом определяется дерево DOM документа.

Что же сделает XPath процессор, если мы попросим его найти узлы, у которых есть ребёнок типа TEXT_NODE («//node()[text()]«). Он пробежится по дереву и вернёт нам узлы node-1-2 и node-1-3 в виде массива ссылок. Обратите внимание, не копии объектов, а ссылки на них. Не древовидную структуру, а список (одномерный массив). Имея ссылку на объект, который находится в дереве, мы легко можем получить доступ к другим объектам через previousSibling, firstChild и т.д.

Что же сделает XPath процессор класса myXTree? Он откомпилирует XPath выражение в SQL запрос, выполнит его и получит массив с записями базы данных. По этим записям он создаст объекты определённого типа (ELEMENT_NODE, TEXT_NODE и т.д.) и тоже вернёт в виде списка ссылок на объекты. Но, кому они нужны в таком виде? Для того чтобы они стали полезными необходимо восстановить отношения предок-потомок, т.е. воссоздать дерево объектов. Метод select() делает и это. Но, именно здесь и заключается большая разница в использовании XPath выражений для DOM документа и для SQL дерева.

Дело в том, что метод select() воссоздаёт дерево из того, что было возвращено запросом. Например, выражение «//node()[text()]» вернёт только узлы node-1-2 и node-1-3. Так как они не связаны непосредственно друг с другом, то дерева из них не получиться. Кроме того, текст «Simple Text» и «Very Simple Text» это отдельные текстовые узлы, которые тоже не возвращаются выражением «//node()[text()]«. Поэтому практика использования XPath выражений для SQL дерева отличается от DOM дерева. Например, для того чтобы текстовые узлы попали в результат выражения, нужно дописать его — «//node()[text()]//». Таким образом, метод select() вернёт список ссылок уже на четыре объекта и воссоздаст два фрагмента дерева.

Осталось только сказать, что для того чтобы select() мог создать объекты Node ему необходимо указать DOM документ. А для того чтобы воссоздать дерево объектов, необходимо также указать объект (Reception Node) в который будет помещено это дерево.

А теперь пример.

<?php
// Создаём узел, к которому будет добавлено воссозданное дерево.
$resultNode =& $dom->createElement('resultNode');
// Даём знать об этом объекту класса myXTree.
$xtree->setReceptionNode(&$resultNode);
$nodeSet = $xtree->select("//node-1-1//");
PEAR::isError($nodeSet) and
trigger_error($nodeSet->getMessage(), E_USER_ERROR);

В результате выполнения метода select() узел $resultNode будет содержать узел node-1-1 вместе со всеми потомками, а $nodeSet будет содержать список ссылок на узел node-1-1 и всех его потомков. В зависимости от конкретных потребностей вы можете использовать тот или другой способ получения результата.

Обновление данных

А здесь предстоит разобраться с проблемой сравнения двух XML документов.

Почему именно XML документов, а не DOM документов? Потому что обмен данными происходит именно в XML формате. Нам необходимо обеспечить возможность получить данные SQL дерева, отправить для изменения пользователю, а потом полученные изменения внести обратно в SQL дерево. Отправление данных пользователю и получение их обратно будет происходить в XML формате (XForms на пороге).

Давайте на некоторое время забудем про SQL дерево и представим, что мы имеем дело с бо-о-ольшим XML документом.

Это будет наш бо-о-ольшой             А это та часть которая была
      документ                         изменена пользователем
<root>
  <node-1-1 id="1">                  <node-1-1 id="1">
    <node-1-2>                         <node-1-2>
      Simple Text                        Unsimple Text
    </node-1-2>                        </node-1-2>
    <node-2-2>                       </node-1-1>
      <node-1-3>
        Very Simple Text
      </node-1-3>
    </node-2-2>
  </node-1-1>
</root>

Для того чтобы внести изменения нам необходимо установить соответствие между этими двумя XML документами. Так как XML документ это дерево, элементы определяются своим положением в дереве. Этот способ нам не подходит. Имя элемента тоже не может служить идентификатором. Можно для этой цели использовать атрибут. Но текстовые узлы и узлы CDATA не могут иметь атрибутов. Так как выбирать больше не из чего, было решено использовать атрибут с именем «id» уникальный в пределах всего дерева. А для идентификации текстовых и CDATA узлов использовать атрибут «id» их родителя. Следствием такого решения стало то, что элементы должны иметь только одного ребёнка типа TEXT_NODE или CDATA_SECTION_NODE. Кроме того, если узел типа ELEMENT_NODE не имеет атрибута «id», также будет выполнена попытка идентифицировать его по атрибуту «id» родителя и по имени самого элемента. Есть ещё способ идентификации элемента по атрибуту «id» и по имени элемента. Т.е. атрибут «id» в таком случае должен быть уникальным только в пределах указанного имени. Данный способ не реализован в myXTree.

Итак, мы определились ЧТО обновлять. Теперь нужно разобраться, КАК обновлять. Самое простое, что приходит в голову это заменить одно на другое. Т.е. (см. пример выше) заменить узел node-1-1 в большом XML документе на соответствующий. Назовём этот способ полным обновлением (при таком способе обновления нам нет нужды беспокоится о соответствии дочерних элементов, достаточно чтобы только узел node-1-1 имел атрибут «id»). Однако в большом XML документе у узла node-1-1 есть ещё один дочерний элемент node-2-2, при таком способе обновления он будет удалён из дерева. В тех случаях, когда пользователю позволено менять структуру дерева, нам именно это и нужно. Однако возникают ситуации, когда нужно обновить только часть потомков узла node-1-1. В таком случае нам нужно использовать выборочное обновление. При таком способе обновляться будут только те узлы, которые были реально изменены. Если появились новые элементы, они будут добавлены. Такой алгоритм обновления ещё не окончательный вариант. В основном он сложился в ходе решения конкретной задачи. И тут будут интересны ваши мнения.

Само использование метода update() не представляет никаких сложностей. Первый параметр — это узел, который нужно обновить, второй параметр указывает на то что обновить нужно не только указанный элемент но и потомков, третий параметр определяет способ обновления: true — полное, false — выборочное.

<?php
$result = $xtree->update(&$dom->documentElement, $deep = true, $full = false);
PEAR::isError($result) and
trigger_error($result->getMessage(), E_USER_ERROR);

Если алгоритм обновления вас не устраивает, всегда остаётся возможность «ручного» поочерёдного обновления. В таком случае параметр $deep должен быть false.

Немного о работе класса

Выше упоминался параметр $recursive для метода select(). Пришло время рассказать об этом поподробнее.

Чем хороши SQL деревья с моделью вложенных множеств? Тем, что за один SQL запрос можно получить узел со всеми его потомками независимо от глубины вложения. Чем плохи SQL деревья? Тем, что составить такой SQL запрос, мягко говоря, непросто. В тоже время существует хороший язык запросов к древовидным структурам — XPath. Так было решено попробовать использовать язык XPath для SQL деревьев. Получилось. Но какой ценой?

При построении SQL запроса по XPath выражению используется самообъединение таблиц. Например, выражение «/root» создаст SQL запрос вида:

SELECT * FROM Objects, Objects AS Table1, Objects AS Table2 WHERE ...

Самообъединение означает, что таблица будет помножена на саму себя. Т.е. таблица из 100 строк породИт 10000 строк! Страшная цифра! Такой результат ставил под сомнение целесообразность всего проекта. Поэтому был реализован ещё и другой способ обработки XPath выражения — рекурсивный. Название второго способа говорит само за себя.

После того как класс заработал, было проведено соревнование между первым (selfjoin) и вторым (recursive) способом, в результате которого самообъединение было реабилитировано! Трудно сказать каким способом, но mySQL с успехом справляется с этой огромной задачей. Тут интересно было бы мнение специалистов по mySQL.

Вместе с классом myXTree распространяются также тестовые файлы, на которых и сравнивалась работа класса двумя способами. В тесте сперва создаётся DOM документ из 1000 элементов. Потом этот документ сохраняется в SQL дереве, и, наконец, методом select() производится выборка элементов двумя способами. В качестве запросов используются выражения вида «/*», «/*/*», «/*/*/*» и т.д. до десяти уровней вложенности. Реально DOM документ имеет 5 уровней вложенности. Причём в одном случае у каждого элемента есть только по одному дочернему элементу, а в другом по 5. Т.е. количество выбираемых элементов растёт с каждым уровнем. И в том и в другом варианте способ самообъединения таблиц превосходил рекурсивный способ.

В погоне за скоростью

Наверно многие захотят поинтересоваться, как быстро это всё работает?

Вопрос слишком неоднозначный чтобы на него можно было ответить однозначно. Какие критерии оценки использовать? Узнать конкретное время выполнения вы можете на своём компьютере, запустив тесты прилагаемые к myXTree. Можно было бы сравнить скорость работы myXTree с другим классом с подобной функциональностью, но на сегодняшний день мне такие не известны. Есть интерфейсы к SQL деревьям, но без поддержки DOM и XPath. А это в свою очередь сказывается на скорости (чем больше функциональность, тем меньше скорость). Есть и ещё один критерий оценки — скорость разработки. Причём время, затраченное на разработку, стоит значительно дороже, чем время выполнения скрипта. Сколько вы потратите времени, чтобы сделать скрипт работающий на полсекунды быстрее. Стоит ли это тех усилий. myXTree поможет сэкономить время разработки. Стоит только один раз разобраться с ним. Как говориться «лучше полдня учиться летать, а потом за пять минут долететь», ведь навык летать останется.

Автор: Терещенко Андрей

Источник: http://www.php.su/articles/?cat=pear&page=005

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты