DOC.PROTOTYPES.RU

Главная > Базы данных > Деревья SQL > Nested Sets > практика PHP >

Практика NestedSets - класс PHP выборки ветвей

Введение

Так случилось, что принцип Nested Sets при построении деревьев используют не только Perl программисты. Появилась задача - написать класс на PHP для работы с деревьями Nested Sets, причем в довольно короткие сроки. Сразу хочу предупредить, что на PHP я не пишу, а если и пишу, то правильность кода может быть не совсем "правильной", но по крайней мере рабочей. Если найдутся специалисты, которые найдут в моем коде ошибку или неправильность, я буду рад их выслушать и исправиться.

Итак, как и при написании модуля к Perl, сначала я, конечно, посмотрел уже готовые решения по этому поводу. Решение нашел только одно - класс CDBTree. Как ни странно, его (класс) все друг другу предлагали, но обсуждений конкретных решений так и не увидел. Посмотрев код, я решил, что он мне не поможет... не буду обсуждать этот класс, просто скажу, что реализация мне не понравилась.... Есть еще пакет PEAR::DB_NestedSet, но он работает только в Pear, поэтому использовать его отдельно - не представляется возможным, поэтому я его и не рассматриваю даже.

Основные функции класса, я все-же подразумеваю выборку данных, несмотря на то, что все же рекомендуют делать запросы для выборки в ручную, не каждому начинающему программисту получается сразу объяснить принцип хранения деревьев Nested Sets, и в дальнейшем возникает целая куча проблем по отладке запросов. Функции управления деревьями, естественно, тоже рассмотрим. Итак...

Концепция

Основные функции которые будем использовать:

В качестве "обертки" к базе данных, я лично, ничего не использую, в PHP своя, замечательная обертка, а выдумывать свою - IMHO пустая трата времени, так, только самолюбие потешить. Так же как и в модуле Perl, класс будет собираться с возможностью работы с мультидеревьями, то есть, когда в одной таблице хранится несколько деревьев.

1. Объявление класса

PHP код (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, в котором выбираются, параметры узла относительно его идентификатора. Опишем и его.

PHP код (2)
// Выбор текущего узла
    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. Выбор родительской ветки узла

Несмотря на то, что структура запроса выборки родителя одна, для него существует несколько корректирующих параметров, а именно:

Так же, в связи с тем, что структура таблицы базы данных у нас страндартна только на уровне ключей, а остальные поля могут быть любыми, то одним из параметров выборки является список дополнительных полей. Точнее, этот параметр один из самых основных, потому как выбирать ключи родительских узлов практически всегда - незачем. Итак, код:

PHP код (3)
// Выборка родительской ветки с дополнительными полями в массиве массивов
    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 узла; список полей в виде массива, которые мы должны так же вернуть; и хеш-массив дополнительных парамертов. Их всего четыре и проверяются они сразу же:

Далее запрос, по привычке Perl, я его формирование собрал в одну строку, и понять , что это запрос можно только по слову SELECT. попробую перевести запрос на русский:

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

Пример использования (строка навигации):

PHP код (4)
<?
...
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. Выбор подчиненных узлов

Для выборки подчиненных узлов все же пригодятся две процедуры: одна для выборки непосредственно подчиненных узлов (один уровень вниз), вторая - выборка идентификаторов подчиненных узлов всей подчиненной ветки.

Эти фукции мало отличаются от предыдущей, так как запрос практически тот же, только сравнение ключей обратно, а так же список параметров выборки различен.

PHP код (5)
// Выборка подчиненных узлов с дополнительными полями в массиве массивов
// (только 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 позволяет расширить условие запроса, например если в дереве предусмотрено включение (выключение) веток (узлов), то можно дополнительным условием задать исключение выключенных узлов из результата запроса.

Пример использования функции:

PHP код (5)
<?
...
$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;}
    }
}
...
?>

Пока все.

Сергей Томулевич aka Phoinix (01.06.2005, ред. 02.09.2009 г.)

Еще статьи по теме:

Valid HTML 4.01 Transitional
Copyright © 2011 Сергей Томулевич