Ubah XML: atribut menjadi elemen

11

Saya memiliki XMLkolom yang berisi data dengan struktur serupa:

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

Bagaimana saya bisa memodifikasi data menggunakan SQL Server untuk mengubah setiap Valueatribut menjadi elemen?

<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>

Memperbarui:

XML saya terlihat lebih seperti ini:

<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>

Saya hanya ingin memindahkan Valueatribut dan mempertahankan semua atribut dan elemen lainnya.

Wojteq
sumber
Mengapa Anda ingin melakukan ini sejak awal? Saya tidak dapat memikirkan manfaatnya kecuali Anda berencana memiliki beberapa <Value>elemen per masing-masing <Element>. Jika tidak, maka memindahkan atribut ke elemen hanya membuat XML lebih kembung, dan mungkin kurang efisien.
Solomon Rutzky
@rutzky, ini adalah bagian dari refactoring. Langkah kedua adalah menyimpan data kompleks di dalam <Value>elemen atau sebagai gantinya.
Wojteq

Jawaban:

13

Anda dapat menghancurkan XML dan membangunnya kembali menggunakan 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"]
    }');

Hasil:

<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>

Jika Elementsbukan elemen pertama di bawah Rootkueri perlu dimodifikasi untuk menambahkan semua elemen sebelum elemen Elementspertama dan Elementssesudahnya.

Mikael Eriksson
sumber
5

Anda juga dapat menggunakan metode tipe data XML (misalnya, modifikasi ) dan beberapa XQuery untuk memodifikasi xml, misalnya

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

Metode ini tidak cenderung untuk menskalakan XML dalam jumlah besar tetapi mungkin lebih cocok untuk Anda daripada penggantian XML secara grosir.

Anda juga dapat dengan mudah mengadaptasi metode ini jika XML Anda disimpan dalam sebuah tabel. Sekali lagi dari pengalaman saya tidak akan merekomendasikan menjalankan pembaruan tunggal terhadap tabel juta baris. Jika meja Anda besar, pertimbangkan menjalankan kursor melewatinya atau mengelompokkan pembaruan. Inilah tekniknya:

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 
wBob
sumber
4

MEMPERBARUI:

Saya telah memperbarui kode, serta input dan output XML dalam contoh pertanyaan di bawah ini untuk mencerminkan persyaratan terbaru, dinyatakan dalam komentar pada jawaban baik @ Mikael , yaitu:

untuk tidak membuat elemen Nilai jika @Nilai kosong atau tidak ada

Sementara satu ekspresi dapat dengan benar mencocokkan variasi baru ini, tampaknya tidak ada cara untuk menghilangkan <Value/>elemen kosong dalam satu lintasan karena logika kondisional tidak diperbolehkan dalam string pengganti. Jadi, saya telah mengadaptasi ini menjadi modifikasi 2 bagian: satu pass untuk mendapatkan @Valueatribut yang tidak kosong dan satu pass untuk mendapatkan @Valueatribut kosong . Tidak perlu menangani atribut <Element>yang hilang @Valuekarena keinginannya adalah untuk tidak memiliki <Value>elemen.


Salah satu pilihan adalah memperlakukan XML sebagai string biasa dan mengubahnya berdasarkan suatu pola. Ini mudah dilakukan dengan menggunakan Ekspresi Reguler (khususnya fungsi "Ganti") yang dapat disediakan melalui kode SQLCLR.

Contoh di bawah ini menggunakan skalar UDF RegEx_Replace dari pustaka SQL # (yang saya penulis, tetapi fungsi RegEx ini tersedia dalam versi Gratis, bersama dengan banyak lainnya):

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

The PRINTpernyataan berada di sana hanya untuk membuat lebih mudah perbandingan sisi-by-side di tab "Pesan". Output yang dihasilkan adalah (saya memodifikasi XML asli sedikit untuk membuatnya sangat jelas bahwa hanya bagian yang diinginkan yang disentuh dan tidak ada yang lain):

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>

Jika Anda ingin memperbarui bidang dalam tabel, Anda bisa menyesuaikan yang di atas sebagai berikut:

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;
Solomon Rutzky
sumber
solusi Anda terlihat bagus dan itu membantu tetapi saya dapat menggunakan CLR.
Wojteq
@ Wojteq Terima kasih. Baik memiliki opsi, bukan? Hanya karena penasaran, mengapa Anda tidak dapat menggunakan SQLCLR?
Solomon Rutzky 6-15
Itu karena arsitektur kita. Kami punya aplikasi web multi-tenancy. Setiap penyewa memiliki database sendiri. Kami tidak ingin menambahkan 'bagian bergerak' lain yang dapat gagal selama proses penyebaran yaitu. Menggunakan pendekatan hanya kode / webapp saja jauh lebih mudah dikelola untuk kita.
Wojteq
1

Mungkin ada cara yang lebih baik untuk melakukannya di luar SQL Server. Namun, inilah salah satu cara melakukannya.

Data Anda:

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>';

Pertanyaan:

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 mengubah variabel xml Anda menjadi sebuah tabel.

Pilih utama kemudian mengubah CTE kembali ke xml.

Keluaran:

<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>

Itu juga bisa dilakukan menggunakan For XML Explicit.

Julien Vavasseur
sumber
Terima kasih atas bantuan Anda namun saya telah memperbarui pertanyaan saya - kasing saya rumit. Saya ingin memperbarui XML saya menggunakan SQL Server karena kinerja. Saya punya tabel yang berisi ratusan ribu catatan. Alternatif lainnya adalah memuatnya, menghilangkan deserialisasi dan membuat serial di dalam aplikasi ASP MVC.
Wojteq