แก้ไข XML: คุณสมบัติเป็นองค์ประกอบ


11

ฉันมีXMLคอลัมน์ที่มีข้อมูลที่มีโครงสร้างคล้ายกัน:

<Root>
    <Elements>
        <Element Code="1" Value="aaa"></Element>
        <Element Code="2" Value="bbb"></Element>
        <Element Code="3" Value="ccc"></Element>
    </Elements>
</Root>

ฉันจะแก้ไขข้อมูลโดยใช้ SQL Server เพื่อเปลี่ยนแต่ละValueแอตทริบิวต์เป็นองค์ประกอบได้อย่างไร

<Root>
    <Elements>
        <Element Code="1">
            <Value>aaa</Value>
        </Element>
        <Element Code="2">
            <Value>bbb</Value>
        </Element>
        <Element Code="3">
            <Value>ccc</Value>
        </Element>
    </Elements>
</Root>

ปรับปรุง:

XML ของฉันมีลักษณะเช่นนี้:

<Root attr1="val1" attr2="val2">
    <Elements>
        <Element Code="1" Value="aaa" ExtraData="extra" />
        <Element Code="2" Value="bbb" ExtraData="extra" />
        <Element Code="3" Value="ccc" ExtraData="extra" />
        <Element Code="4" Value="" ExtraData="extra" />
        <Element Code="5" ExtraData="extra" />
    </Elements>
    <ExtraData>
       <!-- Some XML is here -->
    </ExtraData>
</Root>

ฉันต้องการย้ายValueเฉพาะคุณลักษณะและเก็บรักษาคุณลักษณะและองค์ประกอบอื่น ๆ ทั้งหมด


ทำไมคุณต้องการทำสิ่งนี้ตั้งแต่แรก? ฉันไม่สามารถคิดของผลประโยชน์ใด ๆ ในการนี้ถ้าคุณวางแผนที่จะมีหลายองค์ประกอบละ<Value> <Element>ถ้าไม่เช่นนั้นการย้ายแอตทริบิวต์ไปยังองค์ประกอบจะทำให้ XML พองตัวและมีประสิทธิภาพน้อยกว่า
โซโลมอน Rutzky

@ srutzky มันเป็นส่วนหนึ่งของการฟื้นฟู ขั้นตอนที่สองคือการจัดเก็บข้อมูลที่ซับซ้อนภายใน<Value>องค์ประกอบหรือแทนมัน
Wojteq

คำตอบ:


13

คุณสามารถทำลาย XML และสร้างใหม่อีกครั้งโดยใช้ XQuery

declare @X xml = '
<Root attr1="val1" attr2="val2">
    <Elements>
        <Element Code="1" Value="aaa" ExtraData="extra" />
        <Element Code="2" Value="" ExtraData="extra" />
        <Element Code="3" ExtraData="extra" />
    </Elements>
    <ExtraData>
       <!-- Some XML is here -->
    </ExtraData>
</Root>';

