ВыходВход

Меню сайта

Категории статей
Теория [1]
Древовидные структуры данных. Методология, описание
Использование в SQL DB [0]
Решения используемые в SQL DB
Использование в Perl [7]
Применение технологии Nested Sets в Perl программировании
Использование в PHP [2]
Применение технологии Nested Sets в PHP программировании

Меню пользователя

Поиск по статьям

Друзья сайта

Класс PHP или гонки слепых котят - 1. Выборка данных
» Каталог статей » Использование в PHP
Класс PHP или гонки слепых котят - 1. Выборка данных

Класс PHP или гонки слепых котят - 1. Выборка данных

Введение

Так случилось, что принцип 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>&gt;&gt; ';
    }
}
...
?>

Выводит в строку родительскую ветку текущего узла.

В общем, с родительской веткой все, правда, в модуле 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;}
    }
}
...
?>
Категория: Использование в PHP | Добавил: phoinix (2005-11-22) | Автор: Сергей Томулевич (aka Phoinix)
Просмотров: 5992 | Рейтинг: 3.2 |

Комментарии

 

Бесплатный конструктор сайтов - uCoz