MPTT (Modified Preorder Tree Traversal) и PHP


MPTT (Modified Preorder Tree Traversal) и PHP
Каждый разработчик рано или поздно сталкивается с проблемой хранения и выборки данных в виде дерева. Самый распространенный вариант - наличие у потомков ссылки на родителя. У него есть свои плюсы - это простота изменения дерева (удаление, добавление, перенос веток). Однако есть и свои минусы - это ресурсоемкие и медленные алгоритмы выборки. Как правило это рекурсивные функции с множеством мелких выборок из БД, либо одна выборка с последующей циклической и рекурсивной обработкой и упорядочиванием данных.
Но давайте разберем еще один метод реализации древовидной структуры данных в БД.. Это MPTT. Этот метод очень похож на Nested sets (Вложенные множества), с той лишь разницей что тут не хранится уровень вложенности.. Используя этот метод мы можем выбрать ветку или часть дерева используя один SQL запрос. Так же просто можно выбрать путь к любому элементу дерева.

В MPTT для каждого элемента дерева мы определим 2 числа - левое и правое. Начнем их устанавливать с верхнего элемента. Путешевствуя по всему дереву мы начнем обход с левой стороны корневого элемента и закончим его правой стороной. Перемещаясь по краям дерева каждому элементу мы устанавливаем число, каждый раз увеличивающеемя на 1 - сначала с левой стороны, а потом с правой. Подробнее видно на картинке:


Назовем эти числа lft и rgt (left и right). То есть вы видите, что левое числа для Food - 1, а правое - 18. Эти числа показывают некую связь между элементами дерева. Так как 'Red' имеет числа 3 и 6 - значит он является потомком элемента 'Food' с числами 1 и 18 Точно так же мы можем сказать что любой элемент с левым числом больше 2х и меньше 11 является потомком элемента 2-11 'Fruit'. Таким образом древовидная структура зранится в левых и правих значениях.

Перед тем как продолжить, давайте посмотрим на наши значения в виде таблицы:

Таким образом нам больше не нужно поле parentId

Выборка дерева

Если вы хотите отобразить часть дерева используя таблицу с левыми и правыми значениями, вам нужно просто знать левое и правое числа того элемента, от которого начинается эта ветка.
Например для ветки, начинающейся с элемента 'Fruit' - это числа 2 и 11, то есть:
SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;
Результат:


Таким образом мы получили целое поддерево используя один запрос. Для того чтобы отобразить это дерево как будто мы использовали рекурсивную фенкцию, нам нужно только добавить условие ORDER BY. Если вы добавите или удалите строки из таблицы, то она возможно уже не будет в правильном порядке, поэтому мы сортируем строки по их левому значению:
SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;
Осталась одна проблема - Отступы (или определение уровня вложенности).

