Построить динамическое меню, используя вложенные наборы - PullRequest
0 голосов
/ 19 июня 2009

Я пытаюсь создать динамическое меню в моей PHP CMS; страницы / категории организованы с использованием модели вложенных множеств.

Полное дерево:

root
 A
 B
  B1
   B1.1
   B1.2
  B2
   B2.1
   B2.1
 C
  C1
  C2
  C3
 D

Я хочу преобразовать этот результирующий набор в неупорядоченный список, который отображает только часть дерева. Например: Если я нажму на B, я хочу показать только следующую часть списка:

A
B
 B1
 B2
C
D

Далее, если я нажму на B1, я хочу показать этот список:

A
B
 B1
  B1.1
  B1.2
 B2
C
D

и т.д.

Я использую следующий запрос SQL, чтобы получить все узлы из базы данных (mysql):

SELECT node.id, node.lft, node.rgt, node.name, 
GROUP_CONCAT(parent.name ORDER BY parent.lft  SEPARATOR "/" ) AS path, 
(COUNT(parent.lft) - 1) AS depth 
FROM pages AS node, pages AS parent 
WHERE node.lft BETWEEN parent.lft AND parent.rgt 
AND ( parent.hidden = "no" AND node.hidden = "no")  AND parent.lft > 1 
GROUP BY node.id ORDER BY node.lft

Мне удалось создать полный список без рекурсии (используя столбец глубины), но я не могу отфильтровать меню, как показано выше; Я думаю, что мне нужно получить значения lft и rgt для каждого узла и отфильтровать элементы с помощью PHP. Но как я могу получить эти значения в одном запросе?

Есть ли другие предложения о том, как этого добиться?

Заранее спасибо!

Ответы [ 4 ]

1 голос
/ 25 июня 2009

Следующий запрос позволит вам открыть любой путь (или набор путей), воспользовавшись предложением SQL и функцией MySQL group_concat .

Ниже приведены определение таблицы и примеры данных, которые я использовал:

drop table nested_set;

CREATE TABLE nested_set (
 id INT,
 name VARCHAR(20) NOT NULL,
 lft INT NOT NULL,
 rgt INT NOT NULL
);

INSERT INTO nested_set (id, name, lft, rgt) VALUES (1,'HEAD',1,28);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (2,'A',2,3);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (3,'B',4,17);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (4,'B1',5,10);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (5,'B1.1',6,7);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (6,'B1.2',8,9);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (7,'B2',11,16);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (8,'B2.1',12,13);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (9,'B2.2',14,15);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (10,'C',18,25);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (11,'C1',19,20);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (12,'C2',21,22);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (13,'C3',23,24);
INSERT INTO nested_set (id, name, lft, rgt) VALUES (14,'D',26,27);

Следующий запрос дает вам все дерево (кроме HEAD):

SELECT
  node.id
, node.lft
, node.rgt
, node.name
,  GROUP_CONCAT(parent.name ORDER BY parent.lft  SEPARATOR "/" ) AS path
,  (COUNT(parent.lft) - 1) AS depth
FROM nested_set AS node
inner join nested_set AS parent
on node.lft BETWEEN parent.lft AND parent.rgt
where parent.lft > 1
GROUP BY node.id

С выводом следующего при запуске с данными образца:

+------+-----+-----+------+-----------+-------+
| id   | lft | rgt | name | path      | depth |
+------+-----+-----+------+-----------+-------+
|    2 |   2 |   3 | A    | A         |     0 |
|    3 |   4 |  17 | B    | B         |     0 |
|    4 |   5 |  10 | B1   | B/B1      |     1 |
|    5 |   6 |   7 | B1.1 | B/B1/B1.1 |     2 |
|    6 |   8 |   9 | B1.2 | B/B1/B1.2 |     2 |
|    7 |  11 |  16 | B2   | B/B2      |     1 |
|    8 |  12 |  13 | B2.1 | B/B2/B2.1 |     2 |
|    9 |  14 |  15 | B2.2 | B/B2/B2.2 |     2 |
|   10 |  18 |  25 | C    | C         |     0 |
|   11 |  19 |  20 | C1   | C/C1      |     1 |
|   12 |  21 |  22 | C2   | C/C2      |     1 |
|   13 |  23 |  24 | C3   | C/C3      |     1 |
|   14 |  26 |  27 | D    | D         |     0 |
+------+-----+-----+------+-----------+-------+

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

having
depth = 0
or ('<PATH_TO_OPEN>' =  left(path, length('<PATH_TO_OPEN>'))
   and depth = length('<PATH_TO_OPEN>') - length(replace('<PATH_TO_OPEN>', '/', '')) + 1)

