Need to update multiple nodes in a XML string using values from an SQL query

有些话、适合烂在心里 提交于 2019-12-23 23:03:10

问题


I have a SQL stored procedure local variable @DocList (Declare @DocList XML) which contains the follwing XML data:

<JobList ListItems="7">
  <Job JobFriendlyName="EMAIL INVOICES">
    <DocumentList>
      <Document Doc="1" ID="5280301.2019050148902.00020" Date="05-03-2019" Status="NEW" />
      <Document Doc="2" ID="5280301.2019050148902.00022" Date="05-03-2019" Status="NEW" />
      <Document Doc="3" ID="5280301.2019050148902.00023" Date="05-03-2019" Status="NEW" />
      <Document Doc="4" ID="5280301.2019050104301.00055" Date="05-02-2019" Status="NEW" />
      <Document Doc="5" ID="5280301.2019050104301.00056" Date="05-02-2019" Status="NEW" />
    </DocumentList>
  </Job>
  <Job JobFriendlyName="INVOICES">
    <DocumentList>
      <Document Doc="6" ID="5280300.2019050148901.00001" Date="05-03-2019" Status="NEW" />
      <Document Doc="7" ID="5280300.2019050148901.00002" Date="05-03-2019" Status="NEW" />
    </DocumentList>
  </Job>
</JobList>

I also have an SQL table "DocAccess" which contains 0 or more rows with DocIDNumber the correlate to matching "ID" attribute values in the XML:

TABLE [tblDocAccess]
(
    [Key] varachar(10),
    [DocIDNumber] [varchar](35),
    [DocLastOpenDtg] [smalldatetime]
)

I want to apply query "select [DocIDNumber] from [tblDocAccess] where [Key] = {some arbitrary value}" against the XML in @DocList to modify attribute "Status" value from "NEW" to "OLD" for each node where attribute "ID" values matches a returned [DocIdNumber] values.

I know I could create a cursor fror the select statement, then loop to locate/update any matching node/attribute value, but that does note seem efficient.

Any assistance of suggestions would be appreciated.

===================================

Follow-up question: Using the XML document shown above in @DocList, and another local variable, @SearchID varchar(35) which contains a value to search for, how would I code the required {While ... Exists ... Set) logic to set Status to "OLD" for the Document with ID matching the value in @SearchID.

Please forgive my ignorance here. I have been working with SQL for many years, but this is my first attempt to update an existing XML document.

|improve this question

回答1:


The XML-method .modify() allows for one change at a time. This would mean to use one statement per needed change. A CURSOR or a WHILE loop might be a good idea.

As I try to avoid procedural logic, you can have a look at these alternatives:

Shred and re-create

One approach was to shred the whole thing and recreate it from scratch:

First I create a mockup to simulate your situation

DECLARE @DocList XML=
N'<JobList ListItems="7">
  <Job JobFriendlyName="EMAIL INVOICES">
    <DocumentList>
      <Document Doc="1" ID="5280301.2019050148902.00020" Date="05-03-2019" Status="NEW" />
      <Document Doc="2" ID="5280301.2019050148902.00022" Date="05-03-2019" Status="NEW" />
      <Document Doc="3" ID="5280301.2019050148902.00023" Date="05-03-2019" Status="NEW" />
      <Document Doc="4" ID="5280301.2019050104301.00055" Date="05-02-2019" Status="NEW" />
      <Document Doc="5" ID="5280301.2019050104301.00056" Date="05-02-2019" Status="NEW" />
    </DocumentList>
  </Job>
  <Job JobFriendlyName="INVOICES">
    <DocumentList>
      <Document Doc="6" ID="5280300.2019050148901.00001" Date="05-03-2019" Status="NEW" />
      <Document Doc="7" ID="5280300.2019050148901.00002" Date="05-03-2019" Status="NEW" />
    </DocumentList>
  </Job>
</JobList>';

DECLARE @mockupDocAccess TABLE
(
    [Key] varchar(10),
    [DocIDNumber] [varchar](35),
    [DocLastOpenDtg] [smalldatetime]
);

INSERT INTO @mockupDocAccess VALUES('SomeKey','5280301.2019050148902.00022',GETDATE())   --Doc 2
                                  ,('SomeKey','5280301.2019050104301.00055',GETDATE())   --Doc 4
                                  ,('SomeKey','5280300.2019050148901.00001',GETDATE())   --Doc 6
                                  ,('OtherKey','5280301.2019050104301.00056',GETDATE()); --Doc 5

