使用递归 SQL 创建 DNN 导航菜单
我正在开发一个包含树形导航菜单的 DotNetNuke 模块。
到目前为止,我已经可以正常工作了,从某种意义上说,子节点已连接到其正确的父节点,但节点兄弟姐妹仍然没有顺序。有一个名为 TabOrder 的字段,用于确定兄弟姐妹的顺序,但由于递归,我无法对它们进行正确排序。
我正在尝试在 SQL Server 存储过程中执行此操作,这可能是一个错误,但我觉得我已经很接近了,必须有一个解决方案。有谁知道我做错了什么?
如果您有任何想法,我将不胜感激。提前致谢。
解决方案:
我终于找到了我的问题的解决方案。关键是递归地创建从根选项卡到叶选项卡的选项卡沿袭(TabLevel + TabOrder)。创建后,我就能够正确排序返回的记录。
然而,当我回来发布这篇文章时,我看到了 MarkXA 的答案,这可能是最好的解决方案。我什至不知道 GetNavigationNodes 方法的存在。
我认为他是正确的,使用 GetNavigationNodes 是一种更面向未来的解决方案,但目前我将使用基于 SQL 的解决方案。 ——我能说什么?我学习的方式很艰难。
这里是:
ALTER procedure [dbo].[Nav_GetTabs]
@CurrentTabID int = 0
AS
--============================================================
--create and populate @TabLineage table variable with Tab Lineage
--
--"Lineage" consists of the concatenation of TabLevel & TabOrder, concatenated recursively from the root to leaf.
--The lineage is VERY important, making it possible to properly order the Tab links in the navigation module.
--This will be used as a lookup table to match Tabs with their lineage.
--============================================================
DECLARE @TabLineage table
(
TabID int,
Lineage varchar(100)
);
WITH TabLineage AS
(
--start with root Tabs
SELECT T.TabID, T.ParentID, CAST(REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
FROM Tabs T
INNER JOIN Tabs T2 ON T.TabID = T2.TabID
INNER JOIN TabPermission TP ON T.TabID = TP.TabID
WHERE T.ParentID IS NULL
AND T.IsDeleted = 0
AND T.IsVisible = 1
AND TP.RoleID = -1
UNION ALL
--continue recursively, from parent to child Tabs
SELECT T.TabID, T.ParentID, CAST(TL.Lineage + REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
FROM Tabs T
INNER JOIN Tabs T2 ON T.TabID = T2.TabID
INNER JOIN TabPermission TP ON T.TabID = TP.TabID
INNER JOIN TabLineage TL ON T.ParentID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
AND TP.RoleID = -1
)
--insert results of recursive query into temporary table
INSERT @TabLineage
SELECT TL.TabID, TL.Lineage FROM TabLineage TL ORDER BY TL.Lineage
OPTION (maxrecursion 10); --to increase number of traversed generations, increase "maxrecursion"
--============================================================
--create and populate @Ancestor table variable with @CurrentTab ancestors
--
--"Ancestors" are Tabs following the path from @CurrentTab to the root Tab it's descended from (inclusively).
--These are Tab links we want to see in the navigation.
--============================================================
DECLARE @Ancestor table
(
TabID int
);
WITH Ancestor AS
(
--start with @CurrentTab
SELECT T.TabID, T.ParentID FROM Tabs T WHERE T.TabID = @CurrentTabID
UNION ALL
--continue recursively, from child to parent Tab
SELECT T.TabID, T.ParentID
FROM Ancestor A INNER JOIN Tabs T ON T.TabID = A.ParentID
)
--insert results of recursive query into temporary table
INSERT @Ancestor
SELECT A.TabID FROM Ancestor A
OPTION (maxrecursion 10); --to increase number of traversed generations, increase "maxrecursion"
--============================================================
--retrieve Tabs to display in navigation
--This section UNIONs three query results together, giving us what we want:
-- 1. All Tabs at Level 0.
-- 2. All Tabs in @CurrentTab's lineage.
-- 3. All Tabs which are children of Tabs in @CurrentTab's lineage.
--============================================================
WITH TabNav (TabID, TabLevel, TabName, Lineage) AS
(
--retrieve all Tabs at Level 0 -- (Root Tabs)
(SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
AND T.[Level] = 0
UNION
--retrieve Tabs in @CurrentTab's lineage
SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN @Ancestor A ON T.TabID = A.TabID
INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
UNION
--retrieve Tabs which are children of Tabs in @CurrentTab's lineage
SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN @Ancestor A ON T.ParentID = A.TabID
INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1)
)
--finally, return the Tabs to be included in the navigation module
SELECT TabID, TabLevel, TabName FROM TabNav ORDER BY Lineage;
--============================================================
I'm working on a DotNetNuke module that includes a tree-style navigation menu.
So far, I have it working, in the sense that child-nodes are connected to their correct parent-nodes, but the node-siblings are still out of order. There's a field called TabOrder, used to determine the order of siblings, but due to the recursion, I can't get them sorted properly.
I'm trying to do this in a SQL Server stored procedure, which may be a mistake, but I feel I'm so close that there must be a solution. Does anyone have any idea what I'm doing wrong?
I'd appreciate any ideas you have. Thanks in advance.
Solution:
I finally found a solution to my question. The key was to recursively create a Tab Lineage (TabLevel + TabOrder) from the Root Tab to the Leaf Tabs. Once that was created, I was able to order the returned records properly.
However, as I was coming back to post this I saw MarkXA's answer, which is probably the best solution. I didn't know the method GetNavigationNodes even existed.
I think he is correct that using GetNavigationNodes is a more future-proof solution, but for the time being I'll use my SQL-based solution. --What can I say? I learn the hard way.
Here it is:
ALTER procedure [dbo].[Nav_GetTabs]
@CurrentTabID int = 0
AS
--============================================================
--create and populate @TabLineage table variable with Tab Lineage
--
--"Lineage" consists of the concatenation of TabLevel & TabOrder, concatenated recursively from the root to leaf.
--The lineage is VERY important, making it possible to properly order the Tab links in the navigation module.
--This will be used as a lookup table to match Tabs with their lineage.
--============================================================
DECLARE @TabLineage table
(
TabID int,
Lineage varchar(100)
);
WITH TabLineage AS
(
--start with root Tabs
SELECT T.TabID, T.ParentID, CAST(REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
FROM Tabs T
INNER JOIN Tabs T2 ON T.TabID = T2.TabID
INNER JOIN TabPermission TP ON T.TabID = TP.TabID
WHERE T.ParentID IS NULL
AND T.IsDeleted = 0
AND T.IsVisible = 1
AND TP.RoleID = -1
UNION ALL
--continue recursively, from parent to child Tabs
SELECT T.TabID, T.ParentID, CAST(TL.Lineage + REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
FROM Tabs T
INNER JOIN Tabs T2 ON T.TabID = T2.TabID
INNER JOIN TabPermission TP ON T.TabID = TP.TabID
INNER JOIN TabLineage TL ON T.ParentID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
AND TP.RoleID = -1
)
--insert results of recursive query into temporary table
INSERT @TabLineage
SELECT TL.TabID, TL.Lineage FROM TabLineage TL ORDER BY TL.Lineage
OPTION (maxrecursion 10); --to increase number of traversed generations, increase "maxrecursion"
--============================================================
--create and populate @Ancestor table variable with @CurrentTab ancestors
--
--"Ancestors" are Tabs following the path from @CurrentTab to the root Tab it's descended from (inclusively).
--These are Tab links we want to see in the navigation.
--============================================================
DECLARE @Ancestor table
(
TabID int
);
WITH Ancestor AS
(
--start with @CurrentTab
SELECT T.TabID, T.ParentID FROM Tabs T WHERE T.TabID = @CurrentTabID
UNION ALL
--continue recursively, from child to parent Tab
SELECT T.TabID, T.ParentID
FROM Ancestor A INNER JOIN Tabs T ON T.TabID = A.ParentID
)
--insert results of recursive query into temporary table
INSERT @Ancestor
SELECT A.TabID FROM Ancestor A
OPTION (maxrecursion 10); --to increase number of traversed generations, increase "maxrecursion"
--============================================================
--retrieve Tabs to display in navigation
--This section UNIONs three query results together, giving us what we want:
-- 1. All Tabs at Level 0.
-- 2. All Tabs in @CurrentTab's lineage.
-- 3. All Tabs which are children of Tabs in @CurrentTab's lineage.
--============================================================
WITH TabNav (TabID, TabLevel, TabName, Lineage) AS
(
--retrieve all Tabs at Level 0 -- (Root Tabs)
(SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
AND T.[Level] = 0
UNION
--retrieve Tabs in @CurrentTab's lineage
SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN @Ancestor A ON T.TabID = A.TabID
INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
UNION
--retrieve Tabs which are children of Tabs in @CurrentTab's lineage
SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN @Ancestor A ON T.ParentID = A.TabID
INNER JOIN @TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1)
)
--finally, return the Tabs to be included in the navigation module
SELECT TabID, TabLevel, TabName FROM TabNav ORDER BY Lineage;
--============================================================
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
答案是“不要使用 SQL”。已经有一个方法 DotNetNuke.UI.Navigation.GetNavigationNodes 可以为您执行此操作,如果您使用它,那么即使数据库架构发生更改,您的模块也不会中断。即使您需要做一些 GetNavigationNodes 无法处理的事情,您仍然最好通过 API 检索页面,以便面向未来。直接去数据库只是自找麻烦:)
The answer is "don't use SQL". There's already a method DotNetNuke.UI.Navigation.GetNavigationNodes that does this for you, and if you use it then your module won't break if and when the database schema changes. Even if you need to do something that GetNavigationNodes won't handle, you're still better off retrieving the pages via the API to be futureproof. Going directly to the database is just asking for trouble :)
这里是一个递归树 CTE 的样板(不基于给定 OP 的代码)示例,它展示了如何对树进行排序:
输出:
关键是
LevelOf
列。在 CTE 中选择主要父级时,看看它如何只是一个文字1
。然后,LevelOf
列在递归 CTE 的UNION ALL
部分中递增。对 CTE 的每次递归调用(不是行)都会命中该UNION ALL
一次并递增。没有比这更多的了。here is a boiler plate (not based on the given OP's code) example of a recursive tree CTE, which shows how to sort a tree:
OUTPUT:
The key is the
LevelOf
column. See how it is just a literal1
when selecting the main parent in the CTE. TheLevelOf
column is then incremented in theUNION ALL
portion of the recursive CTE. Each recursive call (not row) to the CTE will hit thatUNION ALL
one time and the increment. Not a whole lot more to it than that.