select @X.query('
  (: Create element Root :)
  element Root 
    {
      (: Add all attributes from Root to Root :)
      /Root/@*, 
      (: create element Elements under Root :)
      element Elements 
        {
          (: For each Element element in /Root/Elements :)
          for $e in /Root/Elements/Element
          return 
            (: Add element Element :)
            element Element 
              {
                (: Add all attributes except Value to Element :)
                $e/@*[local-name() != "Value"], 

                (: Check if Attribute Value exist :)
                if (data($e/@Value) != "")
                then
                  (: Create a Value element under Element :)
                  element Value 
                  {
                    (: Add attribute Value as data to the element Element :)
                    data($e/@Value)
                  }
                else () (: Empty element :)
              } 
          },
      (: Add all childelements to Root except the Elements element :)
      /Root/*[local-name() != "Elements"]
    }');

ผลลัพธ์:

<Root attr1="val1" attr2="val2">
  <Elements>
    <Element Code="1" ExtraData="extra">
      <Value>aaa</Value>
    </Element>
    <Element Code="2" ExtraData="extra" />
    <Element Code="3" ExtraData="extra" />
  </Elements>
  <ExtraData>
    <!-- Some XML is here -->
  </ExtraData>
</Root>

หากElementsไม่ใช่องค์ประกอบแรกภายใต้Rootแบบสอบถามจะต้องมีการแก้ไขเพื่อเพิ่มองค์ประกอบทั้งหมดก่อนก่อนElementsและองค์ประกอบทั้งหมดหลังจากElementsหลังจาก


5

คุณยังสามารถใช้วิธีการของประเภทข้อมูล XML (เช่นแก้ไข ) และ XQuery บางอย่างเพื่อแก้ไข xml เช่น

DECLARE @x XML = '<Root attr1="val1" attr2="val2">
    <Elements>
        <Element Code="1" Value="aaa" ExtraData="extra" />
        <Element Code="2" Value="bbb" ExtraData="extra" />
        <Element Code="3" Value="ccc" ExtraData="extra" />
    </Elements>
    <ExtraData>
       <!-- Some XML is here -->
    </ExtraData>
</Root>'


SELECT 'before' s, DATALENGTH(@x) dl, @x x

-- Add 'Value' element to each Element which doesn't already have one
DECLARE @i INT = 0

WHILE @x.exist('Root/Elements/Element[not(Value)]') = 1
BEGIN

    SET @x.modify( 'insert element Value {data(Root/Elements/Element[not(Value)]/@Value)[1]} into (Root/Elements/Element[not(Value)])[1]' )

    SET @i += 1

    IF @i > 99 BEGIN RAISERROR( 'Too many loops...', 16, 1 ) BREAK END

END

-- Now delete all Value attributes
SET @x.modify('delete Root/Elements/Element/@Value' )

SELECT 'after' s, DATALENGTH(@x) dl, @x x

วิธีนี้ไม่ได้มีแนวโน้มที่จะขยายได้ดีเกินขนาดใหญ่ของ XML แต่อาจเหมาะกับคุณดีกว่าการทดแทน XML ขายส่ง

คุณสามารถปรับวิธีนี้ได้อย่างง่ายดายหาก XML ของคุณถูกเก็บไว้ในตาราง จากประสบการณ์อีกครั้งฉันจะไม่แนะนำให้เรียกใช้การปรับปรุงเดียวกับตารางล้านแถว หากตารางของคุณมีขนาดใหญ่ให้ลองเรียกใช้เคอร์เซอร์ผ่านมันหรือทำการอัพเดทเป็นกลุ่มเป็นชุด นี่คือเทคนิค:

DECLARE @t TABLE ( rowId INT IDENTITY PRIMARY KEY, yourXML XML )

INSERT INTO @t ( yourXML )
SELECT '<Root attr1="val1" attr2="val2">
    <Elements>
        <Element Code="1" Value="aaa" ExtraData="extra" />
        <Element Code="2" Value="bbb" ExtraData="extra" />
        <Element Code="3" Value="ccc" ExtraData="extra" />
    </Elements>
    <ExtraData>
       <!-- Some XML is here -->
    </ExtraData>
</Root>'

INSERT INTO @t ( yourXML )
SELECT '<Root attr1="val1" attr2="val2">
    <Elements>
        <Element Code="21" Value="uuu" ExtraData="extra" />
        <Element Code="22" Value="vvv" ExtraData="extra" />
        <Element Code="23" Value="www" ExtraData="extra" />
        <Element Code="24" Value="xxx" ExtraData="extra" />
        <Element Code="25" Value="yyy" ExtraData="extra" />
        <Element Code="26" Value="zzz" ExtraData="extra" />
    </Elements>
    <ExtraData>
       <!-- Some XML is here -->
    </ExtraData>
</Root>'


SELECT 'before' s, DATALENGTH(yourXML) dl, yourXML
FROM @t 

-- Add 'Value' element to each Element which doesn't already have one
DECLARE @i INT = 0

WHILE EXISTS ( SELECT * FROM @t WHERE yourXML.exist('Root/Elements/Element[not(Value)]') = 1 )
BEGIN

    UPDATE @t
    SET yourXML.modify( 'insert element Value {data(Root/Elements/Element[not(Value)]/@Value)[1]} into (Root/Elements/Element[not(Value)])[1]' )

    SET @i += 1

    IF @i > 99 BEGIN RAISERROR( 'Too many loops...', 16, 1 ) BREAK END

END

-- Now delete all Value attributes
UPDATE @t
SET yourXML.modify('delete Root/Elements/Element/@Value' )

SELECT 'after' s, DATALENGTH(yourXML) dl, yourXML
FROM @t 

4

UPDATE:

ฉันได้อัปเดตโค้ดรวมถึง XML อินพุตและเอาต์พุตในเคียวรีตัวอย่างด้านล่างเพื่อแสดงถึงข้อกำหนดล่าสุดที่ระบุไว้ในความคิดเห็นในคำตอบที่ดีของ @ Mikael ซึ่งก็คือ:

เพื่อไม่สร้างองค์ประกอบค่าหาก @Value ว่างเปล่าหรือไม่มีอยู่

ในขณะที่นิพจน์เดียวสามารถจับคู่รูปแบบใหม่นี้ได้อย่างถูกต้องดูเหมือนจะไม่มีทางที่จะละเว้น<Value/>องค์ประกอบที่ว่างเปล่าในการส่งครั้งเดียวเนื่องจากตรรกะเงื่อนไขที่ไม่ได้รับอนุญาตในสตริงการเปลี่ยน ดังนั้นฉันได้ดัดแปลงสิ่งนี้ให้เป็นการดัดแปลง 2 ส่วน: หนึ่งรอบเพื่อรับ@Valueแอตทริบิวต์ที่ไม่ว่างเปล่าและหนึ่งรอบเพื่อให้ได้@Valueแอตทริบิวต์ที่ว่างเปล่า ไม่จำเป็นต้องจัดการ<Element>กับ@Valueคุณลักษณะที่ขาดหายไปเนื่องจากความปรารถนาที่จะไม่มี<Value>องค์ประกอบอยู่ดี


ทางเลือกหนึ่งคือการปฏิบัติ XML เป็นสตริงปกติและแปลงมันตามรูปแบบ สิ่งนี้สามารถทำได้อย่างง่ายดายโดยใช้นิพจน์ปกติ (โดยเฉพาะฟังก์ชั่น "แทนที่") ซึ่งสามารถทำได้ผ่านรหัส SQLCLR

ตัวอย่างด้านล่างใช้UDF สเกลาร์RegEx_ReplaceจากไลบรารีSQL # (ซึ่งฉันเป็นผู้เขียน แต่ฟังก์ชัน RegEx นี้มีให้บริการในรุ่นฟรีพร้อมด้วยอื่น ๆ อีกมากมาย):

DECLARE @SomeXml XML;
SET @SomeXml = N'<Root attr1="val1" attr2="val2">
    <Elements>
        <Element Code="1" Value="aaa" ExtraData="extra1" />
        <Element Code="22" Value="bbb" ExtraData="extra2" />
        <Element Code="333" Value="ccc" ExtraData="extra3" />
        <Element Code="4444" Value="" ExtraData="extra4" />
        <Element Code="55555" ExtraData="extra5" />
    </Elements>
    <ExtraData>
       <Something Val="1">qwerty A</Something>
       <Something Val="2">qwerty B</Something>
    </ExtraData>
</Root>';

DECLARE @TempStringOfXml NVARCHAR(MAX),
        @Expression NVARCHAR(4000),
        @Replacement NVARCHAR(4000);


SET @TempStringOfXml = CONVERT(NVARCHAR(MAX), @SomeXml);
PRINT N'Original: ' + @TempStringOfXml;

---

SET @Expression =
              N'(<Element Code="[^"]+")\s+Value="([^"]+)"\s+(ExtraData="[^"]+")\s*/>';
SET @Replacement = N'$1 $3><Value>$2</Value></Element>';

SELECT @TempStringOfXml = SQL#.RegEx_Replace(@TempStringOfXml, @Expression,
                                             @Replacement, -1, 1, '');

PRINT '-------------------------------------';
PRINT N'Phase 1:  ' + @TempStringOfXml; -- transform Elements with a non-empty @Value

---

SET @Expression = N'(<Element Code="[^"]+")\s+Value=""\s+(ExtraData="[^"]+")\s*/>';
SET @Replacement = N'$1 $2 />';

SELECT @TempStringOfXml = SQL#.RegEx_Replace(@TempStringOfXml, @Expression,
                                             @Replacement, -1, 1, '');

