Обновление атрибута с помощью xquery в sql server 2008 R2

У меня есть таблица, которая определяет макет формы в XML. Внутри него есть <Control ...> узлы, которые имеют такие атрибуты, как Id (GUID) и DataType (char) и т. д.

Эти XML-данные используются для создания формы во время выполнения, когда данные сохраняются, они записываются в XML в элементах с именем <Field ...>, которые имеют атрибуты: Имя (соответствует GUID в идентификаторе узла управления) и Данные (содержит значение, которое был введен в контроль).

У нас есть проблема, из-за которой, когда вы .ToString объект Date, он использует настройки машины для формата даты. Таким образом, даты могут быть сохранены в любом возможном формате. Я обновил код, чтобы всегда сохранять даты в формате YYYY/MM/DD, но теперь мне нужно обновить существующие данные в базе данных до YYYY/MM/DD. Поскольку у нас нет возможности узнать, в каком формате была сохранена дата, мы просто предположим, что она была сохранена как MM/DD/YYYY.

Итак, теперь моя проблема заключается в попытке обновить XML в SQL Server. Используя xquery и CROSS APPLY .nodes(), я могу написать запрос, чтобы найти все неправильно отформатированные даты, но я не могу понять, как их обновить.

Этот сценарий SQL создаст табличные переменные и заполнит их некоторыми тестовыми данными, а также имеет запрос, который вернет значения неправильных дат. последняя закомментированная часть - это то, как я пытался обновить их до нового формата YYYY/MM/DD, но если вы раскомментируете его, вы увидите, что это не работает.

У кого-нибудь есть идеи?

P.S. Это можно сделать только в сценарии SQL. Я знаю, что было бы очень легко написать функцию в .NET, которая считывает все данные xml и обновляет атрибуты, а затем сохраняет данные обратно в базу данных, но в данном случае это невозможно по следующим причинам.

DECLARE @Form AS Table
(
  FormID INT,
  FormDataXML XML
);

DECLARE @ScreenData AS Table
(
  ScreenDataID INT,
  FormID INT,
  ScreenDataXML XML 
);

DECLARE @ControlsToUpdate AS TABLE
(
  FormID INT,
  ControlID char(36),
  DataType CHAR(1)
);

INSERT INTO @Form
VALUES (1, '<Form><Control Id="00000000-0000-0000-0000-000000000000" DataType="D" /><Control Id="00000000-0000-0000-0000-000000000001" DataType="N" /></Form>');
INSERT INTO @Form
VALUES (2, '<Form><Control Id="00000000-0000-0000-0000-000000000002" DataType="D" /><Control Id="00000000-0000-0000-0000-000000000003" DataType="D" /></Form>');

INSERT INTO @ScreenData
VALUES (1, 1, '<ScreenData><Field Name="00000000-0000-0000-0000-000000000000" Data="01/31/2012" /><Field Name="00000000-0000-0000-0000-000000000001" Data="1234.56" /></ScreenData>');
INSERT INTO @ScreenData
VALUES (2, 1, '<ScreenData><Field Name="00000000-0000-0000-0000-000000000000" Data="02/28/2013" /><Field Name="00000000-0000-0000-0000-000000000001" Data="0" /></ScreenData>');
INSERT INTO @ScreenData
VALUES (3, 2, '<ScreenData><Field Name="00000000-0000-0000-0000-000000000002" Data="03/31/2013" /><Field Name="00000000-0000-0000-0000-000000000003" Data="04/30/2013" /></ScreenData>');
INSERT INTO @ScreenData
VALUES (4, 2, '<ScreenData><Field Name="00000000-0000-0000-0000-000000000002" Data="2013/05/31" /><Field Name="00000000-0000-0000-0000-000000000003" Data="2013/06/30" /></ScreenData>');
--Data treated as scheduled items
INSERT INTO @ScreenData
VALUES (5, 2, '<ScreenData><Item><Field Name="00000000-0000-0000-0000-000000000002" Data="01/01/2012" /><Field Name="00000000-0000-0000-0000-000000000003" Data="02/02/2012" /></Item><Item><Field Name="00000000-0000-0000-0000-000000000002" Data="03/03/2012" /><Field Name="00000000-0000-0000-0000-000000000003" Data="04/04/2012" /></Item></ScreenData>')

