如何使用T-SQL XML Modify('replace value of ...)'搜索/替换空白属性

时间:2018-11-27 18:18:32

标签: sql-server xml string replace

这是一个两部分的问题。我有一个带有XML列的表,其中包含零件的组件和属性数据。需要在所有记录的所有组件和属性中更新属性之一。 我从下面的Technet文章中找到了使用技术的部分解决方案: https://social.technet.microsoft.com/wiki/contents/articles/28601.t-sql-tips-search-and-replace-string-from-multiple-nodes-within-a-xml-document.aspx 不足之处:

  1. 在PrintCode为空字符串(PrintCode =“”)的情况下,该方法将失败,保留原始的空字符串而不是将其替换为值“ I”。这是因为,当$ arg2是零长度的字符串时,contains()函数始终返回TRUE。
  2. WHILE EXISTS循环仅将一个“旧”值重新映射为一个“新”值。我希望能够利用映射表并一次更新所有属性。 我尝试了其他几种方法,但没有提出单遍解决方案。

这是数据的简化形式。实际上,还有许多条目需要重新映射。

我确实使用了蛮力版本来重新映射除零长度字符串以外的所有值,但如果可能的话,我还是宁愿在一个语句中执行所有更新。

示例代码:

    DECLARE @Map TABLE ([Old] VARCHAR(1) NOT NULL, [New] VARCHAR(1) NOT NULL)
insert @Map ([Old], [New])
Values   ('D', 'E'),('3', 'I'),('', 'I');

DECLARE  @ConfigMaster TABLE (
    [ConfigCode] [nvarchar](15) NOT NULL,
    [ConfigMaster] [xml] NOT NULL
    )
INSERT @configMaster (ConfigCode, [ConfigMaster])
values('TestPart01', '<ConfigMaster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Components>
    <Component Name="FRSTK">
      <Attributes>
        <Attribute Name="O-FLGOPT" Value="N" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="22" />
        <Attribute Name="O-GRDLGT" Value="0" OptionListId="" PrintCode="" DataEntryOrder="0" />
        <Attribute Name="O-LEADLG" Value="72" OptionListId="" PrintCode="D" DataEntryOrder="4" />
        <Attribute Name="O-LEADTP" Value="R" OptionListId="1F3" PrintCode="D" DataEntryOrder="3" />
        <Attribute Name="O-LOCOPT" Value="N" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="24" />
        <Attribute Name="O-CODE" Value="N18A13" OptionListId="1F1" PrintCode="D" DataEntryOrder="1" />
        <Attribute Name="O-CONOPT" Value="N" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="25" />
        <Attribute Name="O-FITOPT" Value="Y" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="23" />
        <Attribute Name="O-FITTIN" Value="BG" OptionListId="1F5" PrintCode="D" DataEntryOrder="32" />
        <Attribute Name="O-FLANGE" Value="" OptionListId="1F2" PrintCode="" DataEntryOrder="0" />
      </Attributes>
    </Component>
  </Components>
</ConfigMaster>')
select * from @ConfigMaster; --before

WHILE EXISTS
(
SELECT  *
        FROM    @ConfigMaster
        WHERE   ConfigMaster.exist('//Component/Attributes/Attribute[ contains(@PrintCode, "D")]') = 1
)
BEGIN
         UPDATE [m]
         SET ConfigMaster.modify('replace value of 
                                    (//Component/Attributes/Attribute[ contains(@PrintCode , "D")]/@PrintCode)[1] 
                                  with sql:column("New") ')
         FROM
         (
              SELECT  ConfigMaster,
                        t.u.value('./@PrintCode[1]','varchar(1)') AS OrigValue
              FROM    @ConfigMaster as [p]
              CROSS APPLY p.ConfigMaster.nodes('/ConfigMaster/Components/Component/Attributes/Attribute[ contains(@PrintCode , "D")]') as t(u)
         ) as [m]
         Inner join @Map as s on s.Old = m.OrigValue

END
select * from @ConfigMaster --after pass 1: "D" updated to "E"
WHILE EXISTS
(
SELECT  *
        FROM    @ConfigMaster
        WHERE   ConfigMaster.exist('//Component/Attributes/Attribute[ contains(@PrintCode, "3")]') = 1
)
BEGIN
         UPDATE [m]
         SET ConfigMaster.modify('replace value of (//Component/Attributes/Attribute[ contains(@PrintCode , "3")]/@PrintCode)[1] with sql:column("New") ')
         FROM
         (
              SELECT  ConfigMaster,
                        t.u.value('./@PrintCode[1]','varchar(1)') AS OrigValue
              FROM    @ConfigMaster as [p]
              CROSS APPLY p.ConfigMaster.nodes('/ConfigMaster/Components/Component/Attributes/Attribute[ contains(@PrintCode , "3")]') as t(u)
         ) as [m]
         Inner join @Map as s on s.Old = m.OrigValue

END
select * from @ConfigMaster --after pass 2: "3" updated to "I"
WHILE EXISTS
(
SELECT  *
        FROM    @ConfigMaster
        WHERE   ConfigMaster.exist('//Component/Attributes/Attribute[contains(@PrintCode , "")]') = 1 --the empty string "" has special meaning to the contains() function
)
BEGIN
         UPDATE [m]
         SET ConfigMaster.modify('replace value of (//Component/Attributes/Attribute[ contains(@PrintCode , "")]/@PrintCode)[1] with sql:column("New") ')
         FROM
         (
              SELECT  ConfigMaster,
                        t.u.value('./@PrintCode[1]','varchar(1)') AS OrigValue
              FROM    @ConfigMaster as [p]
              CROSS APPLY p.ConfigMaster.nodes('/ConfigMaster/Components/Component/Attributes/Attribute[ contains(@PrintCode , "")]') as t(u)
         ) as [m]
         Inner join @Map as s on s.Old = isnull(m.OrigValue,'')
      break --this will not exit otherwise...                 
END
select * from @ConfigMaster --after pass 3: no change

谢谢!

克莱顿

1 个答案:

答案 0 :(得分:1)

我讨厌SQL Server中的循环...有时我们无法避免它,但是在大多数情况下,还有其他方法,并且在大多数情况下,这些基于集合的方法更好:

您似乎不需要声明的名称空间,所以我让他们放弃...

我减少了一点,所以它可能无法完全满足您的需求。让我知道,如果有什么遗漏:

DECLARE  @ConfigMaster TABLE (
    [ConfigCode] [nvarchar](15) NOT NULL,
    [ConfigMaster] [xml] NOT NULL
    )
INSERT @configMaster (ConfigCode, [ConfigMaster])
values('TestPart01', '<ConfigMaster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Components>
    <Component Name="FRSTK">
      <Attributes>
        <Attribute Name="O-FLGOPT" Value="N" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="22" />
        <Attribute Name="O-GRDLGT" Value="0" OptionListId="" PrintCode="" DataEntryOrder="0" />
        <Attribute Name="O-LEADLG" Value="72" OptionListId="" PrintCode="D" DataEntryOrder="4" />
        <Attribute Name="O-LEADTP" Value="R" OptionListId="1F3" PrintCode="D" DataEntryOrder="3" />
        <Attribute Name="O-LOCOPT" Value="N" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="24" />
        <Attribute Name="O-CODE" Value="N18A13" OptionListId="1F1" PrintCode="D" DataEntryOrder="1" />
        <Attribute Name="O-CONOPT" Value="N" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="25" />
        <Attribute Name="O-FITOPT" Value="Y" OptionListId="YES_NO" PrintCode="3" DataEntryOrder="23" />
        <Attribute Name="O-FITTIN" Value="BG" OptionListId="1F5" PrintCode="D" DataEntryOrder="32" />
        <Attribute Name="O-FLANGE" Value="" OptionListId="1F2" PrintCode="" DataEntryOrder="0" />
      </Attributes>
    </Component>
  </Components>
</ConfigMaster>')
select * from @ConfigMaster; --before

-蛮力强行,但简单俗气的一句话 ...

UPDATE @ConfigMaster SET ConfigMaster=
REPLACE(REPLACE(REPLACE(CAST(ConfigMaster AS NVARCHAR(MAX)),' PrintCode="D"',' PrintCode="E"'),' PrintCode="3"',' PrintCode="I"'),' PrintCode=""',' PrintCode="I"');

-XQuery,如果对您来说这些限制还可以(假设可能有多个<Component>):

UPDATE @ConfigMaster SET ConfigMaster=
ConfigMaster.query
(N' 
  <ConfigMaster>
    <Components>
    {
        for $comp in //ConfigMaster/Components/Component
        return
        <Component>{$comp/@Name}
        <Attributes>
        {
            for $node in $comp/Attributes/Attribute
            return
            <Attribute>
            {
                for $attr in $node/@*
                return
                if(local-name($attr)!="PrintCode") then 
                    $attr
                else
                    attribute PrintCode {if($attr="D") then "E" else "I"}
            }
            </Attribute>
        }
        </Attributes>
        </Component>
    }
    </Components>
    </ConfigMaster>
'); 

-最后是分解/重新组合(假设可能有多个组成部分)

WITH comp AS
(
    SELECT comp.value(N'@Name',N'nvarchar(max)') AS ComponentName
          ,comp.query(N'*') AS Children
    FROM @ConfigMaster 
    CROSS APPLY ConfigMaster.nodes(N'/ConfigMaster/Components/Component') A(comp)
)
SELECT ComponentName AS [ComponentName/@Name]
      ,(
        SELECT attr.value(N'@Name',N'nvarchar(max)') AS [@Name]
              ,attr.value(N'@Value',N'nvarchar(max)') AS [@Value]
              ,attr.value(N'@OptionListId',N'nvarchar(max)') AS [@OptionListId]
              ,CASE attr.value(N'@PrintCode',N'nvarchar(max)') WHEN 'D' THEN 'E' ELSE 'I' END AS [@PrintCode]
              ,attr.value(N'@DataEntryOrder',N'nvarchar(max)') AS [@DataEntryOrde]
        FROM Children.nodes(N'Attributes/Attribute') A(attr)
        FOR XML PATH('Attribute'),ROOT('Attributes'),TYPE     
       )
FROM comp
FOR XML PATH('Components'),ROOT('ConfigMaster')