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이 부풀어지고 효율성이 떨어집니다.
Solomon Rutzky

@ srutzky, 그것은 리팩토링의 일부입니다. 두 번째 단계는 복잡한 데이터를 <Value>요소 내부 또는 대신 저장하는 것입니다.
Wojteq

답변:


13

XQuery를 사용하여 XML을 파쇄하고 다시 작성할 수 있습니다.

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>

If Elements가 첫 번째 요소가 아닌 경우 첫 번째 Root요소 앞에 Elements및 모든 요소 를 추가하도록 수정해야합니다 Elements.


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이 테이블에 저장된 경우이 방법을 쉽게 적용 할 수도 있습니다. 다시 한 번 경험상 백만 행 테이블에 대해 단일 업데이트를 실행하지 않는 것이 좋습니다. 테이블이 큰 경우 커서를 테이블을 통해 실행하거나 업데이트를 일괄 처리하는 것이 좋습니다. 기술은 다음과 같습니다.

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 's fine answer 에 대한 의견에 명시된 최신 요구 사항을 반영하기 위해 아래 예제 쿼리의 입력 및 출력 XML뿐만 아니라 코드를 업데이트했습니다 .

@Value가 비어 있거나 존재하지 않는 경우 Value 요소를 만들지 않습니다.

단일 표현식이이 새로운 변형과 정확하게 일치 할 수 있지만 <Value/>, 대체 문자열에서 조건부 논리가 허용되지 않으므로 단일 패스에서 빈 요소 를 생략 할 수있는 방법 이 없습니다. 그래서 나는 이것을 두 부분으로 수정했습니다. 하나 @Value는 비어 있지 않은 @Value속성 을 얻는 패스와 하나는 빈 속성 을 얻는 패스 입니다. 어쨌든 요소를 갖지 않기를 원 <Element>하므로 @Value속성이 누락 된 을 처리 할 필요가 없었 <Value>습니다.


한 가지 옵션은 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

@Wojteq 감사합니다. 옵션이 있으면 좋을까요? 호기심 때문에 SQLCLR을 사용할 수없는 이유는 무엇입니까?
Solomon Rutzky

우리의 건축 때문입니다. 멀티 테넌시 웹 애플리케이션이 있습니다. 모든 테넌트에는 자체 데이터베이스가 있습니다. 배포 프로세스 중에 실패 할 수있는 다른 '이동 부품'을 추가하고 싶지 않습니다. 코드 전용 / 웹앱 전용 접근 방식을 사용하면 훨씬 더 유지 관리가 가능합니다.
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.


도와 주셔서 감사합니다.하지만 질문을 업데이트했습니다. 제 사건은 복잡합니다. 성능 때문에 SQL Server를 사용하여 XML을 업데이트하고 싶습니다. 수십만 개의 레코드가 포함 된 테이블이 있습니다. 다른 대안은 ASP MVC 응용 프로그램 내에서로드, 직렬화 해제 및 직렬화하는 것입니다.
Wojteq
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.