Метод хранения древовидных структур данных Adjacency List один из самых простых в понимании, так как узел включает прямую связь с родителем и не перенасыщен избыточными данными.
По сути за структуру девева отвечает два поля: id и pid и вычислить цепочку наследования довольно просто. Правда наследование ограниченно одним уровнем, т.е. за один шаг мы можем вычислить только родительский узел или ребенка, но не "дедушек" и "внуков".
Создадим таблицу для нашего дерева:
Где:
id - идентификатор узла;
pid - идентификатор родительского узла;
Соответственно id у нас либо AUTO_INCREMENT или SERIAL.
Как использовать
Кстати да, выборка данных из Adjacency List, по моему мнению, самая сложная, ибо подводных камней неимоверное количество. Хотя, на самом деле, все зависит от степени понимания работы других алгоритмов.
Определим, как мы можем выбирать данные из этого дерева. Для начала соберем список требуемых данных:
родительский узел;
подчиненные узлы;
родительская ветка;
подчиненные ветви;
Существует два основных аглоритма выборки - с использованием рекурсивной процедуры и обычным формированием массива.
Уровень реализации решения, может быть тоже различен, эти задачи можно решить как на уровне приложения так и на уровне базы данных, все зависит только лишь от знаний разработчика и его пристрастий.
Родительский узел
Подчиненные узлы
Эти две задачи самые простые и решаются буквально одним запросом, так родительский узел можно получить с помощью запроса:
... а подчиненные узлы:
... начало многообещающее, но простыми выборками закончили, теперь более сложное действо:
Родительская ветка
Когда нам требуется выбрать всю родительскую ветку, то мы не можем её выбрать одним стандартным запросом. Впрочем, одним запросом мы можем выбрать, но он будет динамическим, то есть формироваться относительно текущего узла.
Выбрать родителькую ветку одним запросом с помощью JOIN таблицы, по моему мнению не самый правильный способ, так как мы выберем несколько строк, склееных в одну, и потом нам придется её нарезать, а так же "многоэтажный" JOIN не самое лучшее решение. Но такое решение имеет место быть, поэтому рассмотрим его.
Если использовать такой вариант решения, то рекомендую добавить в таблицу дополнительное поле level, в котором будем указывать уровень узла. В итоге для получения родительской ветки получим запрос вида:
вышеуказанный код динамически формирует запрос к базе данных такого вида:
Ресурсоемко, но работает. Впрочем, это еще не предел. Можно собрать более сложный запрос, который будет возвращать не одну строку, а как и положено - несколько:
Список-то такой запрос вернет, но использовать такой алгоритм крайне не рекомендовано, так как количество реальных запросов к таблице будет равно сумме натуральных чисел уровня, то бишь, чуть более чем дофига.
Разработчикам использующим PostgreSQL еще проще, так как начиная с версии 8.4 PostgreSQL поддерживает рекурсивные запросы:
Немного поясню, для тех кто не понял: WITH RECURSIVE мы создаем рекурсивный подзапрос parents, первый запрос: первый запрос рекурсии, в нем указываем точку отсчета. Второй запрос, собственно, рекурсивный запрос. Поле exist - массив уже полученных id, который мы проверяем в поле cycle, что бы не уйти в бесконечную рекурсию. LIMIT ограничивает глубину выборки ветки, так как родитель у узла может быть только лишь один.
На этом решения использующие всего один запрос можно считать исчерпаны. Теперь можно рассмотреть следущие алгоритмы:
Рекурсивная функция по определению - это функция вызывающая саму себя, поэтому очень важно соблюдать область видимости переменных внутри нее, итак:
Практически, ничего сложного, правда мы делаем несколько запросов, в зависимости от заданной глубины или длинны родительской ветки.
Можно обойтись и без рекурсивной функции посредством простого цикла динамического массива:
Это решение проще в понимании и менее ресурсоемко, так как нет лишних вызовов, но, на самом деле, в основном все зависит от пристрастий разработчика, так как оба решения по сути - равнозначны. Кстати использовать динамический массив в этой задаче вовсе не обязательно, так как родитель у узла всегда один, можно обойтись обычной проверкой текущего pid, что, впрочем, рассмотрим далее.
Так же можно еще решить эту задачу используя хранимые процедуры базы данных:
Для PostgreSQL:
Я выбрал второй алгоритм циклической выборки, так как он более простой в понимании, тем более, из-за того, что родитель всего один, то можно вообще не использовать динамический массив, а проверять один параметр pin_now.
Вызов на уровне приложения более чем прост:
Для MySQL:
Конечно, приходится жестко указывать получаемые поля из запросов, но увы, либо так, тибо лишние запросы на получение pid.
Есть одно "но" - процедуры в MySQL возвращают несколько result set для каждого запроса внутри процедуры, что немного усложняет получение результата на уровне приложения, но не на много:
Так, при использовании Perl DBI код получения будет выглядеть так:
При использовании PHP:
Правда для этого нам потребуется MySQLi. Хотя можно обойтись и без нескольких result sets, но это рассмотрим далее.
Итак мы рассмотрели основные алгоритмы выборки родительской ветки, продолжаем:
Подчиненные ветви
Основное отличие выборки подчиненной ветки от выборки родительских узлов заключается в направлении выборки и в том, что подчиненных узлов может быть долее одного на каждом из уровней выборки. Это накладывает довольно существенные ограничения и изменения на алгоритмы.
Так, мы не можем знать максимальную глубину выборки по дополнительному полю level, поэтому варианты запросов с JOIN таблицы отпадает. Впрочем, можно вычислять максимальную глубину дерева и относительно нее формировать запросы с JOIN но это решение будет настолько ресурсоемким для базы данных, что сводит на нет любую оптимизацию.
Хотя, для разработчиков, использующих PostreSQL 8.4, есть возможность использовать рекурсивные запросы, потребуется сделать определенный "финт ушами":
Где:
[order_field] - поле сортировки в пределах подчинения;
[item.id] - ID узла от которого производится выборка;
[max_deep] - максимальная глубина рекурсии;
[limit] - количество выбираемых строк;
Основной особенностью этого запроса является дополнительное поле path, которое практически является Materialized path, за исключением того, что мы добавляем в него дополнительное поле сортировки текущего узла.
Причем защита от зацикливания (поля exist и cycle) локализована в пределах отдельных ветвей, поэтому это хоть и предохранит от бесконечного зацикливания, но позволит повторно выбрать строки, так что будте внимательны.
В общем, пока вышеуказанные пользователи PostgreSQL 8.4 тихо радуются, остальные пишут свои процедуры.
Рекурсивная процедура выборки подчиненных ветвей:
Кажется, что все хорошо, но стоит посмотреть, что происходит во время выполнения этого кода. Так, для каждого из узлов ветви делается запрос на получение его потомков, в итоге, к базе данных делается ровно столько запросов, сколько мы получим в итоге.
Данную проблему можно решить двумя способами:
Ввести денормализацию таблицы создав дополнительное поле счетчика потомков. В этом случае можно будет проверять наличие потомков и делать или нет запрос на их получение. Это решение рассмотрено более детально в статье "Управление деревьями Adjacency List";
Производить выборки не построчно, а списочно для каждого уровня вложенности. Это решение подробно рассмотрим здесь;
Так как мы выбираем список потомков узла, то из него можно сформировать массив pid для следующего уровня вложенности. Но, следует иметь ввиду, что список потомков должен возвращаться в строго определенном порядке, относительно порядка выборки родительских узлов. Это делается довольно просто:
Где:
[parents_array] - массив id родительских узлов;
[order_field] - поле сортировки;
Отсюда реализуем алгоритм выборки. В качестве базового алгоритма возьмем алгоритм выборки посредством цикла динамического массива:
Логика немного сложнее, но гораздо дешевле по ресурсам.
Теперь можно и хранимые процедуры сделать для получения подчиненных ветвей:
Для PostgreSQL:
Делать рекурсивную хранимую процедуру я бы не рекомендовал, так как для этого придется передавать достаточно много дополнительных переменных и массивов при каждом её вызове.
Для MySQL хранимой процедуры я делать не буду, так как, как не лепил все одно кучка говна получается. БД MySQL не рассчитана та такие сложности.