INSERT INTO @ControlsToUpdate
SELECT FormID,  
data.control.value('@Id', 'char(36)'),
data.control.value('@DataType', 'char(1)')
FROM @Form
CROSS APPLY FormDataXML.nodes('//Control') data(control)
WHERE data.control.value('@DataType', 'char(1)') = 'D';


--This will display the ScreenDataID, FormID, ControlID, and current Date value for dates that are in the old mm/dd/yyyy format
SELECT d.ScreenDataID, d.FormID, c.ControlID,
data.field.value('@Data', 'char(10)') as Date
FROM @ScreenData d
CROSS APPLY d.ScreenDataXML.nodes('//Field') as data(field)
INNER JOIN @ControlsToUpdate c ON c.ControlID = data.field.value('@Name', 'CHAR(36)') 
                              AND d.FormID = c.FormID
WHERE data.field.value('@Data', 'char(10)') 
LIKE '[01][0123456789]/[0123][0123456789]/[12][0123456789][0123456789][0123456789]';

--UPDATE d
--SET data.field.modify('replace value of (/@Data) with "' + 
--    SUBSTRING(data.field.value('@Data', 'char(10)'), 7, 4) + '/' + 
--    SUBSTRING(data.field.value('@Data', 'char(10)'), 1, 2) + '/' + 
--    SUBSTRING(data.field.value('@Data', 'char(10)'), 4, 2) + '"')
--FROM @ScreenData d
--CROSS APPLY d.ScreenDataXML.nodes('//Field') as data(field)
--INNER JOIN @ControlsToUpdate c ON c.Id = data.field.value('@Name', 'CHAR(36)') 
--                              AND d.FormID = c.FormID
--WHERE data.field.value('@Data', 'varchar(MAX)') 
--LIKE '[01][0123456789]/[0123][0123456789]/[12][0123456789][0123456789][0123456789]';

GO

--Edit-- XML-файл в @ScreenData также может содержать <Item> узлов, которые содержат <Field> узлов. Когда это произойдет, у вас будет несколько <Field> узлов с одинаковым значением атрибута Name. Это тот случай, когда на экране есть список элементов, у вас будет несколько элементов, каждый из которых ссылается на один и тот же элемент управления, но имеет свои собственные значения. ScreenDataID 5 показывает это.


person Nick    schedule 05.02.2013    source источник
comment
Можно ли обновить более одного атрибута в одной строке?   -  person Mikael Eriksson    schedule 06.02.2013
comment
Каждый элемент будет иметь только 1 атрибут для обновления, но каждый XML может иметь несколько элементов, каждый из которых имеет 1 атрибут для обновления. В примере данных ScreenDataID 1 имеет 1 элемент, для которого требуется обновить атрибут, ScreenDataID 2 имеет 1 элемент, для которого требуется обновление атрибута, ScreenDataID 3 имеет 2 элемента, для которых требуется обновление атрибута, а ScreenDataID 4 не нуждается в каких-либо обновлениях.   -  person Nick    schedule 06.02.2013


Ответы (1)


Я не покажу тебе, что именно ты хочешь. Я покажу вам, как изменить все ваши даты на yyyy-mm-dd, поскольку именно такими они должны быть в XML.

До сих пор вы очень хорошо разбирались во всем, поэтому я не думаю, что у вас возникнут проблемы с изменением приведенного ниже кода в соответствии с вашими потребностями, если вы решите использовать другой формат даты.

