在 SQL Server 中创建带有子级的 JSON(Web 树的结构)

问题描述 投票:0回答:4

我的 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
json sql-server t-sql tree common-table-expression
4个回答
1
投票

不幸的是,在递归 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);

db<>小提琴


1
投票

使用工作表和嵌套迭代的替代方案:

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;

db<>小提琴


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
      }
    ]
  }
]

db<>小提琴


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
          }
        ]
      }
    ]
  }
]

db<>小提琴

© www.soinside.com 2019 - 2024. All rights reserved.