Предложение has применяет фильтры к результатам группы по запросу. Часть «deep = 0» предназначена для того, чтобы у нас всегда были узлы базового меню (A, B, C и D). Следующая часть - это часть, которая контролирует, какие узлы открыты. Он сравнивает путь узлов с заданным путем, который вы хотите открыть (''), чтобы увидеть, совпадает ли он, и также проверяет, что он только открывает уровень в пути. Весь раздел или раздел с логикой '' можно дублировать и добавлять по мере необходимости, чтобы при необходимости открывать несколько путей. Убедитесь, что '' не заканчивается косой чертой (/).

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

=========Open B==========

SELECT
  node.id
, node.lft
, node.rgt
, node.name
,  GROUP_CONCAT(parent.name ORDER BY parent.lft  SEPARATOR "/" ) AS path
,  (COUNT(parent.lft) - 1) AS depth
FROM nested_set AS node
inner join nested_set AS parent
on node.lft BETWEEN parent.lft AND parent.rgt
where parent.lft > 1
GROUP BY node.id
having
depth = 0
or ('B' =  left(path, length('B'))
   and depth = length('B') - length(replace('B', '/', '')) + 1)

+------+-----+-----+------+------+-------+
| id   | lft | rgt | name | path | depth |
+------+-----+-----+------+------+-------+
|    2 |   2 |   3 | A    | A    |     0 |
|    3 |   4 |  17 | B    | B    |     0 |
|    4 |   5 |  10 | B1   | B/B1 |     1 |
|    7 |  11 |  16 | B2   | B/B2 |     1 |
|   10 |  18 |  25 | C    | C    |     0 |
|   14 |  26 |  27 | D    | D    |     0 |
+------+-----+-----+------+------+-------+

=========Open B and B/B1==========

SELECT
  node.id
, node.lft
, node.rgt
, node.name
,  GROUP_CONCAT(parent.name ORDER BY parent.lft  SEPARATOR "/" ) AS path
,  (COUNT(parent.lft) - 1) AS depth
FROM nested_set AS node
inner join nested_set AS parent
on node.lft BETWEEN parent.lft AND parent.rgt
where parent.lft > 1
GROUP BY node.id
having
depth = 0
or ('B' =  left(path, length('B'))
   and depth = length('B') - length(replace('B', '/', '')) + 1)
or ('B/B1' =  left(path, length('B/B1'))
   and depth = length('B/B1') - length(replace('B/B1', '/', '')) + 1)

+------+-----+-----+------+-----------+-------+
| id   | lft | rgt | name | path      | depth |
+------+-----+-----+------+-----------+-------+
|    2 |   2 |   3 | A    | A         |     0 |
|    3 |   4 |  17 | B    | B         |     0 |
|    4 |   5 |  10 | B1   | B/B1      |     1 |
|    5 |   6 |   7 | B1.1 | B/B1/B1.1 |     2 |
|    6 |   8 |   9 | B1.2 | B/B1/B1.2 |     2 |
|    7 |  11 |  16 | B2   | B/B2      |     1 |
|   10 |  18 |  25 | C    | C         |     0 |
|   14 |  26 |  27 | D    | D         |     0 |
+------+-----+-----+------+-----------+-------+

=========Open B and B/B1 and C==========

SELECT
  node.id
, node.lft
, node.rgt
, node.name
,  GROUP_CONCAT(parent.name ORDER BY parent.lft  SEPARATOR "/" ) AS path
,  (COUNT(parent.lft) - 1) AS depth
FROM nested_set AS node
inner join nested_set AS parent
on node.lft BETWEEN parent.lft AND parent.rgt
where parent.lft > 1
GROUP BY node.id
having
depth = 0
or ('B' =  left(path, length('B'))
   and depth = length('B') - length(replace('B', '/', '')) + 1)
or ('B/B1' =  left(path, length('B/B1'))
   and depth = length('B/B1') - length(replace('B/B1', '/', '')) + 1)
or ('C' =  left(path, length('C'))
   and depth = length('C') - length(replace('C', '/', '')) + 1)

+------+-----+-----+------+-----------+-------+
| id   | lft | rgt | name | path      | depth |
+------+-----+-----+------+-----------+-------+
|    2 |   2 |   3 | A    | A         |     0 |
|    3 |   4 |  17 | B    | B         |     0 |
|    4 |   5 |  10 | B1   | B/B1      |     1 |
|    5 |   6 |   7 | B1.1 | B/B1/B1.1 |     2 |
|    6 |   8 |   9 | B1.2 | B/B1/B1.2 |     2 |
|    7 |  11 |  16 | B2   | B/B2      |     1 |
|   10 |  18 |  25 | C    | C         |     0 |
|   11 |  19 |  20 | C1   | C/C1      |     1 |
|   12 |  21 |  22 | C2   | C/C2      |     1 |
|   13 |  23 |  24 | C3   | C/C3      |     1 |
|   14 |  26 |  27 | D    | D         |     0 |
+------+-----+-----+------+-----------+-------+

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

