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が肥大化し、おそらく効率が低下します。
ソロモンルツキー2015年

@srutzky、それはリファクタリングの一部です。2番目のステップは、<Value>要素内またはその代わりに複雑なデータを格納することです。
Wojteq 2015年

回答:


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>

ElementsRootクエリの最初の要素でない場合は、最初の前にすべての要素を追加しElementsElementsその後にすべての要素を追加するように変更する必要があります。


ご協力ありがとうございます。ただし、質問を更新しました-私のケースは複雑な動きです。
Wojteq 2015年

2
@Wojteqはより複雑な答えを追加しました。
ミカエルエリクソン2015年

それはとても見栄えが良く、うまくいきます!空であるか存在しないValue場合、要素を作成しないようにクエリを変更していただけ@Valueませんか?試しましたが失敗しました。
Wojteq 2015年

1
@srutzkyは、変更のflworが機能するかどうかはわかりませんが、一度に1つの要素のみを追加または変更するという制限があるため、ここでは変更の使用を停止しています。ループで一度に1つの要素を実行しない限り。一度に複数の要素を削除できますが、ここでは半分しかありませんでした。
ミカエルエリクソン2015年

1
@srutzkyところで、私は(testinなしで)あなたの答えが最も速いと信じています。したがって、パフォーマンスが問題であり、それが完全にXMLのサイズに依存している場合は、間違いなく正規表現の置き換えを試してください。
ミカエルエリクソン2015年

5

XMLデータ型のメソッド(例:modify)と一部の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がテーブルに格納されている場合は、このメソッドを簡単に適応させることもできます。繰り返しになりますが、100万行のテーブルに対して単一の更新を実行することはお勧めしません。テーブルが大きい場合は、テーブルを介してカーソルを実行するか、更新をバッチ処理することを検討してください。ここにテクニックがあります:

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

更新:

@Mikaelの細かい回答に関するコメントに記載されている最新の要件を反映するために、以下のクエリ例のコードと入力XMLおよび出力XMLを更新しました。

@Valueが空または存在しない場合にValue要素を作成しない

単一の式はこの新しいバリエーションに正しく一致します<Value/>が、条件付きロジックは置換文字列で許可されないため、単一のパスで空の要素を省略する方法はないようです。そこで、これを2つの部分からなる変更に改造しました。1つ@Valueは空でない@Value属性を取得するためのパスで、もう1つは空の属性を取得するためのパスです。とにかく要素を持たないことが望ましいので、属性<Element>のないs を処理する必要はあり@Valueませんでし<Value>た。


1つのオプションは、XMLを通常の文字列として扱い、パターンに基づいて変換することです。これは、SQLCLRコードを介して利用できる正規表現(特に「置換」関数)を使用して簡単に実現できます。

以下の例では、SQL#ライブラリのRegEx_ReplaceスカラーUDFを使用しています(私はこれを作成していますが、この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 2015年

@Wojteqありがとう。オプションがあるのはいいことですよね?好奇心から、SQLCLRを使用できないのはなぜですか?
ソロモンルツキー2015年

それは私たちのアーキテクチャによるものです。マルチテナンシーWebアプリケーションがあります。すべてのテナントには独自のデータベースがあります。導入プロセス中に失敗する可能性のある他の「可動部分」を追加したくありません。コードのみ/ Webアプリのみのアプローチを使用する方がはるかにメンテナンスしやすくなります。
Wojteq 2016年

1

SQL Serverの外部でそれを行うには、おそらくより良い方法があります。ただし、これを行う1つの方法があります。

あなたのデータ:

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ます。


ご協力ありがとうございます。ただし、質問を更新しました-私のケースは複雑な動きです。パフォーマンスのため、SQL Serverを使用してXMLを更新したいのですが。数十万のレコードを含むテーブルがあります。もう1つの方法は、それをロードし、ASP MVCアプリケーション内で逆シリアル化およびシリアル化することです。
Wojteq 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.