用Transact-SQL替换所有记录中的多个XML属性值?

时间:2017-04-21 15:20:28

标签: sql-server xml xpath xquery

问题

我的MSSQL数据库有一个表records,其中包含XML列data,如下所示:

<record id="1">
  <field tag="DI" occ="1" lang="de-DE">Höhe</field>
  <field tag="DI" occ="1" lang="en-GB">height</field>
  <field tag="WA">173</field>
  <field tag="EE">cm</field>
  <field tag="DI" occ="2" lang="de-DE">Breite</field>
  <field tag="DI" occ="2" lang="en-GB">width</field>
  <field tag="WA">55</field>
  <field tag="EE">cm</field>
</record>

我希望一次更新表格中的所有行,将/record/field/@lang替换为en-US此时它是en-GB的所有元素属性值)。

已尝试类似......

declare @i int;
declare @xml xml;
set @xml = (select top(1) [data] from [my-database].[dbo].[records]);
select @i = @xml.value('count(/record/field[lang="en-GB"])', 'int')

while @i > 0
begin
    set @xml.modify('
            replace value of
                (/record/field[lang="en-GB"]/text())[1]
            with "en-US"
    ')

    set @i = @i - 1
end

select @xml;

...但它返回数据不变,只有在选择了一行时才有效。如何使这项工作一次更新所有行?

解决方案

我最终使用了Shnugo建议的XQuery。我稍微概括的查询如下所示:

UPDATE [my-database].[dbo].[records] SET data = data.query(N'
    <record>
    {
        for $attr in /record/@*
        return $attr
    }
    {
        for $fld in /record/*
        return
        if (local-name($fld) = "field")
        then <field>
        {
            for $attr in $fld/@*
            return
            if (local-name($attr) = "lang" and $attr = "en-GB")
            then attribute lang {"en-US"}
            else $attr
        }
        {$fld/node()}
        </field>
        else $fld
    }
    </record>
')
FROM [my-database].[dbo].[records]
WHERE [data].exist('/record/field[@lang="en-GB"]') = 1;
SELECT * FROM [my-database].[dbo].[records]

最顶层节点<record>的名称似乎需要进行硬编码,因为MSSQL服务器不支持动态元素名称(也不支持属性名称)。它的属性以及除<field>以外的所有子元素都会自动复制上面的代码。

4 个答案:

答案 0 :(得分:1)

是的,遗憾的是,replace value of语句一次只更新一个节点。因此,在您的情况下,快速和脏的替换将是最容易编写(并且运气好,甚至可能是最快的运行):

update t set [data] = cast(
  replace(cast(t.[data] as nvarchar(max)), N' lang="en-GB"', N' lang="en-US"')
as xml)
from dbo.Records t
where t.[data].exist('/record/field[@lang="en-GB"]') = 1;

如果XML架构有所不同,例如无法保证/record节点始终位于顶层,您可能希望修改过滤器:

where t.[data].exist('//record/field[@lang="en-GB"]') = 1;

另一种方法是使用FLWOR语句,但如果XML结构变化很大并且包含其他不可预测的节点,则不会意外丢失任何内容变得相当困难。这反过来会导致较差的表现。为了使这种方法可行,您的XML模式必须非常稳定。

答案 1 :(得分:1)

由于副作用,我避免将强制转换为字符串类型(但这可能是最简单的方法,尤其是如果XML可能包含其他节点,您未在示例中显示这些节点...)< / p>

我也避免循环。

我的方法是切碎并重新创建XML:

DECLARE @xml XML=
N'<record id="1">
  <field tag="DI" occ="1" lang="de-DE">Höhe</field>
  <field tag="DI" occ="1" lang="en-GB">height</field>
  <field tag="WA">173</field>
  <field tag="EE">cm</field>
  <field tag="DI" occ="2" lang="de-DE">Breite</field>
  <field tag="DI" occ="2" lang="en-GB">width</field>
  <field tag="WA">55</field>
  <field tag="EE">cm</field>
</record>';

- 查询将读取所有字段的值,并使用更改的语言重建XML

WITH Shredded AS
(
    SELECT fld.value(N'@tag',N'nvarchar(max)') AS tag
          ,fld.value(N'@occ',N'int') AS occ
          ,fld.value(N'@lang',N'nvarchar(max)') AS lang
          ,fld.value(N'(./text())[1]',N'nvarchar(max)') AS content
    FROM @xml.nodes(N'/record/field') AS A(fld)
)
SELECT @xml.value(N'(/record/@id)[1]',N'int') AS [@id]
     ,(
        SELECT   tag AS [@tag]
                ,occ AS [@occ]
                ,CASE WHEN lang='en-GB' THEN 'en_US' ELSE lang END AS [@lang]
                ,content AS [*]
        FROM Shredded
        FOR XML PATH('field'),TYPE
      ) AS [*]
FOR XML PATH(N'record')

结果

<record id="1">
  <field tag="DI" occ="1" lang="de-DE">Höhe</field>
  <field tag="DI" occ="1" lang="en_US">height</field>
  <field tag="WA">173</field>
  <field tag="EE">cm</field>
  <field tag="DI" occ="2" lang="de-DE">Breite</field>
  <field tag="DI" occ="2" lang="en_US">width</field>
  <field tag="WA">55</field>
  <field tag="EE">cm</field>
</record>

答案 2 :(得分:1)

我将此作为第二个答案添加,因为它遵循完全不同的方法。以下代码将使用.query()FLWOR查询来按原样读取XML ,但在内容为{{1}时更改属性lang }:

en_GB

查询

DECLARE @xml XML=
N'<record id="1">
  <field tag="DI" occ="1" lang="de-DE">Höhe</field>
  <field tag="DI" occ="1" lang="en-GB">height</field>
  <field tag="WA">173</field>
  <field tag="EE">cm</field>
  <field tag="DI" occ="2" lang="de-DE">Breite</field>
  <field tag="DI" occ="2" lang="en-GB">width</field>
  <field tag="WA">55</field>
  <field tag="EE">cm</field>
</record>';

结果

SELECT @xml.query
(N'
    <record id="{/record/@id}">
    {
        for $fld in /record/field
        return <field>
        {
            for $attr in $fld/@*
            return
            if(local-name($attr)="lang" and $attr="en-GB") then attribute lang {"en-US"}
            else $attr
        }
        {$fld/text()}
        </field>
    }
    </record>
')

更新:这也适用于所有表格行:

尝试此操作立即更新完整的表格:

<record id="1">
  <field tag="DI" occ="1" lang="de-DE">Höhe</field>
  <field tag="DI" occ="1" lang="en-US">height</field>
  <field tag="WA">173</field>
  <field tag="EE">cm</field>
  <field tag="DI" occ="2" lang="de-DE">Breite</field>
  <field tag="DI" occ="2" lang="en-US">width</field>
  <field tag="WA">55</field>
  <field tag="EE">cm</field>
</record>

答案 3 :(得分:0)

没有xquery, xpath的丑陋解决方案......:

  DECLARE @xml XML = N'<record id="1">
  <field tag="DI" occ="1" lang="de-DE">Höhe</field>
  <field tag="DI" occ="1" lang="en-GB">height</field>
  <field tag="WA">173</field>
  <field tag="EE">cm</field>
  <field tag="DI" occ="2" lang="de-DE">Breite</field>
  <field tag="DI" occ="2" lang="en-GB">width</field>
  <field tag="WA">55</field>
  <field tag="EE">cm</field>
</record>'

SET @xml = REPLACE(CAST(@xml AS nvarchar(max)), '"en-GB"', '"en-US"')

SELECT @xml

并使用modify()

DECLARE @nodeCount int
DECLARE @i int

SET @i = 1

SELECT @nodeCount = @xml.value('count(/record/field/@lang)','int') 

WHILE (@i <= @nodeCount)
BEGIN
    Set @xml.modify('replace value of (/record/field/@lang)[.="en-GB"][1] with "en-US"')
    SET @i = @i + 1
END

SELECT @xml

演示链接:Rextester