使用 XQuery 仅查找和替换 xml 值的一部分?

Posted

技术标签:

【中文标题】使用 XQuery 仅查找和替换 xml 值的一部分?【英文标题】:Find and replace just a part of a xml value using XQuery? 【发布时间】:2017-01-01 10:39:58 【问题描述】:

我的一个专栏中有一个 XML,看起来像这样:

<BenutzerEinstellungen>
       <State>Original</State>
       <VorlagenHistorie>/path/path3/test123/file.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path21/anothertest/second.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path15/test123/file.doc</VorlagenHistorie>
</BenutzerEinstellungen>

我想用another test 替换VorlagenHistorie 中的所有 test123 出现(可能不止一个),在我更新后所有路径都指向test123。

我知道,如何用相等运算符检查和替换所有值,我在这个答案中看到了它: Dynamically replacing the value of a node in XML DML

但是是否有 CONTAINS 运算符,是否可以替换值的 INSIDE,我的意思是只替换值的一部分?

提前致谢!

【问题讨论】:

是否总是有一个State 元素和一个VorlagenHistorie 元素列表而没有别的? State 对象只是一个示例,列表中还有很多其他元素。 【参考方案1】:

我通常不会建议基于字符串的方法。但在这种情况下,做这样的事情可能最容易

declare @xml XML=
'<BenutzerEinstellungen>
       <State>Original</State>
       <VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';

SELECT CAST(REPLACE(CAST(@xml AS nvarchar(MAX)),'/test123/','/anothertest/') AS xml);

更新

如果这种方法是全局,您可以尝试这样的方法:

我将 XML 作为派生表读取并将其作为 XML 写回。在这种情况下,您可以确定,只有带有VorlageHistorie 的节点会被触及...

SELECT @xml.value('(/BenutzerEinstellungen/State)[1]','nvarchar(max)') AS [State]
      ,(
        SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
        FROM @xml.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
        FOR XML PATH('VorlagenHistorie'),TYPE
       )
FOR XML PATH('BenutzerEinstellungen');

更新 2

试试这个。它将读取所有未被称为VorlagenHistorie 原样的节点,然后将添加具有替换值的VorlageHistorie 节点。唯一的缺点可能是,如果在 VorlagenHistorie 元素之后还有其他节点,则文件的顺序会有所不同。但这不应该真正触及您的 XML 的有效性...

declare @xml XML=
'<BenutzerEinstellungen>
       <State>Original</State>
       <Unknown>Original</Unknown>
       <UnknownComplex>
       <A>Test</A>
       </UnknownComplex>
       <VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>';

SELECT @xml.query('/BenutzerEinstellungen/*[local-name(.)!="VorlagenHistorie"]') AS [node()]
      ,(
        SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
        FROM @xml.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
        FOR XML PATH('VorlagenHistorie'),TYPE
       )
FOR XML PATH('BenutzerEinstellungen');

更新 3

使用可更新的 CTE 首先获取值,然后一次性设置它们:

declare @tbl TABLE(ID INT IDENTITY,xmlColumn XML);
INSERT INTO @tbl VALUES
(
'<BenutzerEinstellungen>
       <State>Original</State>
       <Unknown>Original</Unknown>
       <UnknownComplex>
       <A>Test</A>
       </UnknownComplex>
       <VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
</BenutzerEinstellungen>')
,('<BenutzerEinstellungen>
       <State>Original</State>
       <VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
    </BenutzerEinstellungen>');

WITH NewData AS
(
    SELECT ID
      ,xmlColumn AS OldData
      ,(
        SELECT t.xmlColumn.query('/BenutzerEinstellungen/*[local-name(.)!="VorlagenHistorie"]') AS [node()]
              ,(
                SELECT REPLACE(vh.value('.','nvarchar(max)'),'/test123/','/anothertest/') AS [*]
                FROM t.xmlColumn.nodes('/BenutzerEinstellungen/VorlagenHistorie') AS A(vh)
                FOR XML PATH('VorlagenHistorie'),TYPE
               )
        FOR XML PATH('BenutzerEinstellungen'),TYPE
       ) AS NewXML
    FROM @tbl AS t
)
UPDATE NewData
SET OldData=NewXml;

SELECT * FROM @tbl;

【讨论】:

我们还有其他元素,里面可能有相同的字符串,但不应该更新。这不太可能,但会有风险。 @Jannik 查看我的更新...如果有更多属性,您可以像我使用 State 所做的那样包含它们 好吧,我们并不了解所有这些,不幸的是它们几乎是动态的。 谢谢,这看起来很有希望。你能告诉我,你是如何在真实的桌子上执行的吗? 我犯了一个小错误。它可以与这样的东西一起使用吗? OriginalOriginalTest /path/path/test123/file.doc /path/path/test123/file123.doc/path/path/anothertest/second.doc innerElement> 【参考方案2】:

一个奇怪的解决方案,但效果很好:

DECLARE @xml XML = '
<BenutzerEinstellungen>
       <State>Original</State>
       <VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path5/test123/third.doc</VorlagenHistorie>
</BenutzerEinstellungen>';

DECLARE @Counter int = 1,
        @newValue nvarchar(max),
        @old nvarchar(max) = N'test123',
        @new nvarchar(max) = N'anothertest';

WHILE @Counter <= @xml.value('fn:count(//*//*)','int')
BEGIN
    SET @newValue = REPLACE(CONVERT(nvarchar(100), @xml.query('((/*/*)[position()=sql:variable("@Counter")]/text())[1]')), @old, @new)
    SET @xml.modify('replace value of ((/*/*)[position()=sql:variable("@Counter")]/text())[1] with sql:variable("@newValue")');
    SET @Counter = @Counter + 1;
END

SELECT  @xml; 

输出:

<BenutzerEinstellungen>
  <State>Original</State>
  <VorlagenHistorie>/path/path/anothertest/file.doc</VorlagenHistorie>
  <VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
  <VorlagenHistorie>/path/path5/anothertest/third.doc</VorlagenHistorie>
</BenutzerEinstellungen>

【讨论】:

有趣,这是我在 Pawel 的回答下面建议的方法 :-) 您好 gofr1,我用你的答案再放了一个答案,因为我认为你的方法可以通过使用更具体的 XPath 得到广泛的增强。我投票赞成,因为我偷了你的代码,但是你偷了 Pawel 的代码,所以我希望这对你没问题:-) 嗨@Shnugo,这肯定没问题:) 老实说,除了Pawel 的回答,我没有阅读最近的cmets,我只是按原样发布了这个解决方案。我认为这不是当前 OP 情况的好方法,但它可以帮助其他开发人员。 您好 gofr1,您没有遗漏任何东西!您的回答在我发表评论的同一秒内出现。我们只是有同样的想法...... @Shnugo 它只是发生了,我猜 :)【参考方案3】:

如果@shnugo 的回答不符合您的需求,您可以使用 XML/XQuery 方法:

DECLARE @xml xml = '<BenutzerEinstellungen>
       <State>Original</State>
       <VorlagenHistorie>/path/path/test123/file.doc</VorlagenHistorie>
       <VorlagenHistorie>/path/path/anothertest/second.doc</VorlagenHistorie>
    </BenutzerEinstellungen>';
DECLARE @from nvarchar(20) = N'test123';
DECLARE @to nvarchar(20) = N'another test';
DECLARE @newValue nvarchar(100) = REPLACE(CONVERT(nvarchar(100), @xml.query('(/BenutzerEinstellungen/VorlagenHistorie/text()[contains(.,sql:variable("@from"))])[1]')), @from, @to)

SET @xml.modify('
    replace value of (/BenutzerEinstellungen/VorlagenHistorie/text()[contains(.,sql:variable("@from"))])[1]
    with sql:variable("@newValue")')

SELECT @xml

【讨论】:

我会尝试这种方法并给你一些反馈:) 如果还有一个像&lt;VorlagenHistorie&gt;/path/path/test123/third.doc&lt;/VorlagenHistorie&gt;这样的节点呢?还是VorlagenHistorienodes内容会切换到那里? 可能不止一个。这不会与多个一起使用,对吧? Pawel,您正在使用第一个 VorlagenHistorie 来构建您的 @newValue。但不止一个。您可以计算VorlagenHistorie 节点的数量并使用循环将节点的索引注入为sql:variable...【参考方案4】:

gofr1 的答案可能会通过使用更具体的 XPath 表达式得到增强:

DECLARE @Counter int = 1,
        @newValue nvarchar(max),
        @old nvarchar(max) = N'test123',
        @new nvarchar(max) = N'anothertest';

WHILE @Counter <= @xml.value('fn:count(/BenutzerEinstellungen/VorlagenHistorie)','int')
BEGIN
    SET @newValue = REPLACE(CONVERT(nvarchar(100), @xml.value('(/BenutzerEinstellungen/VorlagenHistorie)[sql:variable("@Counter")][1]','nvarchar(max)')), @old, @new)
    SET @xml.modify('replace value of (/BenutzerEinstellungen/VorlagenHistorie[sql:variable("@Counter")]/text())[1] with sql:variable("@newValue")');
    SET @Counter = @Counter + 1;
END

SELECT  @xml; 

【讨论】:

以上是关于使用 XQuery 仅查找和替换 xml 值的一部分?的主要内容,如果未能解决你的问题,请参考以下文章

如何将XQuery表达式标记为确定性? (为了保留来自XML值的计算列)

text 查找HTML字符实体并将引用转换为XQuery中与XML兼容的字符

eXist DB & XQuery:带有属性的 xml-root 导致没有结果

如何仅替换找到的文本的一部分?

XQuery 更新:插入或替换取决于节点是不是存在不可能?

Powershell 中非常大的 XML 文件