PRINT '-------------------------------------';
PRINT N'Phase 2:  ' + @TempStringOfXml; -- transform Elements with an empty @Value

SELECT CONVERT(XML, @TempStringOfXml); -- prove that this is valid XML

PRINTงบมีอยู่ในเพียงเพื่อให้สำหรับการเปรียบเทียบด้านข้างได้ง่ายขึ้นในแท็บ "ข้อความ" ผลลัพธ์ที่ได้คือ (ฉันปรับเปลี่ยน XML ดั้งเดิมเล็กน้อยเพื่อให้ชัดเจนมากว่าเฉพาะส่วนที่ต้องการเท่านั้นที่สัมผัสและไม่มีอะไรอื่น):

Original: <Root attr1="val1" attr2="val2"><Elements><Element Code="1" Value="aaa" ExtraData="extra1"/><Element Code="22" Value="bbb" ExtraData="extra2"/><Element Code="333" Value="ccc" ExtraData="extra3"/><Element Code="4444" Value="" ExtraData="extra4"/><Element Code="55555" ExtraData="extra5"/></Elements><ExtraData><Something Val="1">qwerty A</Something><Something Val="2">qwerty B</Something></ExtraData></Root>
-------------------------------------
Phase 1:  <Root attr1="val1" attr2="val2"><Elements><Element Code="1" ExtraData="extra1"><Value>aaa</Value></Element><Element Code="22" ExtraData="extra2"><Value>bbb</Value></Element><Element Code="333" ExtraData="extra3"><Value>ccc</Value></Element><Element Code="4444" Value="" ExtraData="extra4"/><Element Code="55555" ExtraData="extra5"/></Elements><ExtraData><Something Val="1">qwerty A</Something><Something Val="2">qwerty B</Something></ExtraData></Root>
-------------------------------------
Phase 2:  <Root attr1="val1" attr2="val2"><Elements><Element Code="1" ExtraData="extra1"><Value>aaa</Value></Element><Element Code="22" ExtraData="extra2"><Value>bbb</Value></Element><Element Code="333" ExtraData="extra3"><Value>ccc</Value></Element><Element Code="4444" ExtraData="extra4" /><Element Code="55555" ExtraData="extra5"/></Elements><ExtraData><Something Val="1">qwerty A</Something><Something Val="2">qwerty B</Something></ExtraData></Root>