См. http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/, если вам нужна общая информация о работе с вложенными наборами в MySQL.

Дайте мне знать, если у вас есть какие-либо вопросы.

НТН,

-Dipin

0 голосов
/ 24 сентября 2009

Хороший пост о том, как создать вложенный набор с нуля, который написал друг, находится здесь; Вложенный набор в MySQL

Может быть, это полезно для вас.

0 голосов
/ 24 сентября 2009

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

- Ответ Дипинса был тот, на котором я основывал свой прогресс, и теперь я думаю, что у меня есть решение без всех «ИЛИ».

Просто замените имеющую часть на:

HAVING
  depth = 1
  OR
  '".$path."' LIKE CONCAT(SUBSTRING(path, 1, (LENGTH(path) - LENGTH(menu_node_name) -1)), '%')

$path = requested path. parent node's path that the user clicked, "A/B" for example

path = the path of the current node including the nodes name "A/B/B1" for example, which is a child for the node the user clicked.

menu-node-name = the name of the node in progress, "B1" for example.

Что он делает, так это сравнивает запрошенный путь, скажем, A / B / B1 с путем узла. Путь к узлу требовал серьезной работы. LIKE path-of-node% работал, но он давал только верхний уровень и не давал никаких других узлов на том же уровне. Эта версия делает.

Мы объединяем path_of_node с подстановочным знаком (%), который означает, что после него может быть что угодно. Подстрока REMOVES имеет собственное имя и тире, делая path_of_node фактически путем к его родительскому узлу . Таким образом, A / B / B1 становится «A / B%», что соответствует нашему запросу, если мы щелкаем ссылку, чтобы открыть новое поддерево.

Причина, по которой у меня глубина = 1, заключается в том, что у меня может быть несколько меню в одном дереве, и я не хочу, чтобы люди видели что-то вроде «МЕНЮ ДЛЯ БОГАТЫХ ЛЮДЕЙ», «МЕНЮ ДЛЯ БЕДНЫХ ЛЮДЕЙ» или как там ни зовут имена. Узлы верхнего уровня моего набора являются своего рода удерживающими узлами, я исключаю их из фактического результата.

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

Я думаю, через несколько дней вы можете подтвердить, что это сработало, посмотрев на www.race.fi

РЕДАКТИРОВАТЬ / ПРИМЕЧАНИЕ:

Я проверил еще немного, и похоже, что порядок был ошибочным. Вот быстрая копия моего запроса с правильным порядком. Есть некоторые ненужные вещи, такие как locales, content и content_localised, но ключевые моменты должны быть ясными.

SELECT
    REPEAT('-',(COUNT(MENU.par_name) - 2)) as indt,
    GROUP_CONCAT(MENU.par_name ORDER BY MENU.par_lft  SEPARATOR '/' ) AS path,
    (COUNT(MENU.par_lft) - 1) AS depth,
    MENU.*,
    MENU.content
FROM 
    (SELECT 
        parent.menu_node_name AS par_name,
        parent.lft AS par_lft,
        node.menu_node_id,
        node.menu_node_name,
        node.content_id,
        node.node_types,
        node.node_iprop,
        node.node_aprop,
        node.node_brands,
        node.rgt,
        node.lft,
        [TPF]content_localised.content

    FROM [TPF]" . $this->nestedset_table . " AS node 
    JOIN [TPF]" . $this->nestedset_table . " AS parent
            ON node.lft BETWEEN parent.lft AND parent.rgt
    JOIN [TPF]content
        ON node.content_id = [TPF]content.content_id
    JOIN [TPF]content_localised
        ON [TPF]content.content_id = [TPF]content_localised.content_id  
    JOIN [TPF]locales 
        ON [TPF]content_localised.locale_id = [TPF]locales.locale_id

    ORDER BY node.rgt, FIELD(locale, '" . implode("' , '", $locales) . "', locale) ASC
    ) AS MENU

GROUP BY MENU.menu_node_id
HAVING depth = 1
    OR '".$path."' LIKE CONCAT(SUBSTRING(path, 1, (LENGTH(path) - LENGTH(MENU.menu_node_name) -1)), '%')
    AND depth > 0
ORDER BY MENU.lft";
0 голосов
/ 21 июня 2009

Вписывается ли в рамки вашего проекта просто скрытие нежелательных элементов? например (css):

  • .menu li> ul {display: none;}
  • .menu li.clicked> ul {display: block;}

Затем используйте javascript, чтобы добавить класс «clicked» к любому элементу

, по которому щелкнули Обратите внимание, что этот CSS не будет работать в IE6.
...