--Now we can read all values from the XML and re-create the XML after using CASE to set the needed status values to OLD:

DECLARE @Key VARCHAR(10)='SomeKey';

WITH AllEmailInvoices AS
(
    SELECT d.value('@Doc','int') AS Doc
          ,d.value('@ID','nvarchar(35)') AS ID
          ,d.value('@Date','nvarchar(10)') AS [Date] --unconverted
          ,CASE WHEN EXISTS(SELECT 1 FROM @mockupDocAccess da WHERE da.DocIDNumber=d.value('@ID','nvarchar(35)') AND da.[Key]=@Key) THEN 'OLD' ELSE d.value('@Status','nvarchar(10)') END AS [Status]

    FROM @DocList.nodes('/JobList/Job[@JobFriendlyName="EMAIL INVOICES"]/DocumentList/Document') A(d)
)
,AllInvoices AS
(
    SELECT d.value('@Doc','int') AS Doc
          ,d.value('@ID','nvarchar(35)') AS ID
          ,d.value('@Date','nvarchar(10)') AS [Date] --unconverted
          ,CASE WHEN EXISTS(SELECT 1 FROM @mockupDocAccess da WHERE da.DocIDNumber=d.value('@ID','nvarchar(35)') AND da.[Key]=@Key) THEN 'OLD' ELSE d.value('@Status','nvarchar(10)') END AS [Status]

    FROM @DocList.nodes('/JobList/Job[@JobFriendlyName="INVOICES"]/DocumentList/Document') A(d)
)
SELECT @DocList.value('(/JobList/@ListItems)[1]','int') AS [@ListItems]
      ,(
            SELECT 'EMAIL INVOICES' AS [@JobFriendlyName]
                   ,(
                        SELECT Doc AS [@Doc]
                              ,ID AS [@ID]
                              ,[Date] AS [@Date]
                              ,[Status] AS [@Status]      
                        FROM AllEmailInvoices
                        FOR XML PATH('Document'),ROOT('DocumentList'),TYPE
                    )
            FOR XML PATH('Job'),TYPE
        )
        ,(
            SELECT 'INVOICES' AS [@JobFriendlyName]
                   ,(
                        SELECT Doc AS [@Doc]
                              ,ID AS [@ID]
                              ,[Date] AS [@Date]
                              ,[Status] AS [@Status]      
                        FROM AllInvoices
                        FOR XML PATH('Document'),ROOT('DocumentList'),TYPE
                    )
            FOR XML PATH('Job'),TYPE
        )
FOR XML PATH('JobList');

XQuery and FLWOR approach

Alternatively you can try something along this:

DECLARE @Key VARCHAR(10)='SomeKey';

SELECT
(
    SELECT (SELECT DocIDNumber AS ID FROM @mockupDocAccess WHERE [Key]=@Key FOR XML PATH(''),TYPE) DocAccess
          ,@DocList
    FOR XML PATH(''),TYPE
).query
(N'
    <JobList> {/JobList/@*}
    {
    for $j in /JobList/Job 
    return
        <Job> {$j/@*}
        {
            <DocumentList>
            {
            for $d in $j/DocumentList/Document
            return
                <Document Doc="{$d/@Doc}" 
                          ID="{$d/@ID}" 
                          Date="{$d/@Date}" 
                          Status="{if(/DocAccess[ID=$d/@ID]) then "OLD" else xs:string($d/@Status)}" />
            }
            </DocumentList>
        }
        </Job>
    }
    </JobList>
');

First we create a XML where we include the values from the DocAccess-table. This will look like this:

<DocAccess>
  <ID>5280301.2019050148902.00022</ID>
  <ID>5280301.2019050104301.00055</ID>
  <ID>5280300.2019050148901.00001</ID>
</DocAccess>
<JobList ListItems="7">
  <!-- Your Content here -->
</JobList>

The XQuery will rebuild the document but will set the Status-attribute depending on the existance of a corresponding ID element in <DocAccess>.

Final statement

You can use a

  • CURSOR with separate statemets per change,
  • you can shred and re-create the XML or
  • you can use XQuery/FLWOR to re-build the XML.

It depends on your needs, which approach you prefer.



来源:https://stackoverflow.com/questions/56568445/need-to-update-multiple-nodes-in-a-xml-string-using-values-from-an-sql-query

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!