使用嵌套集构建动态菜单

时间:2009-06-18 22:52:43

标签: php sql mysql nested-sets

我正在尝试在PHP CMS中构建动态菜单;页面/类别使用嵌套集模型进行组织。

完整的树:

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

我想将此结果集转换为unordererd列表,该列表仅显示树的一部分。 例如: 如果我点击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 个答案:

答案 0 :(得分:1)

以下查询将允许您利用SQL的having子句和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)

having子句通过查询将过滤器应用于组的结果。 “depth = 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 |
+------+-----+-----+------+-----------+-------+

就是这样。你只需要为你需要打开的每个路径复制那个或部分。

如果您需要有关在MySQL中使用嵌套集的一般信息,请参阅http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/

如果您有任何问题,请与我们联系。

HTH,

-Dipin

答案 1 :(得分:0)

我意识到这可能是一个老问题,但当我偶然发现同样的问题时,我决定提供一些意见,以便其他人也能从中受益。

-Dipins回答是我根据我的进展得出的答案,现在我认为我有一个没有所有'OR'的解决方案。

只需将部分替换为:

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%确实有效,但它只给出了上层,并没有给出同一级别的任何其他节点。这个版本确实如此。

WE将path_of_node与通配符(%)连接起来,这意味着任何东西都可以在它之后。子串 REMOVES 节点拥有的名称和短划线,使path_of_node实际上是它的父级节点的路径。因此,如果我们点击链接打开一个新的子树,A / B / B1就会变成“A / B%”,这与我们的请求相符。

我有深度= 1的原因是我可能在同一个树中有多个菜单,我不希望人们看到类似“菜单丰富的人”,“菜单为穷人”的东西或者无论名称是什么。我的集合的顶级节点有点保持节点,我将它们从实际结果中排除。

我希望这证明对某人有用,至少我找了几个小时的解决方案然后想出来。

我想,有几天你可以通过查看www.race.fi

来确认这是有效的

编辑/注意:

我测试了一些,看起来排序有问题。这是我的查询的快速copypaste与正确的顺序。有一些不必要的东西,如语言环境,内容和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";

答案 2 :(得分:0)

关于如何从朋友写的基础上构建嵌套集的好帖子就在这里; Nested Set in MySQL

也许这对你有帮助。

答案 3 :(得分:-1)

只是隐藏不需要的元素,是否适合您的项目范围?例如(css):

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

然后使用javascript将“点击”类添加到任何&lt; li&gt;被点击的元素。请注意,此CSS在IE6中不起作用。