事实证明,我们可以分步进行,尽管 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>