Учитывая следующий XML в столбце таблицы:
<UserFields>
<UserField>
<Name>UserName</Name>
<Value>test_user</Value>
</UserField>
<UserField>
<Name>Age</Name>
<Value>24</Value>
</UserField>
<UserField>
<Name>Password</Name>
<Value>test1234!</Value>
</UserField>
<UserField>
<Name>AgentPassword</Name>
<Value>1234Test!</Value>
</UserField>
</UserFields>
<ActionAttributes>
<Action>
<Name>LoginPassword</Name>
<Value>Test123!</Value>
</Action>
</ActionAttributes>
Я хочу запросить XML, чтобы найти любой элемент, имеющий узел <Name>
, содержащий text()
слово password
, и заменить текст узла <Value>
звездочками. В примере будет три замены, и я получу следующий XML.
<UserFields>
<UserField>
<Name>UserName</Name>
<Value>test_user</Value>
</UserField>
<UserField>
<Name>Age</Name>
<Value>24</Value>
</UserField>
<UserField>
<Name>Password</Name>
<Value>********</Value>
</UserField>
<UserField>
<Name>AgentPassword</Name>
<Value>********</Value>
</UserField>
</UserFields>
<ActionAttributes>
<Action>
<Name>LoginPassword</Name>
<Value>********</Value>
</Action>
</ActionAttributes>
Я смог придумать это:
SET @MyDoc.modify('
replace value of (//UserField[Name = "Password"]/Value/text())[1]
with "********"
');
Проблема с этим кодом:
<Name>
, поэтому находит только «пароль», а не другие варианты.По-английски я бы сказал:
«Для любого узла, у которого есть дочерние узлы <Name>
и <Value>
. Если дочерний узел <Name>
содержит слово «пароль», замените текст дочернего узла <Value>
звездочками».
Опция or
противоречит цели — попытка избежать необходимости перечислять все случаи, когда пароль может быть в имени. Опция contains
выдает ошибку [modify()]: 'contains()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'
Я считаю, что это конструктивное ограничение реализации XML mssql. or
, вероятно, тоже не будет работать, поскольку [1]
в конце может ограничивать обновление 1 и только 1 узлом. То же самое, если бы было несколько узлов типа Password
.
Не думайте, что метод изменения xml сервера sql может заменить несколько узлов, вам, вероятно, придется проанализировать и воссоздать xml с заменой полей вручную. Однако, как обычно, вопрос касается не той проблемы: зачем работать над манипулированием XML, когда вы можете работать над процессом создания такого XML
@siggemannen - Легче поймать его в одном месте, чем пытаться прочесать систему, которая была засыпана за последние 15 лет, и найти все возможные комбинации вызовов, которые можно было бы вставить. :-)
Ну, конечно, вы не храните XML, но все равно уничтожаете его
Выражение XPath должно определять один узел
Выражение1
Идентифицирует узел, значение которого должно быть обновлено. Он должен идентифицировать только один узел. То есть Expression1 должен быть статическим синглтоном. Если XML является типизированным, тип узла должен быть простым. При выборе нескольких узлов возникает ошибка.
Использование переменных SQL может быть альтернативой, позволяющей избежать жесткого кодирования предиката в выражении.
DECLARE @Name VARCHAR(50) = 'Password';
DECLARE @NewValue VARCHAR(50) = '********';
SET @MyDoc.modify('
replace value of (//UserField[Name = sql:variable("@Name")]/Value/text())[1]
with sql:variable("@NewValue")
');
Итерация возможных значений
DECLARE @myDoc XML;
DECLARE @Name VARCHAR(50);
DECLARE @parent VARCHAR(50);
DECLARE @NewValue VARCHAR(50) = '********';
DECLARE @rowid int;
DECLARE @tnames TABLE(
rowid iNT,
names VARCHAR(50),
parent VARCHAR(50)
);
SET @myDoc = '<root>
<UserFields>
<UserField>
<Name>UserName</Name>
<Value>test_user</Value>
</UserField>
<UserField>
<Name>Age</Name>
<Value>24</Value>
</UserField>
<UserField>
<Name>Password</Name>
<Value>test1234!</Value>
</UserField>
<UserField>
<Name>AgentPassword</Name>
<Value>1234Test!</Value>
</UserField>
</UserFields>
<ActionAttributes>
<Action>
<Name>LoginPassword</Name>
<Value>Test123!</Value>
</Action>
</ActionAttributes>
</root>
';
--SELECT @myDoc;
INSERT INTO @tnames (rowid, names, parent)
SELECT ROW_NUMBER() OVER (ORDER BY t.xname) AS ROWID, T.xname.value('(Name/text())[1]', 'varchar(max)') as names, T.xname.value('(local-name(.))[1]', 'varchar(max)') as parent
FROM @myDoc.nodes('//*[Name[contains(., "Password")]]') T(xname);
SELECT * FROM @tnames;
WHILE (SELECT COUNT(1) FROM @tnames) > 0
BEGIN
SELECT @rowid = rowid, @Name = names, @parent= parent FROM @tnames;
SET @MyDoc.modify('
replace value of (//*[local-name() = sql:variable("@parent")][Name = sql:variable("@Name")]/Value/text())[1]
with sql:variable("@NewValue")
');
DELETE FROM @tnames where rowid = @rowid;
--SELECT ROW_NUMBER() OVER (ORDER BY t.xname) AS ROWID, T.xname.value('(Name/text())[1]', 'varchar(max)') as names, T.xname.value('(Value/text())[1]', 'varchar(max)') as value
--FROM @myDoc.nodes('//*[local-name() = sql:variable("@parent")][Name = sql:variable("@Name")]') T(xname);
END
Как многие уже упоминали, SQL Server не поддерживает обновление нескольких элементов XML за один раз.
Попробуйте следующее решение, основанное на XQuery SQL Server и его выражении FLWOR.
SQL №1
-- DDL and sample data population, start
DECLARE @tbl TABLE (id INT IDENTITY PRIMARY KEY, xml_data XML);
INSERT INTO @tbl (xml_data) VALUES
(N'<UserFields>
<UserField>
<Name>UserName</Name>
<Value>test_user</Value>
</UserField>
<UserField>
<Name>Age</Name>
<Value>24</Value>
</UserField>
<UserField>
<Name>Password</Name>
<Value>test1234!</Value>
</UserField>
<UserField>
<Name>AgentPassword</Name>
<Value>1234Test!</Value>
</UserField>
</UserFields>');
-- DDL and sample data population, end
-- just to see
SELECT *
, xml_data.query('<UserFields>{
for $x in /UserFields/*
return element UserField {
if (contains(($x/Name/text())[1],"Password")) then
($x/*[local-name() != "Value"], element Value {"********"})
else $x/*
}
}</UserFields>') AS modified_xml
FROM @tbl;
-- real deal
UPDATE @tbl
SET xml_data = xml_data.query('<UserFields>{
for $x in /UserFields/*
return element UserField {
if (contains(($x/Name/text())[1],"Password")) then
($x/*[local-name() != "Value"], element Value {"********"})
else $x/*
}
}</UserFields>');
-- test
SELECT * FROM @tbl;
SQL № 2
Для измененного XML в вопросе мы можем использовать другой подход.
-- DDL and sample data population, start
DECLARE @tbl TABLE (id INT IDENTITY PRIMARY KEY, xml_data XML);
INSERT INTO @tbl (xml_data) VALUES
(N'<UserFields>
<UserField>
<Name>UserName</Name>
<Value>test_user</Value>
</UserField>
<UserField>
<Name>Age</Name>
<Value>24</Value>
</UserField>
<UserField>
<Name>Password</Name>
<Value>test1234!</Value>
</UserField>
<UserField>
<Name>AgentPassword</Name>
<Value>1234Test!</Value>
</UserField>
</UserFields>
<ActionAttributes>
<Action>
<Name>LoginPassword</Name>
<Value>Test123!</Value>
</Action>
</ActionAttributes>');
-- DDL and sample data population, end
-- before
SELECT * FROM @tbl;
DECLARE @ReplaceWith VARCHAR(10) = '*********';
DECLARE @UPDATE_STATUS BIT = 1;
WHILE @UPDATE_STATUS > 0
BEGIN
UPDATE t
SET xml_data.modify('replace value of
(/*/*[contains((Name/text())[1], "Password") and Value/text()!=sql:variable("@ReplaceWith")]/Value/text())[1]
with sql:variable("@ReplaceWith")')
FROM @tbl AS t
WHERE xml_data.exist('/*/*[contains((Name/text())[1], "Password") and Value/text()!=sql:variable("@ReplaceWith")]')=1;
SET @UPDATE_STATUS = @@ROWCOUNT;
PRINT @UPDATE_STATUS;
END;
-- after
SELECT * FROM @tbl;
SQL №2 сделал свою работу! Спасибо тонны. Чего бы это ни стоило, я немного изменил код, чтобы включить замену значений независимо от регистра слова «пароль». ...[contains(upper-case(Name/text())[1]), "PASSWORD") ...
@RHarris, приятно слышать, что предложенное решение работает для вас!
Есть ли способ заставить его проверять на любом уровне? Прямо сейчас синтаксис (/*/*[contains...
заставляет его искать на два уровня глубже. Иногда я нахожу XML, в котором мы можем иметь глубину 3 или 1 уровень.
Да, это возможно, переключившись на следующее выражение XPath: (//*[contains...
в обоих местах.
Также, пожалуйста, проголосуйте за мое предложение в Microsoft: Feedback.azure.com/d365community/idea/… И не пропустите мои многочисленные комментарии.
Попробуйте
//UserField[contains(Name,"Password")]
или//UserField[Name = "Password" or Name = "AgentPassword"]