Чтобы вывести данные в древовидном виде потомки должны иметь больший отступ чем из родители. Мы это можем реализовать храня стек правых значений элементов.
Каждый раз когда вы доходите нового потомка ветки (переходите на больший уровень вложенности), во добавляете правое значение этого элемента в стек. Мы знаем, что все дети этой ветки имеют правое значение меньшее чем у родителя. Итак, сравнивая правые числа текущей ветки и последним правым числом в стеке мы знаем что отображаем детей этого родителя. А когда мы выведем всех детей родителя, мы просто удаляем его правое числа из стека. А сосчитав количество элементов в стеке мы получаем уровень вложенности:
<?php
function display_tree($root) {
    // Получаем левое и правое числа ветки, начинающейся с $root
    $result = mysql_query('
        SELECT lft, rgt FROM tree WHERE title="'.$root.'"
    ');
    $row = mysql_fetch_array($result);

    // Начинаем с пустого стека правых чисел
    $right = array();

    // Теперь  выбираем всех потомков ветки, начинающейся с $root
    $result = mysql_query('
        SELECT title, lft, rgt 
        FROM tree 
        WHERE lft BETWEEN '.$row['lft'].' AND '.$row['rgt'].' 
        ORDER BY lft ASC
    ');

    // Отображаем каждую строку
    while ($row = mysql_fetch_array($result)) {
        // Проверим стек, есть ли он
        if (count($right)>0) {
            // Проверим, надо ли нам уже удалить ветку из стека
            while ($right[count($right)-1] < $row['rgt']) {
                array_pop($right);
            }
        }

        // Отобразим ветки с нужным отступом
        echo str_repeat(' ', count($right))
        echo $row['title']."\n";

        // Добавим элемент в стек
        $right[] = $row['rgt'];
    }
}
?>
Если вы запустите этот код, вы получите точно такое же дерево, как если бы воспользовались рекурсивной функцией. Но этот метод быстрее: Но не рекурсивный и использует всего лишь 2 запроса к БД (а если передавать сразу 2 счила элемента, с которого начинается ветка - то и вообще один запрос)

Путь к любому элементу дерева

С помощью этого нового алгоритма мы так же получили новую возможность легко получить путь к любому элементу дерева. Для этого нам нужно выбрать всех родителей элемента для корня дерева.

С нашей новой структурой таблицы это не сложно.. Если мы взглянем например на элемент 4-5 'Cherry', мы можем заметить что левые значения всех родителей меньше 4, в то время как все правые больше 5. То етсь чтобы получить всех предков надо выполнить запрос:
SELECT title FROM tree WHERE lft < 4 AND rgt > 5 ORDER BY lft ASC;
Заметим, что как и в предыдущем запросе, нам надо использовать ORDER BY для сортировки результата. Вот что мы получим:
+-------+
| title |
+-------+
| Food  |
| Fruit |
| Red   |
+-------+
Нам осталось всего лишь объединить результаты выборки

Сколько потомков?

Если вы скажете левое и правое число любого узла, я с легкостью могу сказать сколько у него потомков, используя немного математики.

Так как каждый потомок увеличивает правое значение узла на 2, то количество потомков можно вычислить при помощи формулы:
Кол-во детей = (rgt – lft - 1) / 2
С помощью этой простой формулы я могу сказать вам, что 2-11 'Fruit' узел имеет 4 потомка, а 8-9 'Banana' узел ни одного, и является только потомком, не родителем.

Автоматический обход дерева

Вы видели сколько полезных вещей можно сделать с этой таблицей, а теперь давайте посмотрим как же можно автоматизировать создание такой таблицы.
Напишем скрипт который бы за нас прошелся по дереву узлов и проставил левое и правое значение для всех узлов дерева
<?php
function rebuild_tree($parent, $left) {
    // Правое значение узла - это на 1 увеличенное левое знначение
    $right = $left+1;
    
    // Берем всех детей узла
    $result = mysql_query('
        SELECT title FROM tree WHERE parent="'.$parent.'"
    ');
    while ($row = mysql_fetch_array($result)) {
        // Рекурсивно вызываем эту функцию для каждого потомка
        // $right - это текущее rgt, увеличенное ф-ей rebuild_tree
        $right = rebuild_tree($row['title'], $right);
    }

    mysql_query('
        UPDATE tree SET lft='.$left.', rgt='.$right.' 
        WHERE title="'.$parent.'"
    ');

    // Вернем правое значение узла +1
    return $right+1;
}
?>
Это рекурсивная функция. Ее следует запускать для первого узла, после чего она сама доберется до всех потомков, то есть
rebuild_tree('Food',1);

Если нет дочерних узлов, она останавливает левые и правые значения. Левое значение дается (1), а правое - это на 1 больше левого. Если есть дочерние узлы, то функция вызывается рекурсивно и возвращает последнее правое значение. Это значение и устанавливается для 'Food'.

Рекурсия усложняет для понимания эту функцию. Однако она сохранит те значения, которые мы установили вручную в самом начале для нашего дерева. Она пройдет вокруг всего дерева, устанавливая значения lft и rgt. Быстрая проверка правильности установки чисел - правое значение коренного узла дерева равно удвоенному числу узлов дерева.

Добавление узла

Как добавить узел в дерево? Есть 2 решения. Первый - хранить ссылку на родительский узел для каждого элемента и после добавления нового просто запускать rebuild_tree() - простой но неэлегантный метод.
И второй - обновить правые и левые числа всех узлов по правую часть от добавляемого узла.

Первое решение - простое. Это некий симбиоз старого метода со ссылками на родительский узел и нового метода, когда для быстрой выборки мы используем повый метод с правыми и левыми значениями, а для обновления и изменения дерева используем старый метод со ссылками на родителя с последующей переустановкой левых и правых чисел каждого узла запуском функции rebuild_tree(). Это решение простое, но трудоемкое и неэффективное для больших деревьев.

Второй метод добавлять и удалять узлы это обновлять левые и правые значения всех узлов, которые идут следом за добавляемым (справа от него) по ходу движения по дереву.
Давайте назберем на примере. Нам надо добавить новый тип фруктов, 'Strawberry' последним узлом и потомком 'Red'
Для начала нам нужно немного места. Правое значение узла 'Red' должно поменяться с 6 на 8, узла 7-10 'Yellow' должны поменяться на 9-12, и так далее.
Обновления узла 'Red' означает что нам надо увеличить на 2 все правые и левые значения, большие либо равные 6.

Что ж, используем запрос
UPDATE tree SET rgt=rgt+2 WHERE rgt>=6;
UPDATE tree SET lft=lft+2 WHERE lft>=6;
Теперь просто добавим новый узел ‘Strawberry', чтобы заполнить освободившееся пространство. Этот узел будет иметь числа 6 левое и 7 правое.
Теперь если мы запустим нашу функцию display_tree(), мы увидим что наш новый узел ‘Strawberry' успешно добавлен в дерево:
Food
 Fruit
   Red
     Cherry
     Strawberry
   Yellow
     Banana
 Meat
   Beef
   Pork
Удаление узлов происходит подобным образом - уменьшением всех правых и левых чисел справа от удаляемого узла. Если надо удалить узел, у которого есть потомки, то надо по очереди удалить сначала всех потомков, а затем уже удалить нужный узел.

Другая ситауция возникает когда нам надо изменить родителя у какого-нибудь узла, в этом случае алгоритм существенно усложняется. Мы не будем затрагивать этот случай в рамках этой статьи, так как она имеет статус "Для новичков" Тем более если у переносимого узла у самого есть потомки. В этом случае проще было бы использовать симбиоз этого метода с методом ссылок на родителя - просто поменять родителя у узла, а затем запустить функцию rebuild_tree().
То есть использовать метод MPTT для быстрого доступа к узлам и частям дерева, а метод ссылок на родителя для редактирования дерева с последующим пересчетом левых и правых значений.

Заключение

Алгоритм MPTT выглядет сложным для понимания. Он не такой простой как метод ссылок на родителя. Тем не менее, как только вы его попробуете вы сразу поймете что с его помощью можно сделать то же самое что и с помощью метода ссылок на родителя, но алгоритм MPTT гораздо быстрее и менее ресурсоемок, нежели метод ссылок на родителя. Обновление дерева конечно медленнее, так как для этого нужно больше запросов к базе данных, на зато доступ к веткам и узлам становится гораздо быстрее и заключается как правило в одном запросе к БД. А так как обновление дерева необходимо происводить гораздо реже чем чтение с него, преимущества налицо.

Оценить Статью:  
1   2   3   4   5   6   7   8   9   10    

« Назад
SAPE все усложнил?

MainLink - простая и прибыльная продажа ссылок!

Последние поступления:

Стишки пирожки про Олега⁠⁠

Размещена 20 июня 2024 года

Олег купил презервативы
Проник в семидесятый год
И подарил их папе с мамой
Такой нелепый суицид

читать далее…

Размещена 10 августа 2020 года

Я по ТВ видел, что через 10 лет мы будем жить лучше, чем в Германии...
Я не понял, что это они с Германией сделать хотят?!

читать далее…

ТехЗадание на Землю

Размещена 14 марта 2018 года

Пpоект Genesis (из коpпоpативной пеpеписки)

читать далее…

Шпаргалка по работе с Vim

Размещена 05 декабря 2017 года

Vim довольно мощный редактор, но работа с ним не всегда наглядна.
Например если нужно отредактировать какой-то файл например при помощи crontab, без знания специфики работы с viv никак.

читать далее…

Ошибка: Error: Cannot find a valid baseurl for repo

Размещена 13 сентабря 2017 года

Если возникает ошибка на centos 5 вида
YumRepo Error: All mirror URLs are not using ftp, http[s] or file.
Eg. Invalid release/

читать далее…