หากคุณต้องการอัปเดตเขตข้อมูลในตารางคุณสามารถปรับตัวด้านบนให้เป็นดังต่อไปนี้:

DECLARE @NonEmptyValueExpression NVARCHAR(4000),
        @NonEmptyValueReplacement NVARCHAR(4000),
        @EmptyValueExpression NVARCHAR(4000),
        @EmptyValueReplacement NVARCHAR(4000);

SET @NonEmptyValueExpression =
                   N'(<Element Code="[^"]+")\s+Value="([^"]+)"\s+(ExtraData="[^"]+")\s*/>';
SET @NonEmptyValueReplacement = N'$1 $3><Value>$2</Value></Element>';

SET @EmptyValueExpression =
                   N'(<Element Code="[^"]+")\s+Value=""\s+(ExtraData="[^"]+")\s*/>';
SET @EmptyValueReplacement = N'$1 $2 />';

UPDATE tbl
SET    XmlField = SQL#.RegEx_Replace4k(
                                     SQL#.RegEx_Replace4k(
                                                     CONVERT(NVARCHAR(4000), tbl.XmlField),
                                                        @NonEmptyValueExpression,
                                                        @NonEmptyValueReplacement,
                                                        -1, 1, ''),
                                     @EmptyValueExpression,
                                     @EmptyValueReplacement,
                                     -1, 1, '')
