I have several related tables that I want to be able to duplicate some of the rows while updating the references.
I want to duplicate a row in Table1, and all of
(ab)use MERGE with OUTPUT
clause.
MERGE
can INSERT
, UPDATE
and DELETE
rows. In our case we need only to INSERT
. 1=0
is always false, so the NOT MATCHED BY TARGET
part is always executed. In general, there could be other branches, see docs. WHEN MATCHED
is usually used to UPDATE
; WHEN NOT MATCHED BY SOURCE
is usually used to DELETE
, but we don't need them here.
This convoluted form of MERGE
is equivalent to simple INSERT
, but unlike simple INSERT
its OUTPUT
clause allows to refer to the columns that we need.
I will write down the definitions of table explicitly. Each primary key in the tables is IDENTITY
. I've configured foreign keys as well.
Baskets
CREATE TABLE [dbo].[Baskets](
[BasketId] [int] IDENTITY(1,1) NOT NULL,
[BasketName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Baskets] PRIMARY KEY CLUSTERED
(
[BasketId] ASC
)
Fruits
CREATE TABLE [dbo].[Fruits](
[FruitId] [int] IDENTITY(1,1) NOT NULL,
[BasketId] [int] NOT NULL,
[FruitName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Fruits] PRIMARY KEY CLUSTERED
(
[FruitId] ASC
)
ALTER TABLE [dbo].[Fruits] WITH CHECK
ADD CONSTRAINT [FK_Fruits_Baskets] FOREIGN KEY([BasketId])
REFERENCES [dbo].[Baskets] ([BasketId])
ALTER TABLE [dbo].[Fruits] CHECK CONSTRAINT [FK_Fruits_Baskets]
Properties
CREATE TABLE [dbo].[Properties](
[PropertyId] [int] IDENTITY(1,1) NOT NULL,
[FruitId] [int] NOT NULL,
[PropertyText] [varchar](50) NOT NULL,
CONSTRAINT [PK_Properties] PRIMARY KEY CLUSTERED
(
[PropertyId] ASC
)
ALTER TABLE [dbo].[Properties] WITH CHECK
ADD CONSTRAINT [FK_Properties_Fruits] FOREIGN KEY([FruitId])
REFERENCES [dbo].[Fruits] ([FruitId])
ALTER TABLE [dbo].[Properties] CHECK CONSTRAINT [FK_Properties_Fruits]
Copy Basket
At first copy one row in Baskets
table and use SCOPE_IDENTITY
to get the generated ID
.
BEGIN TRANSACTION;
-- Parameter of the procedure. What basket to copy.
DECLARE @VarOldBasketID int = 1;
-- Copy Basket, one row
DECLARE @VarNewBasketID int;
INSERT INTO [dbo].[Baskets] (BasketName)
VALUES ('Friends Basket');
SET @VarNewBasketID = SCOPE_IDENTITY();
Copy Fruits
Then copy Fruits
using MERGE
and remember a mapping between old and new IDs in a table variable.
-- Copy Fruits, multiple rows
DECLARE @FruitIDs TABLE (OldFruitID int, NewFruitID int);
MERGE INTO [dbo].[Fruits]
USING
(
SELECT
[FruitId]
,[BasketId]
,[FruitName]
FROM [dbo].[Fruits]
WHERE [BasketId] = @VarOldBasketID
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT
([BasketId]
,[FruitName])
VALUES
(@VarNewBasketID
,Src.[FruitName])
OUTPUT Src.[FruitId] AS OldFruitID, inserted.[FruitId] AS NewFruitID
INTO @FruitIDs(OldFruitID, NewFruitID)
;
Copy Properties
Then copy Properties
using remembered mapping between old and new Fruit IDs.
-- Copy Properties, many rows
INSERT INTO [dbo].[Properties] ([FruitId], [PropertyText])
SELECT
F.NewFruitID
,[dbo].[Properties].PropertyText
FROM
[dbo].[Properties]
INNER JOIN @FruitIDs AS F ON F.OldFruitID = [dbo].[Properties].FruitId
;
Check results, change rollback to commit once you confirmed that the code works correctly.
SELECT * FROM [dbo].[Baskets];
SELECT * FROM [dbo].[Fruits];
SELECT * FROM [dbo].[Properties];
ROLLBACK TRANSACTION;
Why dont you join on the FruitName to get a table with old and new FruitId's? Considering information would be added at the same time.... it may not be the best option but you wont be using any cycles.
INSERT INTO BASKET(BASKETNAME)
VALUES ('COPY BASKET')
DECLARE @iBasketId int
SET @iBasketId = @@SCOPE_IDENTITY;
insert into Fruit (BasketId, FruitName)
select @iBasketId, FruitName
from Fruit
where BasketId = @originalBasket
declare @tabFruit table (originalFruitId int, newFruitId int)
insert into @tabFruit (originalFruitId, newFruitId)
select o.FruitId, n.FruitId
from (SELECT FruitId, FruitName from Fruit where BasketId = @originalBasket) as o
join (SELECT FruitId, FruitName from Fruit where BasketId = @newBasket) as n
on o.FruitName = n.FruitName
insert into Property (FruitId, PropertyText)
select NewFruitId, PropertyText
from Fruit f join @tabFruit t on t.originalFruitId = f.FruitId
I had the same need as the OP: cloning sql server tables where they are hierarchical sql server tables that contain foreign keys to one another. Or in other words, cloning sql server tables that have parent-child relationships.
Starting with @Tony_O 's answer/SQL, I converted it to my needs but discovered that the last line '..from Fruit f join..' should be '..from Property f join..'. Also, @newBasket should be @iBasketId.
So along with some other minor housekeeping fixes I found were needed for it to execute, plus using @Vladimir_Baranov 's DDL (with some missing parenthesis added), as well as making both of their SQL's object names consistent, since I had done the work I thought I would post it as a refinement of their work that will allow someone to to quickly test whether this solution solves their need. Just do 'find/replace' of the table names here with values from yours.
And note that if your Properties table has more fields than the single 'PropertyText' one in this example, just make sure to join on those additional fields as noted in the comment in the script.
-----------------
--create tables--
-----------------
--Baskets
CREATE TABLE [dbo].[Baskets](
[BasketId] [int] IDENTITY(1,1) NOT NULL,
[BasketName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Baskets] PRIMARY KEY CLUSTERED
(
[BasketId] ASC
)
)
--Fruits
CREATE TABLE [dbo].[Fruits](
[FruitId] [int] IDENTITY(1,1) NOT NULL,
[BasketId] [int] NOT NULL,
[FruitName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Fruits] PRIMARY KEY CLUSTERED
(
[FruitId] ASC
)
)
ALTER TABLE [dbo].[Fruits] WITH CHECK
ADD CONSTRAINT [FK_Fruits_Baskets] FOREIGN KEY([BasketId])
REFERENCES [dbo].[Baskets] ([BasketId])
ALTER TABLE [dbo].[Fruits] CHECK CONSTRAINT [FK_Fruits_Baskets]
--Properties
CREATE TABLE [dbo].[Properties](
[PropertyId] [int] IDENTITY(1,1) NOT NULL,
[FruitId] [int] NOT NULL,
[PropertyText] [varchar](50) NOT NULL,
CONSTRAINT [PK_Properties] PRIMARY KEY CLUSTERED
(
[PropertyId] ASC
)
)
ALTER TABLE [dbo].[Properties] WITH CHECK
ADD CONSTRAINT [FK_Properties_Fruits] FOREIGN KEY([FruitId])
REFERENCES [dbo].[Fruits] ([FruitId])
ALTER TABLE [dbo].[Properties] CHECK CONSTRAINT [FK_Properties_Fruits]
-------------------------
--Fill tables with data--
-------------------------
SET IDENTITY_INSERT [dbo].[Baskets] ON
GO
INSERT [dbo].[Baskets] ([BasketId], [BasketName]) VALUES (1, N'Home Basket')
GO
SET IDENTITY_INSERT [dbo].[Baskets] OFF
GO
SET IDENTITY_INSERT [dbo].[Fruits] ON
GO
INSERT [dbo].[Fruits] ([FruitId], [BasketId], [FruitName]) VALUES (1, 1, N'Apple')
GO
INSERT [dbo].[Fruits] ([FruitId], [BasketId], [FruitName]) VALUES (2, 1, N'Orange')
GO
SET IDENTITY_INSERT [dbo].[Fruits] OFF
GO
SET IDENTITY_INSERT [dbo].[Properties] ON
GO
INSERT [dbo].[Properties] ([PropertyId], [FruitId], [PropertyText]) VALUES (1, 2, N'is juicy')
GO
INSERT [dbo].[Properties] ([PropertyId], [FruitId], [PropertyText]) VALUES (2, 2, N'hard to peel')
GO
INSERT [dbo].[Properties] ([PropertyId], [FruitId], [PropertyText]) VALUES (3, 1, N'is red')
GO
SET IDENTITY_INSERT [dbo].[Properties] OFF
GO
--------------------------------------------------------------
-- Copy 'Home Basket' to new basket named 'COPY BASKET' --
-- i.e., Copy Basket (and all fruits and their fruit properties) having basket id
-- @origBasketId to a new basket with name 'COPY BASKET'.
--------------------------------------------------------------
DECLARE @originalBasket int
select @originalBasket = 1
begin tran
INSERT INTO BASKETS(BASKETNAME)
VALUES ('COPY BASKET')
DECLARE @newBasketId int
SET @newBasketId = SCOPE_IDENTITY();
insert into Fruits (BasketId, FruitName)
select @newBasketId, FruitName
from Fruits
where BasketId = @originalBasket
declare @tabFruit table (originalFruitId int, newFruitId int)
insert into @tabFruit (originalFruitId, newFruitId)
select o.FruitId, n.FruitId
from (SELECT FruitId, FruitName from Fruits where BasketId = @originalBasket) as o
join (SELECT FruitId, FruitName from Fruits where BasketId = @newBasketId) as n
on o.FruitName = n.FruitName --if your table equivalent to Fruits has other fields, match on those as well here.
insert into Properties (FruitId, PropertyText)
select NewFruitId, PropertyText
from Properties p join @tabFruit t on t.originalFruitId = p.FruitId
commit tran
---------------
--See results--
---------------
select *
from dbo.Baskets b inner join dbo.Fruits f on b.BasketId=f.BasketId
inner join properties p on p.FruitId=f.FruitId
order by b.BasketId, f.FruitId, p.PropertyId