XML: Attribute in Elemente ändern

11

Ich habe eine XMLSpalte, die Daten mit ähnlicher Struktur enthält:

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

Wie kann ich die Daten mit SQL Server ändern, um jedes ValueAttribut in ein Element zu ändern ?

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

Aktualisieren:

Mein XML sieht eher so aus:

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

Ich möchte nur ValueAttribute verschieben und alle anderen Attribute und Elemente beibehalten.

Wojteq
quelle
Warum willst du das überhaupt machen? Ich kann mir keinen Nutzen daraus vorstellen, es sei denn, Sie planen mehrere <Value>Elemente pro Element <Element>. Wenn nicht, führt das Verschieben des Attributs in ein Element nur zu aufgeblähterem und möglicherweise weniger effizientem XML.
Solomon Rutzky
@srutzky, es ist Teil eines Refactorings. Der zweite Schritt ist das Speichern komplexer Daten innerhalb eines <Value>Elements oder stattdessen.
Wojteq

Antworten:

13

Sie können das XML vernichten und mit XQuery erneut erstellen.

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"]
    }');

Ergebnis:

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

Wenn dies Elementsnicht das erste Element unter Rootder Abfrage ist, muss es geändert werden, um alle Elemente vor dem Elementsersten und alle Elemente nach dem Hinzufügen hinzuzufügen Elements.

Mikael Eriksson
quelle
Vielen Dank für Ihre Hilfe, aber ich habe meine Frage aktualisiert - mein Fall ist komplex.
Wojteq
2
@Wojteq hat eine komplexere Antwort hinzugefügt.
Mikael Eriksson
Es sieht sehr schön aus und funktioniert! Könnten Sie bitte die Abfrage so ändern, dass kein ValueElement erstellt wird, wenn @Valuees leer ist oder nicht vorhanden ist? Ich habe es versucht, aber ich habe versagt.
Wojteq
1
@srutzky weiß nicht, ob flwor in modify funktioniert oder nicht, aber die Einschränkung, jeweils nur ein Element hinzuzufügen oder zu ändern, verhindert, dass hier Änderungen vorgenommen werden. Es sei denn, Sie tun dies Element für Element in einer Schleife. Sie können mehr als ein Element gleichzeitig löschen, aber das war hier nur die Hälfte.
Mikael Eriksson
1
@srutzky Übrigens, ich glaube (ohne zu testen), dass Ihre Antwort die schnellste ist. Wenn also die Leistung ein Problem darstellt und dies vollständig von der Größe des XML abhängt, versuchen Sie auf jeden Fall, den regulären Ausdruck zu ersetzen.
Mikael Eriksson
5

Sie können auch die Methoden des XML-Datentyps (z. B. Ändern ) und einige XQuery verwenden, um die XML zu ändern, z

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

Diese Methode lässt sich in der Regel nicht gut über große XML-Teile skalieren, eignet sich jedoch möglicherweise besser als ein umfassender Ersatz für XML.

Sie können diese Methode auch einfach anpassen, wenn Ihr XML in einer Tabelle gespeichert ist. Aus Erfahrung würde ich nicht empfehlen, ein einzelnes Update für eine Million Zeilentabellen auszuführen. Wenn Ihre Tabelle groß ist, ziehen Sie einen Cursor durch die Tabelle oder stapeln Sie die Aktualisierungen auf andere Weise. Hier ist die Technik:

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

AKTUALISIEREN:

Ich habe den Code sowie das Eingabe- und Ausgabe-XML in der folgenden Beispielabfrage aktualisiert, um die neueste Anforderung widerzuspiegeln, die in einem Kommentar zu @ Mikaels feiner Antwort angegeben ist:

Wertelement nicht erstellen, wenn @Value leer ist oder nicht existiert

Während ein einzelner Ausdruck dieser neuen Variante korrekt entsprechen kann, scheint es keine Möglichkeit zu geben, das leere <Value/>Element in einem einzelnen Durchgang wegzulassen , da die bedingte Logik in der Ersetzungszeichenfolge nicht zulässig ist. Daher habe ich dies als zweiteilige Modifikation angepasst: einen Durchgang, um die nicht leeren @ValueAttribute zu erhalten, und einen Durchgang, um die leeren @ValueAttribute zu erhalten. Es war nicht erforderlich, mit dem <Element>Fehlen des @ValueAttributs umzugehen, da der Wunsch besteht, das <Value>Element sowieso nicht zu haben .


Eine Möglichkeit besteht darin, das XML als regulären String zu behandeln und basierend auf einem Muster zu transformieren. Dies kann leicht mit regulären Ausdrücken (insbesondere der Funktion "Ersetzen") erreicht werden, die über den SQLCLR-Code verfügbar gemacht werden können.

Im folgenden Beispiel wird die skalare UDF RegEx_Replace aus der SQL # -Bibliothek verwendet (deren Autor ich bin, aber diese RegEx-Funktion ist in der kostenlosen Version zusammen mit vielen anderen verfügbar):

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

Die PRINTAnweisungen dienen nur dazu, den Vergleich nebeneinander auf der Registerkarte "Nachrichten" zu vereinfachen. Die resultierende Ausgabe ist (ich habe das ursprüngliche XML ein wenig geändert, um deutlich zu machen, dass nur die gewünschten Teile berührt wurden und sonst nichts):

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>

Wenn Sie ein Feld in einer Tabelle aktualisieren möchten, können Sie Folgendes anpassen:

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
quelle
Ihre Lösung sieht gut aus und war hilfreich, aber ich kann CLR verwenden.
Wojteq
@ Wojteq Danke. Es ist gut, Optionen zu haben, oder? Warum können Sie SQLCLR aus Neugier nicht verwenden?
Solomon Rutzky
Es liegt an unserer Architektur. Wir haben eine mandantenfähige Webanwendung. Jeder Mieter hat eine eigene Datenbank. Wir möchten kein anderes "bewegliches Teil" hinzufügen, das während des Bereitstellungsprozesses fehlschlagen kann. Die Verwendung von Code-only / Webapp-only-Ansatz ist für uns viel wartbarer.
Wojteq
1

Es gibt wahrscheinlich bessere Möglichkeiten, dies außerhalb von SQL Server zu tun. Hier ist jedoch eine Möglichkeit, dies zu tun.

Deine Daten:

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

Abfrage:

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;

Der XML-CTE wandelt Ihre XML-Variable in eine Tabelle um.

Die Hauptauswahl wandelt dann den CTE wieder in XML um.

Ausgabe:

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

Dies kann auch mit erfolgen For XML Explicit.

Julien Vavasseur
quelle
Vielen Dank für Ihre Hilfe, aber ich habe meine Frage aktualisiert - mein Fall ist komplex. Ich möchte mein XML aus Leistungsgründen mit SQL Server aktualisieren. Ich habe Tabellen, die Hunderttausende von Datensätzen enthalten. Die andere Alternative besteht darin, es in der ASP MVC-Anwendung zu laden, zu deserialisieren und zu serialisieren.
Wojteq