我的 SQL Server 表中有一个树形结构。
当所有后代节点都变成嵌套 JSON 对象时,我需要将表数据转换为具有子级的 Web 树的 JSON 格式。
我有这个数据表:
DROP TABLE IF EXISTS #tTree;
CREATE TABLE #tTree
(
id INTEGER IDENTITY(1,1),
text VARCHAR(256),
parentId INTEGER,
path VARCHAR(256),
depth TINYINT,
leaf TINYINT,
expanded TINYINT
);
INSERT INTO #tTree (text, parentId, path, depth, leaf, expanded)
VALUES ('Category 1', null, '1', 1, null, 1),
('SubCategory 1', 1, '1,2', 2, null, 1),
('Element 1', 2, '1,2,3', 3, 1, null),
('Category 2', null, '4', 1, null, 1),
('SubCategory 2', 4, '4,5', 2, 1, null),
('SubCategory 3', 4, '4,6', 2, 1, null),
('Element 2', 4, '4,7', 2, null, 1),
('SubElement 1', 5, '4,5,8', 3, 1, null),
('SubSubCategory 1', 2, '1,2,9', 3, 1, null),
('Category 3', null, '10', 1, 1, null)
我需要与孩子一起获取 JSON:
[
{
"id":1,
"text":"Category 1",
"path":"1",
"depth":1,
"expanded":1,
"children":[{
"id":2,
"text":"SubCategory 1",
"parentId":1,
"path":"1,2",
"depth":2,
"expanded":1,
"children":[
{"id":3,"text":"Element 1","parentId":2,"path":"1,2,3","depth":3,"leaf":1},
{"id":9,"text":"SubSubCategory 1","parentId":2,"path":"1,2,9","depth":3,"leaf":1}
]
}]
},
{"id":10,"text":"Category 3","path":"10","depth":1,"leaf":1},
{"id":4,
"text":"Category 2",
"path":"4",
"depth":1,
"expanded":1,
"children":[
{"id":5,
"text":"SubCategory 2",
"parentId":4,
"path":"4,5",
"depth":2,
"expanded":1,
"children":[
{"id":8,"text":"SubElement 1","parentId":5,"path":"4,5,8","depth":3,"leaf":1}
]
},
{"id":6,"text":"SubCategory 3","parentId":4,"path":"4,6","depth":2,"leaf":1},
{"id":7,"text":"Element 2","parentId":4,"path":"4,7","depth":2,"leaf":1}
]
}
]
也许这个查询可以以某种方式修改,但现在它没有“孩子”
;WITH cteTree AS
(
SELECT
tree.id
,tree.text
,tree.parentId
,tree.path
,tree.depth
,tree.leaf
,tree.expanded
FROM
#tTree AS tree
WHERE
parentId IS NULL
UNION ALL
SELECT
tree.id
,tree.text
,tree.parentId
,tree.path
,tree.depth
,tree.leaf
,tree.expanded
FROM
#tTree AS tree
INNER JOIN
cteTree ON tree.parentId = cteTree.id
)
SELECT *
FROM cteTree
ORDER BY path ASC
FOR JSON AUTO
不幸的是,在递归 CTE 中进行任何类型的循环聚合都非常困难。这适用于
GROUP BY
和 FOR JSON
。
我为此找到的唯一直接方法是(哦,太恐怖了!)标量 UDF,它会自行递归。
CREATE FUNCTION dbo.GetJson(@parentId int, @path nvarchar(1000), @depth int)
RETURNS nvarchar(max)
AS BEGIN
RETURN (
SELECT
t.id,
t.text,
t.parentId,
path = CONCAT(@path + ',', t.id),
depth = @depth + 1,
t.leaf,
t.expanded,
children = JSON_QUERY(dbo.GetJson(t.id, CONCAT(@path + ',', t.id), @depth + 1))
FROM tTree t
WHERE EXISTS (SELECT t.parentId INTERSECT SELECT @parentId) -- nullable compare
FOR JSON PATH
);
END;
然后您可以这样做以获得您想要的结果
SELECT dbo.GetJson(NULL, NULL, 0);
使用工作表和嵌套迭代的替代方案:
DROP TABLE IF EXISTS #Work;
SELECT
TT.*,
jsn = CONVERT(nvarchar(max), NULL)
INTO #Work
FROM #tTree AS TT;
CREATE UNIQUE CLUSTERED INDEX cuq ON #Work (depth, parentId, id);
DECLARE @depth integer = (SELECT MAX(TT.depth) FROM #tTree AS TT);
WHILE @depth >= 1
BEGIN
UPDATE W
SET W.jsn =
(
SELECT
W.*,
children =
JSON_QUERY
(
(
SELECT
N'[' +
STRING_AGG(W2.jsn, N',')
WITHIN GROUP (ORDER BY W2.id) +
N']'
FROM #Work AS W2
WHERE
W2.depth = @depth + 1
AND W2.parentId = W.id
)
)
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
)
FROM #Work AS W
WHERE
W.depth = @depth
AND W.jsn IS NULL;
SET @depth -= 1;
END;
SELECT
N'[' +
STRING_AGG(W.jsn, N',')
WITHIN GROUP (ORDER BY W.id) +
N']'
FROM #Work AS W
WHERE
W.depth = 1;
这是一个递归解决方案。它按路径顺序构造每行 json 对象,添加“子”元素,并在必要时关闭数组和对象。最终结果是各个对象的串联,包装为数组:
WITH
Recursion AS
(
-- Anchor part
SELECT
TT.id,
depth = 1,
rpath = CONVERT(nvarchar(4000), TT.id),
has_children =
IIF
(
EXISTS
(
SELECT C.*
FROM #tTree AS C
WHERE C.parentId = TT.id
),
1, 0
),
element =
(
SELECT
TT.id,
TT.[text],
TT.[path],
TT.depth,
TT.expanded
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
)
FROM #tTree AS TT
WHERE TT.parentID IS NULL
UNION ALL
-- Recursive part
SELECT
TT.id,
depth = R.depth + 1,
rpath = CONCAT_WS(N'.', R.rpath, TT.id),
has_children =
IIF
(
EXISTS
(
SELECT C.*
FROM #tTree AS C
WHERE C.parentId = TT.id
),
1, 0
),
element =
(
SELECT
TT.id,
TT.[text],
TT.[path],
TT.depth,
TT.expanded
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
)
FROM Recursion AS R
JOIN #tTree AS TT
WITH (FORCESEEK)
ON TT.parentID = R.id
),
AddNextDepth AS
(
SELECT
R.*,
NextDepth =
LEAD(R.depth) OVER (
ORDER BY R.rpath)
FROM Recursion AS R
),
ModifiedTree AS
(
SELECT
ND.rpath,
element =
CONCAT
(
IIF
(
ND.has_children = 0,
ND.element,
-- Insert "children" element
STUFF
(
ND.element,
LEN(ND.element),
1,
N',"children":['
)
),
-- Close previously opened array(s) if necessary
REPLICATE
(
N']}',
-- Number of closures needed
ND.depth - ISNULL(ND.NextDepth, 1)
),
-- Add comma if no children and not the last line
IIF
(
ND.has_children = 0 AND ND.NextDepth IS NOT NULL,
N',',
N''
)
)
FROM AddNextDepth AS ND
)
-- Concatenate objects in path order and add array wrapper
SELECT
CONCAT
(
N'[',
STRING_AGG(MT.element, N'')
WITHIN GROUP (ORDER BY MT.rpath),
N']'
)
FROM ModifiedTree AS MT
OPTION (MAXRECURSION 0);
输出:
[
{
"id": 1,
"text": "Category 1",
"path": "1",
"depth": 1,
"expanded": 1,
"children": [
{
"id": 2,
"text": "SubCategory 1",
"path": "1,2",
"depth": 2,
"expanded": 1,
"children": [
{
"id": 3,
"text": "Element 1",
"path": "1,2,3",
"depth": 3
},
{
"id": 9,
"text": "SubSubCategory 1",
"path": "1,2,9",
"depth": 3
}
]
}
]
},
{
"id": 10,
"text": "Category 3",
"path": "10",
"depth": 1
},
{
"id": 4,
"text": "Category 2",
"path": "4",
"depth": 1,
"expanded": 1,
"children": [
{
"id": 5,
"text": "SubCategory 2",
"path": "4,5",
"depth": 2,
"children": [
{
"id": 8,
"text": "SubElement 1",
"path": "4,5,8",
"depth": 3
}
]
},
{
"id": 6,
"text": "SubCategory 3",
"path": "4,6",
"depth": 2
},
{
"id": 7,
"text": "Element 2",
"path": "4,7",
"depth": 2,
"expanded": 1
}
]
}
]
第二个递归解决方案。
这个方法的工作原理是从根节点开始,构造一个包含所有子节点的列表。不可能立即解决递归子引用,但我们可以偶尔做一点。
每个递归步骤都会解析该级别的可用子级,并且如果新行也有子级,则可能会添加新的未解析子级。最终,所有 cld 引用都得到解决。
WITH R AS
(
-- Anchor: root elements (parent is null)
SELECT
Roots.id,
Roots.[path],
Roots.children,
element =
(
SELECT Roots.*
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
)
FROM
(
SELECT
TT.id, TT.[text], TT.[path],
TT.depth, TT.leaf, TT.expanded,
-- List of child IDs to resolve during recursion
children =
(
SELECT STRING_AGG(TT2.id, N',')
FROM #tTree AS TT2 WITH (FORCESEEK)
WHERE TT2.parentId = TT.id
)
FROM #tTree AS TT
WHERE TT.parentId IS NULL
) AS Roots
UNION ALL
SELECT
Nodes.id,
Nodes.[path],
Nodes.children,
element =
(
-- Resolve children
REPLACE
(
Nodes.element,
-- Placeholder
CONCAT(N'"children":"', Nodes.resolving, N'"'),
-- Construct array of children
CONCAT
(
N'"children":',
(
SELECT
TT.id, TT.[text], TT.[path],
TT.depth, TT.leaf, TT.expanded,
Nodes.children
-- Split the ID list and join to child records
FROM STRING_SPLIT(Nodes.resolving, N',') AS SS
JOIN #tTree AS TT
WITH (FORCESEEK)
ON TT.id = CONVERT(integer, SS.[value])
FOR JSON PATH
)
)
)
)
FROM
(
SELECT
TT.*,
-- Children we are resolving now
resolving = R.children,
R.element,
-- New list of children to resolve later
children =
STUFF
(
(
-- Cannot use aggregates in the recurive part
SELECT CONCAT(N',', TT2.id)
FROM #tTree AS TT2
WITH (FORCESEEK)
WHERE TT2.parentId = TT.id
FOR XML PATH (''), TYPE
).value('(./text())[1]', 'nvarchar(4000)'),
1, 1, N''
),
-- Any node at the same parent level will do
-- Avoids duplicated work (see WHERE clause)
rn = ROW_NUMBER() OVER (ORDER BY TT.parentId)
FROM R
JOIN #tTree AS TT
ON TT.parentId = R.id
) AS Nodes
WHERE Nodes.rn = 1
)
-- Finished nodes are those with no children
-- Concatenate and wrap as a JSON array
SELECT
CONCAT
(
N'[',
STRING_AGG(R.element, N',')
WITHIN GROUP (ORDER BY R.[path]),
N']'
)
FROM R
WHERE R.children IS NULL
OPTION (MAXRECURSION 0);
输出:
[
{
"id": 1,
"text": "Category 1",
"path": "1",
"depth": 1,
"expanded": 1,
"children": [
{
"id": 2,
"text": "SubCategory 1",
"path": "1,2",
"depth": 2,
"expanded": 1,
"children": [
{
"id": 3,
"text": "Element 1",
"path": "1,2,3",
"depth": 3,
"leaf": 1
},
{
"id": 9,
"text": "SubSubCategory 1",
"path": "1,2,9",
"depth": 3,
"leaf": 1
}
]
}
]
},
{
"id": 10,
"text": "Category 3",
"path": "10",
"depth": 1,
"leaf": 1
},
{
"id": 4,
"text": "Category 2",
"path": "4",
"depth": 1,
"expanded": 1,
"children": [
{
"id": 5,
"text": "SubCategory 2",
"path": "4,5",
"depth": 2,
"leaf": 1,
"children": [
{
"id": 8,
"text": "SubElement 1",
"path": "4,5,8",
"depth": 3,
"leaf": 1
}
]
},
{
"id": 6,
"text": "SubCategory 3",
"path": "4,6",
"depth": 2,
"leaf": 1,
"children": [
{
"id": 8,
"text": "SubElement 1",
"path": "4,5,8",
"depth": 3,
"leaf": 1
}
]
},
{
"id": 7,
"text": "Element 2",
"path": "4,7",
"depth": 2,
"expanded": 1,
"children": [
{
"id": 8,
"text": "SubElement 1",
"path": "4,5,8",
"depth": 3,
"leaf": 1
}
]
}
]
}
]