FROM   SchemaName.TableName tbl
WHERE  tbl.XmlField.exist('Root/Elements/Element/@Value') = 1;

โซลูชันของคุณดูดีและมันก็มีประโยชน์ แต่ฉันใช้ CLR ได้
Wojteq

@ Wojteq ขอบคุณ มันดีที่มีตัวเลือกใช่ไหม? จากความอยากรู้อยากเห็นทำไมคุณไม่สามารถใช้ SQLCLR ได้
โซโลมอน Rutzky

มันเป็นเพราะสถาปัตยกรรมของเรา เรามีเว็บแอปพลิเคชั่นหลายผู้เช่า ผู้เช่าทุกคนมีฐานข้อมูลของตนเอง เราไม่ต้องการเพิ่ม 'ส่วนที่เคลื่อนไหว' อื่น ๆ ที่อาจล้มเหลวระหว่างกระบวนการปรับใช้เช่น การใช้วิธีการแบบรหัสเท่านั้น / webapp-only สามารถบำรุงรักษาได้มากขึ้นสำหรับเรา
Wojteq

1

อาจมีวิธีที่ดีกว่าในการทำนอก SQL Server อย่างไรก็ตามนี่เป็นวิธีหนึ่งในการทำมัน

ข้อมูลของคุณ:

declare @xml xml = N'<Root>
    <Elements>
        <Element Code="1" Value="aaa"></Element>
        <Element Code="2" Value="bbb"></Element>
        <Element Code="3" Value="ccc"></Element>
    </Elements>
</Root>';

ค้นหา:

With xml as (
    Select 
        Code = x.e.value('(@Code)', 'varchar(10)')
        , Value = x.e.value('(@Value)', 'varchar(10)')
    From @xml.nodes('/Root//Elements/Element') as x(e)
)
Select * From (
    Select code
        , (
        Select value
        From xml x1 where x1.Code = Element.Code
        For xml path(''), elements, type
    )
    From xml Element
    For xml auto, type
) as Root(Elements)
for xml auto, elements;

xml CTE แปลงตัวแปร xml ของคุณเป็นตาราง

ตัวเลือกหลักจากนั้นแปลง CTE กลับเป็น xml

เอาท์พุท:

<Root>
  <Elements>
    <Element code="1">
      <value>aaa</value>
    </Element>
    <Element code="2">
      <value>bbb</value>
    </Element>
    <Element code="3">
      <value>ccc</value>
    </Element>
  </Elements>
</Root>

For XML Explicitนอกจากนี้ยังสามารถทำได้โดยใช้


ขอบคุณสำหรับความช่วยเหลือ แต่ฉันได้อัปเดตคำถามแล้ว - กรณีของฉันคือการย้ายที่ซับซ้อน ฉันต้องการอัปเดต XML ของฉันโดยใช้ SQL Server เนื่องจากประสิทธิภาพ ฉันมีตารางที่มีบันทึกนับแสนรายการ อีกทางเลือกหนึ่งคือการโหลดลบซีเรียลไลซ์และทำให้เป็นอนุกรมภายในแอปพลิเคชัน ASP MVC
Wojteq
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.