为什么使用JOIN的SQL Server的xml.modify无法更新所有指定的元素?

时间:2019-11-19 21:22:21

标签: sql sql-server xml

我已经在Google上搜索并阅读了大量此类内容,但我仍然不明白为什么此代码不起作用。我知道.modify()是一个“单例”,但是它没有向我解释为什么它不对返回的集合中的每一行执行一次。这对我来说意义非零,我只是希望能有所了解。老实说,这感觉像是一个错误,而不是预期的行为。

基本上,我正在尝试根据我无法提前预测的输入表更新xml(每个客户端的情况都会有所不同)。我想通过仅加入描述更新的表并使其工作来进行更新。但事实并非如此。

我将其简化为该示例。在此示例中,我希望代码更新使用蔬菜=“ false”属性指定的每个节点。请注意,我使用“ sql:Column()”语法导航到要更新的正确节点。但这只会更新第一个:

SET NOCOUNT ON;
DECLARE @xml xml = '<top>
    <element name="apple">red</element>
    <element name="pear">green</element>
    <element name="banana">yellow</element>
</top>';

DECLARE @xmlData TABLE (ID int, Contents xml);
DECLARE @elements TABLE (ID int, Element varchar(50));

INSERT INTO @xmlData (ID, Contents) VALUES (1, @xml);
INSERT INTO @elements(ID, Element)
VALUES (1, 'apple'), (2, 'pear'), (3, 'banana');

SET NOCOUNT OFF;
SELECT *
  FROM @xmlData CROSS JOIN @elements;

UPDATE x
   SET Contents.modify('insert attribute vegetable {"false"} into (/top/element[@name=sql:column("y.Element")])[1]')
  FROM @xmlData x
       CROSS JOIN @elements y

SELECT * FROM @xmlData;

我将select放入其中以显示它返回三行,因此我希望进行三次更新。但是SET NOCOUNT OFF显示只有一行被更新。这没有意义。

解决此问题的唯一方法是,我发现它会放入WHILE循环,但这似乎很荒谬,因为在上面的代码中,意图已经完美且明确地说明了……这根本行不通。

此代码有效,但是我不知道为什么我必须经历这种骇客和性能杀手才能做一些显而易见的事情:

SET NOCOUNT ON;
DECLARE @xml xml = '<top>
    <element name="apple">red</element>
    <element name="pear">green</element>
    <element name="banana">yellow</element>
</top>';

DECLARE @xmlData TABLE (ID int, Contents xml);
DECLARE @elements TABLE (ID int, Element varchar(50));

INSERT INTO @xmlData (ID, Contents) VALUES (1, @xml);
INSERT INTO @elements(ID, Element)
VALUES (1, 'apple'), (2, 'pear'), (3, 'banana');

SET NOCOUNT OFF;
SELECT *
  FROM @xmlData CROSS JOIN @elements;

DECLARE @maxElement int = (SELECT MAX(ID) FROM @elements);
DECLARE @currentElement int = 1;

WHILE (@currentElement <= @maxElement)
BEGIN
    UPDATE x
       SET Contents.modify('insert attribute vegetable {"false"} into (/top/element[@name=sql:column("y.Element")])[1]')
      FROM @xmlData x
           CROSS JOIN @elements y
     WHERE y.ID = @currentElement
     SET @currentElement += 1;
END

SELECT * FROM @xmlData;

有人可以向我解释为什么第一个块中的每一行都没有执行修改吗?第二个模块中的解决方案是最好的解决方案吗?对我来说,这确实感觉像是个骇人听闻的事情……但是我不知道元素名称或提前知道元素名称的数量,因此我不能仅执行大多数文章建议的编写多个UPDATE语句的操作。我认为,使用JOIN进行更新应该做正确的事情。我为此感到困惑。

1 个答案:

答案 0 :(得分:1)

这是一种使用 XQuery FLWOR 表达式的方法。它通过XML重建来实现。另外,如果@vegetable属性已经存在,我会添加一些保护。 XQuery contains()函数检查元素是否在更新列表中。我添加了一个新的 fig 元素进行测试。

  

SQL

-- DDL and sample data population, start
DECLARE @xmlData TABLE (ID INT IDENTITY PRIMARY KEY, Contents XML);
INSERT INTO @xmlData (Contents) 
VALUES ('<top>
    <element name="apple" vegetable="true">red</element>
    <element name="pear">green</element>
    <element name="fig">reddish</element>
    <element name="banana">yellow</element>
</top>');

DECLARE @elements TABLE (ID INT IDENTITY PRIMARY KEY, Element VARCHAR(50));
INSERT INTO @elements(Element)
VALUES ('apple'), ('pear'), ('banana');
-- DDL and sample data population, end

-- before
SELECT * FROM @xmlData;

DECLARE @element_list AS NVARCHAR(MAX) = '';

SELECT @element_list += ',[' + el.Element + ']'
FROM @elements AS el;

-- just to see
SELECT @element_list AS [element_list];

DECLARE @vegetable VARCHAR(10) = 'false';

UPDATE @xmlData
SET Contents = Contents.query('<top>
    {
        for $x in /top/element
        return if(empty($x/@vegetable)
                    and contains(sql:variable("@element_list"), concat("[",$x/@name,"]"))) then (
            <element name="{($x/@name)}">{data($x)}
                {attribute vegetable {sql:variable("@vegetable")}}
            </element>)
            else ($x)
    }
</top>');

-- after
SELECT * FROM @xmlData;

不幸的是,即使最新的SQL Server 2019仍不支持基于w3c标准的XQuery 3.0或3.1。

有用的链接:

(1)XQuery Update (2)SQL Server vNext (post 2019) and NoSQL functionality