Reputation: 13278
I have a table and I want to update one of its varchar fields based on the values in an XML parameter.
I have the following table:
ID Constraint_Value
1 (OldVal_1) (OldVal_2)
2 (OldVal_2) (OldVal_1)
and I want to use the following XML to update the Constraint_Value
field:
<qaUpdates>
<qaUpdate><old>OldVal_1</old><new>NewVal_1</new></qaUpdate>
<qaUpdate><old>OldVal_2</old><new>NewVal_2</new></qaUpdate>
</qaUpdates>
After the update, I am aiming for the following:
ID Constraint_Value
1 (NewVal_1) (NewVal_2)
2 (NewVal_2) (NewVal_1)
The following SQL illustrates my problem (which you can run in SQL Management Studio without any set up) :
IF OBJECT_ID('tempdb..#tmpConstraint') IS NOT NULL DROP TABLE #tmpConstraint
GO
CREATE TABLE tempdb..#tmpConstraint ( constraint_id INT PRIMARY KEY, constraint_value varchar(256) )
GO
insert into #tmpConstraint
values (1, '(OldVal_1) (OldVal_2)')
insert into #tmpConstraint
values (2, '(OldVal_2) (OldVal_1)')
select * from #tmpConstraint
declare @myXML XML
set @myXML = N'<qaUpdates>
<qaUpdate><old>OldVal_1</old><new>NewVal_1</new></qaUpdate>
<qaUpdate><old>OldVal_2</old><new>NewVal_2</new></qaUpdate>
</qaUpdates>'
update c
set constraint_value = REPLACE(constraint_value, Child.value('(old)[1]', 'varchar(50)'), Child.value('(new)[1]', 'varchar(50)'))
from #tmpConstraint c
cross join @myXML.nodes('/qaUpdates/qaUpdate') as N(Child)
select * from #tmpConstraint
This gives the results:
(Before)
1 (OldVal_1) (OldVal_2)
2 (OldVal_2) (OldVal_1)
(After)
1 (NewVal_1) (OldVal_2)
2 (OldVal_2) (NewVal_1)
As you can see just OldVal_1
has been updated. OldVal_2
has remained the same.
How do I update the field with all the elements specified in the xml parameter?
Upvotes: 2
Views: 1241
Reputation: 1
I realize this has already been answered but I was curious to see if there was a way to do this without using a cte. At any rate the bigger problem is really that you are storing 2 pieces of data in the same column/row. This coupled with the fact that you cannot update the same row twice within a single update statement is causing your issues. Anyhow my approach was this( I apologize ahead of time for complexity):
DECLARE @tmpConstraint TABLE (
constraint_id INT PRIMARY KEY
,constraint_value VARCHAR(256)
)
INSERT INTO @tmpConstraint
VALUES (
1
,'(OldVal_1) (OldVal_2)'
)
INSERT INTO @tmpConstraint
VALUES (
2
,'(OldVal_2) (OldVal_1)'
)
INSERT INTO @tmpConstraint
VALUES (
3
,'(OldVal_3) (OldVal_21) (OldVal_1)'
)
DECLARE @myXML XML
SET @myXML = N'<qaUpdates> <qaUpdate><old>OldVal_1</old><new>NewVal_1</new></qaUpdate> <qaUpdate><old>OldVal_2</old><new>NewVal_2</new></qaUpdate> </qaUpdates>'
SELECT *
FROM @tmpConstraint
UPDATE C
SET constraint_value = c.New_Val
FROM (
SELECT Constraint_ID UpdID
,Constraint_value
,STUFF((
SELECT (' ' + New_value)
FROM (
--Converts XML into a Table effectively splitting the string
SELECT constraint_id
,t.value('.', 'varchar(200)') Current_value
,Coalesce(Nullif('(' + new + ')', '()'), t.value('.', 'varchar(200)')) New_Value
FROM
--Converts single column into an xml document to split rows. Uses a blank space as the identifer of rows
(
SELECT constraint_id
,convert(XML, ('<R>' + replace(constraint_value, ' ', '</R><R>')) + '</R>') xmldoc
FROM @tmpConstraint
) AS a
CROSS APPLY a.xmldoc.nodes('./R') AS b(t)
--Join to table containing proposed changes based on value to change
LEFT JOIN (
SELECT Child.value('./old[1]', 'varchar(100)') old
,Child.value('./new[1]', 'varchar(100)') new
FROM @myXML.nodes('/qaUpdates/qaUpdate') AS N(Child
)
) q2 ON '(' + old + ')' = t.value('.', 'varchar(200)')
) Modified WHERE Modified.constraint_id = base.constraint_id FOR XML path(''))
,1,1,'') New_Val
FROM @tmpConstraint Base ) c
SELECT *
FROM @tmpConstraint
It looks alot messier than it is and could be cleaned up if you had some UDF's in there. But basicaly I break your multivalue column into multiple rows. Turning this
1 (OldVal_1) (OldVal_2)
2 (OldVal_2) (OldVal_1)
3 (OldVal_3) (OldVal_21) (OldVal_1)
into this
1 (OldVal_1)
1 (OldVal_2)
2 (OldVal_2)
2 (OldVal_1)
3 (OldVal_3)
3 (OldVal_21)
3 (OldVal_1)
I do the same with the xml file. Turning it into a Key data pair set like this
OldVal_1 NewVal_1
OldVal_2 NewVal_2
Join that with the table created before to get this (duplicating the orignal value where there was no replacement determined)
1 (OldVal_1) (NewVal_1)
1 (OldVal_2) (NewVal_2)
2 (OldVal_2) (NewVal_2)
2 (OldVal_1) (NewVal_1)
3 (OldVal_3) (OldVal_3)
3 (OldVal_21) (OldVal_21)
3 (OldVal_1) (NewVal_1)
Recombine the seperated rows into a single string again grouped by constraint id like so:
1 (OldVal_1) (OldVal_2) (NewVal_1) (NewVal_2)
2 (OldVal_2) (OldVal_1) (NewVal_2) (NewVal_1)
3 (OldVal_3) (OldVal_21) (OldVal_1) (OldVal_3) (OldVal_21) (NewVal_1)
Then use that in my from statement to update the data in the orignal table. Anyhow I realize the code could be cleaned up a bit and probably even simplified but thats the concept I came up with. Really the bigger problem is how you are storing this data. But your circumstances are unknown to me so Ill reserve judgement.
Upvotes: 0
Reputation: 18379
Making use of a recursive cte allows me to get the result you're looking for. As the following shows. But att least its not a cursor/while-loop ;)
declare @tmpConstraint table (ID int , Constraint_Value varchar(256))
insert into @tmpConstraint values
(1, '(OldVal_1) (OldVal_2)'),
(2, '(OldVal_2) (OldVal_1)')
declare @myXML XML
set @myXML = N'<qaUpdates>
<qaUpdate><old>OldVal_1</old><new>NewVal_1</new></qaUpdate>
<qaUpdate><old>OldVal_2</old><new>NewVal_2</new></qaUpdate>
</qaUpdates>'
declare @xmlData table (oldValue varchar(256), newValue varchar(256))
insert into @xmlData
select
oldValue = Child.value('(old)[1]', 'varchar(50)'),
newValue = Child.value('(new)[1]', 'varchar(50)')
from @myXML.nodes('/qaUpdates/qaUpdate') as N(Child)
The above was just setup for the following.
;with cte (ID, Constraint_Value, CLevel)
as
(
select c.ID, c.Constraint_Value, 1
from @tmpConstraint c
union all
select p.ID, cast(replace(p.Constraint_Value, x.oldValue, x.newValue) as varchar(256)), p.CLevel + 1
from cte p
join @xmlData x on p.Constraint_Value like '%' + x.oldValue + '%'
)
update c
set c.Constraint_Value = t.Constraint_Value
from @tmpConstraint c
join (
select
*,
rn = row_number() over (partition by ID order by CLevel desc)
from cte
) t on t.ID = c.ID and rn = 1
select * from @tmpConstraint
Upvotes: 2
Reputation: 3771
I think the issue here has nothing to do with XML. It's that a single UPDATE will only update each row once, regardless of how many joined rows exist. I think you could add a WHERE clause and a WHILE loop to get all of your substitutions:
WHILE @@ROWCOUNT>0
BEGIN
update c
set constraint_value = REPLACE(constraint_value, Child.value('(old)[1]', 'varchar(50)'), Child.value('(new)[1]', 'varchar(50)'))
from #tmpConstraint c
cross join @myXML.nodes('/qaUpdates/qaUpdate') as N(Child)
WHERE constraint_value LIKE '%' + Child.value('(old)[1]', 'varchar(50)') + '%'
END
Just be sure that this follows a statement that sets @@RowCount.
Upvotes: 1