如何以一般方式创建层级xml

时间:2018-08-09 11:34:16

标签: sql-server sqlxml

我需要能够从SQL输出分层XML,但我不想为每个所需的架构创建类似的功能。

例如,给定的数据如下:

+----+----------+----------------------------------------------------------+
| ID | ParentID |                           XML                            |
+----+----------+----------------------------------------------------------+
|  1 | NULL     | <root/>                                                  |
|  2 | 1        | <A someattr="whatever">some text</A>                     |
|  3 | 1        | <B someattr="stuff">more text <and><elements/></and></B> |
|  4 | 3        | <B1>Child of B</B1>                                      |
|  5 | 4        | <B1A/>                                                   |
|  6 | 2        | <Child_Of-A/>                                            |
+----+----------+----------------------------------------------------------+

我们应该有一个如下所示的输出:

<root>
  <A someattr="whatever">some text<Child_Of-A /></A>
  <B someattr="stuff">more text<and><elements /></and>
    <B1>Child of B<B1A />
    </B1>
  </B>
</root>

(或在语义上相同)

互联网上有很多文章解释了如何非动态地执行此操作,但是我找不到任何通用的解决方案。

如何在不为每个模式创建新功能(或一组新功能)的情况下完成此操作?

1 个答案:

答案 0 :(得分:0)

事实证明,尽管N.B.对于大型数据集,这可能相当慢(我的实际数据集只有191个节点,但仍需要大约6秒钟)

对于大型数据集,可能需要一种非动态方法。

首先,我们创建一个表类型来形式化所需的输入结构。

CREATE TYPE dbo.HierachicalXMLInput AS TABLE 
(
  [ID] int,
  [ParentID] int,
  [XML] xml
)

第一个函数仅创建层次

CREATE FUNCTION dbo.fnHierachicalXMLHierachy
(   @Data dbo.HierachicalXMLInput READONLY,
    @ParentID int = NULL
)   RETURNS XML AS
BEGIN
    DECLARE @Out xml
    ;
    WITH XMLNAMESPACES ('ChangeMetoSomeUniqueNamepsace' as h)
    SELECT @Out = ( 
        SELECT [ID] AS [@ID],
               dbo.fnHierachicalXMLHierachy(@Data, [ID]) AS [*]
        FROM   @Data
        WHERE  ([ParentID]=@ParentID) Or ([ParentID] IS NULL AND @ParentID IS NULL)
        FOR XML PATH ('h:N')
    )
    RETURN @Out
END

这将输出类似的内容(为清楚起见,省略了冗余的名称空间声明)

<h:N xmlns:h="ChangeMetoSomeUniqueNamepsace" ID="1">
  <h:N ID="2">
    <h:N ID="6" />
  </h:N>
  <h:N ID="3">
    <h:N ID="4">
      <h:N  ID="5" />
    </h:N>
  </h:N>
</h:N>

然后,我们可以将其与原始数据结合起来以获取所需的输出:

CREATE FUNCTION dbo.fnHierachicalXML
(   @Data dbo.HierachicalXMLInput READONLY
)   RETURNS XML AS
BEGIN
  DECLARE @XML xml = dbo.fnHierachicalXMLHierachy(@Data, NULL)
  DECLARE @NodeID int 
  -- we need to replace the placeholder nodes one by one.
  -- we should only replace nodes that do not have placeholder children
  -- i.e. we start deepest first
  -- find the first node to replace and get it's ID
  ; WITH XMLNAMESPACES ('ChangeMetoSomeUniqueNamepsace' as h)
  SELECT @NodeID= @XML.value('(//h:N[not(h:N)])[1]/@ID', 'int')
  DECLARE @Original xml

  WHILE @NodeID IS NOT NULL -- there is something to replace
  BEGIN
    --get the actual XML that is supposed to go where the placeholder currently is
    SELECT @Original=[XML]FROM  @Data WHERE ID=@NodeID
    -- insert the desired node after the placeholder
    SET @XML.modify('declare namespace h="ChangeMetoSomeUniqueNamepsace";  
      insert sql:variable("@Original")  after (//h:N[@ID=sql:variable("@NodeID") and not(h:N)])[1]')    
    -- if this isn't the first time around then the placeholder's placeholder children have already been replace with actual content.
    -- so insert the placeholder's content into the real node
    SET @XML.modify('
      declare namespace h="ChangeMetoSomeUniqueNamepsace";  
      insert (//h:N[@ID=sql:variable("@NodeID") and not(h:N)])[1]/*  
      as last into 
     ((//h:N[@ID=sql:variable("@NodeID") and not(h:N)])[1]/../*[. >>
       (//h:N[@ID=sql:variable("@NodeID") and not(h:N)])[1]
     ])[1]
    ')  
    -- in the above: x/../*[. >> x] is basically the same (as x::following-sibling:*)
    -- sadly it seems SQL's XML DML doesn't support axes like following-sibling, though it's not clear why this is so.
    -- Instead we use the more convoluted syntax

    -- finally, delete the placeholder
    SET @XML.modify('
      declare namespace h="ChangeMetoSomeUniqueNamepsace";  
      delete (//h:N[@ID=sql:variable("@NodeID") and not(h:N)])[1]
    ')
    -- and find the next placeholder to replace
    ;WITH XMLNAMESPACES ('ChangeMetoSomeUniqueNamepsace' as h)
    SELECT @NodeID = @XML.value('(//h:N[not(h:N)])[1]/@ID', 'int')
  END
RETURN @XML
END

然后我们可以像这样使用它:

DECLARE @dt AS dbo.HierachicalXMLInput
INSERT INTO @dt SELECT 1, NULL, '<root></root>'
INSERT INTO @dt SELECT 2, 1, '<A someattr="whatever">some text</A>'
INSERT INTO @dt SELECT 3, 1, '<B someattr="stuff">more text <and><elements/></and></B>'
INSERT INTO @dt SELECT 4, 3, '<B1>Child of B</B1>'
INSERT INTO @dt SELECT 5, 4, '<B1A/>'
INSERT INTO @dt SELECT 6, 2, '<Child_Of-A/>'

-- SELECT * FROM @dt

DECLARE @XML xml = dbo.fnHierachicalXML (@dt)
SELECT @XML

并获得所需的输出:

<root>
  <A someattr="whatever">some text<Child_Of-A /></A>
  <B someattr="stuff">more text <and><elements /></and><B1>Child of B<B1A /></B1></B>
</root>