Введение
Так случилось, что принцип Nested Sets при построении деревьев используют не только Perl программисты. Появилась задача - написать класс на PHP для работы с деревьями Nested Sets, причем в довольно короткие сроки. Сразу хочу предупредить, что на PHP я не пишу, а если и пишу, то правильность кода может быть не совсем "правильной", но по крайней мере рабочей. Если найдутся специалисты, которые найдут в моем коде ошибку или неправильность, я буду рад их выслушать и исправиться.
Итак, как и при написании модуля к Perl, сначала я, конечно, посмотрел уже готовые решения по этому поводу. Решение нашел только одно - класс CDBTree. Как ни странно, его (класс) все друг другу предлагали, но обсуждений конкретных решений так и не увидел. Посмотрев код, я решил, что он мне не поможет... не буду обсуждать этот класс, просто скажу, что реализация мне не понравилась.... Есть еще пакет PEAR::DB_NestedSet, но он работает только в Pear, поэтому использовать его отдельно - не представляется возможным, поэтому я его и не рассматриваю даже.
Основные функции класса, я все-же подразумеваю выборку данных, несмотря на то, что все же рекомендуют делать запросы для выборки в ручную, не каждому начинающему программисту получается сразу объяснить принцип хранения деревьев Nested Sets, и в дальнейшем возникает целая куча проблем по отладке запросов. Функции управления деревьями, естественно, тоже рассмотрим. Итак...
Концепция
Основные функции которые будем использовать:
- Объявление класса - само собой;
- Выбор родительской ветки узла;
- Выбор подчиненной ветки узла;
- Создание узла;
- Перемещение узла;
- Удаление узла;
- Проверка целостности.
В качестве "обертки" к базе данных, я лично, ничего не использую, в PHP своя, замечательная обертка, а выдумывать свою - IMHO пустая трата времени, так, только самолюбие потешить. Так же как и в модуле Perl, класс будет собираться с возможностью работы с мультидеревьями, то есть, когда в одной таблице хранится несколько деревьев.
1. Объявление класса
Код:
class NestedSets { // Основные переменные класса var $table; // Имя таблицы var $id = 'id'; // Поле ID var $left = 'left_key'; // Поле левого ключа var $right = 'right_key'; // Поле правого ключа var $level = 'level'; // Поле уровня var $parent = 'parent'; // Поле ID родителя var $multi = 'class'; // Поле ID мультикаталога var $type = 'N'; // Флаг определения мультикаталог или нет (N|M) var $order = 'B'; // Флаг определения сортировки (T|B) var $unit = array( 'id' => 0, // ID текущего узла 'left' => 0, // Левый ключ текущего узла 'right' => 0, // Правый ключ текущего узла 'level' => 0, // Уровень текущего узла 'multi' => 1, // ID каталога текущего узла ); // Метод объявления класса function NestedSets ($table, $Parametres = array()) { // Здесь цепляем к классу объект коннект к базе и имя таблицы $this->table = $table; // Передаем название полей таблицы if(is_array($Parametres) && sizeof($Parametres)) { foreach($Parametres as $k => $v) { if ($k == 'unit') {$this->SelectUnit($v);} $this->$k = $v; } } } ...
Собственно, ничего сложного, единственно, что сразу появляется функция (конструктор, метод - кому как нравится) - SelectUnit, в котором выбираются, параметры узла относительно его идентификатора. Опишем и его.
// Выбор текущего узла function SelectUnit ($ID, $class = 1) { // Проверяем пришедший ID if (empty($ID) || (!is_numeric($ID) || $ID != 'root')) { trigger_error('ERR! Не указан узел!');} // Если текущий узел - корень дерева if ($ID == 'root') { $sql = 'SELECT MAX('.$this->right.') + 1 AS rk'. ($this->type == 'M' ? ', '.$this->multi.' AS cl ' : ''). ' FROM '.$this->table. ($this->type == 'M' ? ' WHERE '.$this->multi.'='.$class : ''); } else { $sql = 'SELECT '.$this->left.' AS lk, '. $this->right.' AS rk, '. $this->level.' AS lv '. ($this->type == 'M' ? ', '.$this->multi.' AS cl ' : ''). ' FROM '.$this->table. ' WHERE '.$this->id.' = '.$ID; } if (($query = mysql_query($sql)) && (mysql_num_rows($query) == 1) && ($Data = mysql_fetch_array($query))) {
$this->unit['id'] = $ID; $this->unit['left'] = $Data['lk']; $this->unit['right'] = $Data['rk']; $this->unit['level'] = $Data['lv']; if (isset($Data['cl'])) {$this->unit['multi'] = $Data['cl'];} return $Data; } else {trigger_error('ERR! Неправильный ID узла!');} }
Собственно, в отличии от соответствующего метода модуля Perl, я включил здесь выборку корневого узла. Этот узел, сам по себе, не существует, и ключами этого узла является общий диапазон ключей дерева. Это нам пригодится в дальнейшем, для выборки корневых узлов дерева. Хочу сразу заметить, что, по сути, выбирается только максимальный правый ключ + 1, левый ключ и уровень приравнены к нулю, что несколько ограничивает их использование, так как ноль - это пустое значение, и в различных ситуациях обращение к этим параметрам может вызвать ошибку скрипта.
2. Выбор родительской ветки узла
Несмотря на то, что структура запроса выборки родителя одна, для него существует несколько корректирующих параметров, а именно:
- выбирается вся родительская ветка или только непосредственный родитель;
- порядок сортировки родительских узлов - от корневого или от непосредственного родителя;
- выбирается ли текущий узел или только его родители.
Так же, в связи с тем, что структура таблицы базы данных у нас страндартна только на уровне ключей, а остальные поля могут быть любыми, то одним из параметров выборки является список дополнительных полей. Точнее, этот параметр один из самых основных, потому как выбирать ключи родительских узлов практически всегда - незачем. Итак, код:
// Выборка родительской ветки с дополнительными полями в массиве массивов function GetParentArray ($unit, $fields = array(), $param = array()) { // Проверка параметров if (empty($param['branch']) || $param['branch'] != 'all') {$param['branch'] = 'one';} if (empty($param['order']) || $param['order'] != 'DESC') {$param['order'] = 'ASC';} if (empty($param['ISU']) || $param['ISU'] != 'N') {$param['ISU'] = 'Y';} if (empty($param['return']) || $param['return'] != 'sql') {$param['return'] = 'res';} // Выборка элемента if (isset($unit)) {$this->SelectUnit($unit);} else if (empty($this->unit['id'])) {trigger_error('ERR! Не выбран исходный узел!');} // Если элемент к корне дерева то возвращаем 'No' if ($this->unit['level'] == 1 && $param['ISU'] == 'N') {return 'No';} // Сам запрос выборки родительской ветки $sql = 'SELECT '.$this->id.' AS id'.(isset($fields[0]) ? ', '.implode(', ', $fields) : ''). ' FROM '.$this->table. ' WHERE '. $this->left.($param['ISU'] == 'N' ? '<' : '<=' ).$this->unit['left'].' AND '. $this->right.($param['ISU'] == 'N' ? '>' : '>=' ).$this->unit['right']. ($this->type == 'M' ? ' AND '.$this->multi.' = '.$this->unit['multi'] : ''). ' ORDER BY '.$this->level.' '.($param['branch'] == 'one' ? 'DESC' : $param['order']).' '. ($param['branch'] == 'one' ? ' LIMIT 1' : ''); // Выполняем запрос и возвращаем результат $mysql_query = mysql_query($sql); if ($param['return'] == 'sql') {return $mysql_query;} while ($row = mysql_fetch_assoc($mysql_query)) { foreach($row as $k => $v) {$mass[$k] = $v;} $ids[] = $mass; } return $ids; }
Теперь буду пояснять. В функцию мы передаем: ID узла; список полей в виде массива, которые мы должны так же вернуть; и хеш-массив дополнительных парамертов. Их всего четыре и проверяются они сразу же:
- branch - параметр указывающий - всю ли ветку мы возвращаем (all) или же только непосредственного родителя (one);
- order - параметр указывающий порядок сортировки родительской ветки, как и в запросе ASC или DESC;
- ISU (Include Selected Unit) - просто сокращенно, включать ли текущий узел в результат или нет;
- return - что возвращать - массив хешей (res) или же подготовленный SQL запрос (sql) для его дальнейшей обработки. Просто во время формирования массива хешей проходит двойной цикл, и в коде основного скрипта, наверняка будет проводиться еще один перебор, что, по моему мнению, не оптимально.
Далее запрос, по привычке Perl, я его формирование собрал в одну строку, и понять , что это запрос можно только по слову SELECT. попробую перевести запрос на русский:
- Выбрать поля (SELECT):
- идентификатор, имя которого берется из класса;
- список полей, переданных в функцию, но для начала, мы определяем есть хотя бы первый элемент массива, что бы имел смысл формировать строку списка полей;
- Из таблицы (FROM) - имя таблицы из класса
- Где (WHERE):
- левые ключи в зависимости от того выбирается текущий узел или нет меньше или равны, или просто меньше левого ключа текущего узла;
- правые ключи в зависимости от того выбирается текущий узел или нет больше или равны, или просто больше правого ключа текущего узла;
- и идентификатор дерева которого равен идентификатору дерева текущего узла если используются мультидеревья;
- Сортировать (ORDER BY) по левому ключу в порядке, который указан в параметре, если выбираем всю ветку;
- В зависимости от того выбирается ли непосредственный родитель или вся ветвь устанавливаем ограничение в один элемент (LIMIT 1) или нет.
В дальнейшем выполняем запрос, и в зависимости от того что нам требуется вернуть возвращаем ссылку на результат запроса или же формируем массив хешей и возвращаем его.
Пример использования (строка навигации):
<? ... require_once "inc/lib/nestedsets.class"; $NestedSets = new NestedSets ('tree_table');
$unit = $_GET['unit']; if (!is_numeric($unit)) {$unit = 'root';}
if ($unit != 'root') { $parents_sql = $NestedSets->GetParentArray($unit, array('name'), array('branch' => 'all', 'return' => 'sql')); while ($row = mysql_fetch_assoc($parents_sql)) { if ($row['id'] == 'root') {break;} echo ' <A href="?unit='.$row['id'].'>'.$row['name'].'</A>>> '; } } ... ?>
Выводит в строку родительскую ветку текущего узла.
В общем, с родительской веткой все, правда, в модуле Perl я описывал еще процедуру которая возвращала только идентификаторы родительской ветки в виде массива, но так никогда этой процедурой и не воспользовался... тем более что вышеуказанная процедура может возвращать только идентификаторы тоже.
3. Выбор подчиненных узлов
Для выборки подчиненных узлов все же пригодятся две процедуры: одна для выборки непосредственно подчиненных узлов (один уровень вниз), вторая - выборка идентификаторов подчиненных узлов всей подчиненной ветки.
Эти фукции мало отличаются от предыдущей, так как запрос практически тот же, только сравнение ключей обратно, а так же список параметров выборки различен.
// Выборка подчиненных узлов с дополнительными полями в массиве массивов // (только 1 уровень вниз) function GetChildArray ($unit, $fields = array(), $param = array()) { // Проверка параметров if (empty($param['class'])) {$param['class'] = 1;} if (empty($param['return']) || $param['return'] != 'sql') {$param['return'] = 'res';} // Выборка элемента if (isset($unit)) {$this->SelectUnit($unit, $param['class']);} else if (empty($this->unit['id'])) {trigger_error('ERR! Не выбран исходный узел!');} // Проверяем есть ли вообще подчиненные узлы if ($this->unit['right'] - $this->unit['left'] == 1) {return 'No';} // Потом бы не забыть в скриптах проверять возврат // Сам запрос выборки родительской ветки $sql = 'SELECT '.$this->id.' AS id'.(isset($fields[0]) ? ', '.implode(', ', $fields) : ''). ' FROM '.$this->table. ' WHERE '.$this->left.' > '.(isset($this->unit['left']) ? $this->unit['left'] : '0').' AND '. $this->right.' < '.$this->unit['right'].' AND '. $this->level.' = '.(isset($this->unit['level']) ? $this->unit['level'] : '0').' + 1 '. ($this->type == 'M' ? ' AND '.$this->multi.' = '.$this->unit['multi'] : ''). ' ORDER BY '.$this->left; // Выполняем запрос и возвращаем результат $mysql_query = mysql_query($sql); if ($param['return'] == 'sql') {return $mysql_query;} while ($row = mysql_fetch_assoc($mysql_query)) { foreach($row as $k => $v) {$mass[$k] = $v;} $ids[] = $mass; } return $ids; }
// Выборка ID подчиненных узлов function GetChildID ($unit, $param = array()) { // Проверка параметров if (empty($param['class'])) {$param['class'] = 1;} if (empty($param['return']) || $param['return'] != 'sql') {$param['return'] = 'res';} if (empty($param['branch']) || $param['branch'] != 'one') {$param['branch'] = 'all';} if (empty($param['ISU']) || $param['ISU'] != 'N') {$param['ISU'] = 'Y';} // Выборка элемента if (isset($unit)) {$this->SelectUnit($unit, $param['class']);} else if (empty($this->unit['id'])) {trigger_error('ERR! Не выбран исходный узел!');} // Проверяем есть ли вообще подчиненные узлы if ($this->unit['right'] - $this->unit['left'] == 1 && $param['ISU'] == 'N') {return 'No';} // Потом бы не забыть в скриптах проверять возврат // Сам запрос выборки родительской ветки $sql = 'SELECT '.$this->id. ' FROM '.$this->table. ' WHERE '. $this->left.($param['ISU'] == 'Y' ? '>=' : '>' ).(isset($this->unit['left']) ? $this->unit['left'] : '0').' AND '. $this->right.($param['ISU'] == 'Y' ? '<=' : '<' ).$this->unit['right']. ($param['branch'] == 'all' ? '' : ' AND '.$this->level.' = '.(isset($this->unit['level']) ? $this->unit['level'] : '0').' + 1 '). ($this->type == 'M' ? ' AND '.$this->multi.' = '.$this->unit['multi'] : ''). (isset($param['where']) ? ' AND '.$param['where'] : ''). ' ORDER BY '.$this->left. (isset($param['limit']) ? ' LIMIT '.$param['limit'] : ''); // Выполняем запрос и возвращаем результат $mysql_query = mysql_query($sql); if ($param['return'] == 'sql') {return $mysql_query;} while ($row = mysql_fetch_array($mysql_query)) { $ids[] = $row[0]; } return $ids; }
Итак, в первой функции не учтены такие параметры как сортировка, выборка свей ветви и выборка текущего узла. Но существует дополнительная проверка в запросе на наличие левого ключа и уровня, это связано с тем, что при выборки подчиненных узлов корня дерева, выбирается весь его (дерева) диапазон и левый ключ и уровень приравнивается к нулю (о чем было сказано ранее). Так же одним из дополнительных параметров является class - это идентификатор дерева при использовании мультидеревьев, его требуется указывать когда выбираем узлы корня дерева, так как при выборке мы указываем текущий узел - root, которого не существует.
Во второй функции же используются практически те же параметры что и при выборки родительской ветки, за исключением сортировки, она, как и в первой функции производится по левому ключу. Так же одними из дополнительных параметров является class и where. Параметр class играет ту же роль, что и в первой процедуре, а вот параметр where позволяет расширить условие запроса, например если в дереве предусмотрено включение (выключение) веток (узлов), то можно дополнительным условием задать исключение выключенных узлов из результата запроса.
Пример использования функции:
<? ... $child_sql = $NestedSets->GetChildArray($unit, array('name', 'firm', 'onof'), array('return' => 'sql')); $i = 1; if ($child_sql != 'No') { while ($row = mysql_fetch_assoc($child_sql)) { if ($row['onof'] == 'N') {continue;} echo '<li><a href=?unit='.$row['id'].'>'.$row['name'].'</a></li>'; if ($i == 1) {$i = 2;} else {echo '</tr><tr>'; $i = 1;} } } ... ?>
|