Так случилось, что принцип Nested Sets при построении деревьев используют не только Perl программисты. Появилась задача - написать класс на PHP для работы с деревьями Nested Sets, причем в довольно короткие сроки. Сразу хочу предупредить, что на PHP я не пишу, а если и пишу, то правильность кода может быть не совсем "правильной", но по крайней мере рабочей. Если найдутся специалисты, которые найдут в моем коде ошибку или неправильность, я буду рад их выслушать и исправиться.
Итак, как и при написании модуля к Perl, сначала я, конечно, посмотрел уже готовые решения по этому поводу. Решение нашел только одно - класс CDBTree. Как ни странно, его (класс) все друг другу предлагали, но обсуждений конкретных решений так и не увидел. Посмотрев код, я решил, что он мне не поможет... не буду обсуждать этот класс, просто скажу, что реализация мне не понравилась.... Есть еще пакет PEAR::DB_NestedSet, но он работает только в Pear, поэтому использовать его отдельно - не представляется возможным, поэтому я его и не рассматриваю даже.
Основные функции класса, я все-же подразумеваю выборку данных, несмотря на то, что все же рекомендуют делать запросы для выборки в ручную, не каждому начинающему программисту получается сразу объяснить принцип хранения деревьев Nested Sets, и в дальнейшем возникает целая куча проблем по отладке запросов. Функции управления деревьями, естественно, тоже рассмотрим. Итак...
Концепция
Основные функции которые будем использовать:
Объявление класса - само собой;
Выбор родительской ветки узла;
Выбор подчиненной ветки узла;
Создание узла;
Перемещение узла;
Удаление узла;
Проверка целостности.
В качестве "обертки" к базе данных, я лично, ничего не использую, в PHP своя, замечательная обертка, а выдумывать свою - IMHO пустая трата времени, так, только самолюбие потешить. Так же как и в модуле Perl, класс будет собираться с возможностью работы с мультидеревьями, то есть, когда в одной таблице хранится несколько деревьев.
1. Объявление класса
Собственно, ничего сложного, единственно, что сразу появляется функция (конструктор, метод - кому как нравится) - SelectUnit, в котором выбираются, параметры узла относительно его идентификатора. Опишем и его.
Собственно, в отличии от соответствующего метода модуля Perl, я включил здесь выборку корневого узла. Этот узел, сам по себе, не существует, и ключами этого узла является общий диапазон ключей дерева. Это нам пригодится в дальнейшем, для выборки корневых узлов дерева. Хочу сразу заметить, что, по сути, выбирается только максимальный правый ключ + 1, левый ключ и уровень приравнены к нулю, что несколько ограничивает их использование, так как ноль - это пустое значение, и в различных ситуациях обращение к этим параметрам может вызвать ошибку скрипта.
2. Выбор родительской ветки узла
Несмотря на то, что структура запроса выборки родителя одна, для него существует несколько корректирующих параметров, а именно:
выбирается вся родительская ветка или только непосредственный родитель;
порядок сортировки родительских узлов - от корневого или от непосредственного родителя;
выбирается ли текущий узел или только его родители.
Так же, в связи с тем, что структура таблицы базы данных у нас страндартна только на уровне ключей, а остальные поля могут быть любыми, то одним из параметров выборки является список дополнительных полей. Точнее, этот параметр один из самых основных, потому как выбирать ключи родительских узлов практически всегда - незачем. Итак, код:
Теперь буду пояснять. В функцию мы передаем: ID узла; список полей в виде массива, которые мы должны так же вернуть; и хеш-массив дополнительных парамертов. Их всего четыре и проверяются они сразу же:
branch - параметр указывающий - всю ли ветку мы возвращаем (all) или же только непосредственного родителя (one);
order - параметр указывающий порядок сортировки родительской ветки, как и в запросе ASC или DESC;
ISU (Include Selected Unit) - просто сокращенно, включать ли текущий узел в результат или нет;
return - что возвращать - массив хешей (res) или же подготовленный SQL запрос (sql) для его дальнейшей обработки. Просто во время формирования массива хешей проходит двойной цикл, и в коде основного скрипта, наверняка будет проводиться еще один перебор, что, по моему мнению, не оптимально.
Далее запрос, по привычке Perl, я его формирование собрал в одну строку, и понять , что это запрос можно только по слову SELECT. попробую перевести запрос на русский:
Выбрать поля (SELECT):
идентификатор, имя которого берется из класса;
список полей, переданных в функцию, но для начала, мы определяем есть хотя бы первый элемент массива, что бы имел смысл формировать строку списка полей;
Из таблицы (FROM) - имя таблицы из класса
Где (WHERE):
левые ключи в зависимости от того выбирается текущий узел или нет меньше или равны, или просто меньше левого ключа текущего узла;
правые ключи в зависимости от того выбирается текущий узел или нет больше или равны, или просто больше правого ключа текущего узла;
и идентификатор дерева которого равен идентификатору дерева текущего узла если используются мультидеревья;
Сортировать (ORDER BY) по левому ключу в порядке, который указан в параметре, если выбираем всю ветку;
В зависимости от того выбирается ли непосредственный родитель или вся ветвь устанавливаем ограничение в один элемент (LIMIT 1) или нет.
В дальнейшем выполняем запрос, и в зависимости от того что нам требуется вернуть возвращаем ссылку на результат запроса или же формируем массив хешей и возвращаем его.
Пример использования (строка навигации):
Выводит в строку родительскую ветку текущего узла.
В общем, с родительской веткой все, правда, в модуле Perl я описывал еще процедуру которая возвращала только идентификаторы родительской ветки в виде массива, но так никогда этой процедурой и не воспользовался... тем более что вышеуказанная процедура может возвращать только идентификаторы тоже.
3. Выбор подчиненных узлов
Для выборки подчиненных узлов все же пригодятся две процедуры: одна для выборки непосредственно подчиненных узлов (один уровень вниз), вторая - выборка идентификаторов подчиненных узлов всей подчиненной ветки.
Эти фукции мало отличаются от предыдущей, так как запрос практически тот же, только сравнение ключей обратно, а так же список параметров выборки различен.
Итак, в первой функции не учтены такие параметры как сортировка, выборка свей ветви и выборка текущего узла. Но существует дополнительная проверка в запросе на наличие левого ключа и уровня, это связано с тем, что при выборки подчиненных узлов корня дерева, выбирается весь его (дерева) диапазон и левый ключ и уровень приравнивается к нулю (о чем было сказано ранее). Так же одним из дополнительных параметров является class - это идентификатор дерева при использовании мультидеревьев, его требуется указывать когда выбираем узлы корня дерева, так как при выборке мы указываем текущий узел - root, которого не существует.
Во второй функции же используются практически те же параметры что и при выборки родительской ветки, за исключением сортировки, она, как и в первой функции производится по левому ключу. Так же одними из дополнительных параметров является class и where. Параметр class играет ту же роль, что и в первой процедуре, а вот параметр where позволяет расширить условие запроса, например если в дереве предусмотрено включение (выключение) веток (узлов), то можно дополнительным условием задать исключение выключенных узлов из результата запроса.