Вы не можете одновременно обновлять более одного значения в XML, поэтому обновление должно выполняться в цикле по формам и полям, которые необходимо обновить.

SET DATEFORMAT ymd; необходимо, чтобы преобразование строки в дату работало как из yyyy/mm/dd, так и из mm/yy/dd одновременно.

SET DATEFORMAT ymd;

DECLARE @FormID INT;
DECLARE @FieldID UNIQUEIDENTIFIER;
DECLARE @I INT;

DECLARE ControlsToUpdate CURSOR FORWARD_ONLY READ_ONLY FOR
SELECT F.FormID,
       T.N.value('@Id', 'uniqueidentifier') AS FieldID
FROM @Form AS F
CROSS APPLY F.FormDataXML.nodes('/Form/Control') AS T(N)
WHERE T.N.value('@DataType', 'char(1)') = 'D';

OPEN ControlsToUpdate;

FETCH NEXT FROM ControlsToUpdate 
INTO @FormID, @FieldID;

WHILE @@FETCH_STATUS = 0
BEGIN

  SELECT @I = MAX(S.ScreenDataXML.value('count(//Field[@Name = sql:variable("@FieldID")])', 'INT'))
  FROM @ScreenData AS S
  WHERE S.FormID = @FormID;

  WHILE @I > 0
  BEGIN
    UPDATE S
    SET ScreenDataXML.modify('replace value of 
                              ((//Field[@Name = sql:variable("@FieldID")]
                                 /@Data)[sql:variable("@I")])[1] 
                              with sql:column("T.D")')
    FROM @ScreenData AS S
    CROSS APPLY 
      (SELECT S.ScreenDataXML.value('((//Field[@Name = sql:variable("@FieldID")]
                                        /@Data)[sql:variable("@I")])[1]', 'DATE')) AS T(D)
    WHERE S.FormID = @FormID;

    SET @I -= 1;
  END

  FETCH NEXT FROM ControlsToUpdate
  INTO @FormID, @FieldID;
END

CLOSE ControlsToUpdate;
person Mikael Eriksson    schedule 05.02.2013
comment
Я забыл упомянуть, что один и тот же элемент управления может появляться несколько раз внутри XML в @ScreenData. Я отредактирую вопрос с дополнительной информацией. - person Nick; 06.02.2013
comment
Я попытался изменить ваш скрипт, добавив вложенный цикл, увеличив индекс и заменив [1] на [sql:variable(@Index)], но это вообще не сработало. - person Nick; 06.02.2013
comment
@Ник, вот как это сделать. Я могу обновить ответ позже. Тем временем вы можете взглянуть на мой первый ответ (нажмите на ссылку после редактирования). Это в основном цикл, который вам нужен в цикле курсора. - person Mikael Eriksson; 06.02.2013
comment
О, я вижу. Я пытался просто сделать (//Field[@Name = sql:variable("@FieldID")]/@Data)[sql:variable("@Index")], но мне нужно обернуть все это вокруг ( ) и добавить [1] в конец. Спасибо. - person Nick; 06.02.2013
comment
Я уже отметил это как ответ, потому что вопрос был конкретно о датах, я думал, что смогу адаптировать решение для дат к другим типам данных, но поскольку оно использует встроенные функции SQL для преобразования строки в дату и обратно, я не уверен... У меня есть другие типы данных, которые сохраняют числовые данные, такие как 1000, и мне нужно убрать запятые. попытка преобразовать 1000 в числовое значение не удалась из-за запятой. любые идеи о том, как исправить это? - person Nick; 06.02.2013
comment
@ Ник, да, SQL Server не понимает, что вы нашли одно значение без этого. - person Mikael Eriksson; 06.02.2013
comment
@Nick, тебе просто нужно изменить часть в перекрестном применении. Укажите varchar в качестве типа данных и выполните модификацию с помощью обычного материала tsql, который все еще находится в перекрестном применении. - person Mikael Eriksson; 06.02.2013