SQL 服务器中层次结构的递归克隆
Recursive clone of hierarchy in SQL Server
我在 table 中有一个层次结构:
Configuration
(
ConfigurationId int identity primary key,
Name nvarchar(100),
Value nvarchar(100),
ParentId` int foreign key referencing ConfigurationId
)
我的任务是克隆一个父级及其所有子级,子级保持结构。请记住,ConfigurationId
是同一性,它需要保持同一性,不一定从 1 开始。我使用与用于插入/更新的过程相同的过程,仅使用 IsClone
参数。
程序如下所示:
ALTER PROCEDURE [dbo].[Configuration_Save]
@ConfigurationId INT,
@Name NVARCHAR(500),
@Value NVARCHAR(500),
@ParentId INT,
@IsClone BIT
AS
BEGIN
IF @IsClone = 0
BEGIN
IF (@ConfigurationId = 0)
BEGIN
INSERT INTO [Configuration]([Name], [Value], [ParentId])
VALUES (@Name, @Value, @ParentId)
END
ELSE
BEGIN
UPDATE [Configuration]
SET [Name] = @Name,
[Value] = @Value,
ParentId = @ParentId
WHERE ConfigurationId = @ConfigurationId
END
END
ELSE -- IF IsClone = 1
BEGIN
DECLARE @SourceConfigid INT
SET @SourceConfigid = @ConfigurationId
DECLARE @ClonedConfigId INT
INSERT INTO [Configuration] ([Name], [Value], ParentId)
VALUES (@Name, @Value, NULL)
SET @ClonedConfigId = SCOPE_IDENTITY()
-- solution goes here
END
SELECT @ConfigurationId
END
当前数据如下所示:
ConfigurationId Name Value ParentId
-------------------------------------------------------
1 prod NULL NULL
2 Security NULL 1
3 SecurityKey NULL 2
4 Issuer NULL 2
5 Audience NULL 2
6 SyncServer NULL 1
7 Address NULL 6
8 SmtpClient NULL 1
9 Host NULL 8
10 Port NULL 8
11 EnableSsl NULL 8
12 Username NULL 8
13 Password NULL 8
14 FromEmail NULL 8
15 Proxy NULL 1
16 UseProxy NULL 15
17 ProxyAddress NULL 15
18 AddressList NULL 15
19 Report NULL 1
20 ApiUrl NULL 19
我希望能够通过插入一个名为 I通过执行存储过程输入并将行复制到当前行,唯一的区别是 ConfigurationId
是标识,ParentId
应根据新的 ConfigurationId
进行更改,同时保持层次结构。
所需数据如下所示:
ConfigurationId Name Value ParentId
------------------------------------------------
1 prod NULL NULL
2 Security NULL 1
3 SecurityKey NULL 2
4 Issuer NULL 2
5 Audience NULL 2
6 SyncServer NULL 1
7 Address NULL 6
8 SmtpClient NULL 1
9 Host NULL 8
10 Port NULL 8
11 EnableSsl NULL 8
12 Username NULL 8
13 Password NULL 8
14 FromEmail NULL 8
15 Proxy NULL 1
16 UseProxy NULL 15
17 ProxyAddress NULL 15
18 AddressList NULL 15
19 Report NULL 1
20 ApiUrl NULL 19
21 prod2 NULL NULL
22 Security NULL 21
23 SecurityKey NULL 22
24 Issuer NULL 22
25 Audience NULL 22
26 SyncServer NULL 21
27 Address NULL 26
28 SmtpClient NULL 21
29 Host NULL 28
30 Port NULL 28
31 EnableSsl NULL 28
32 Username NULL 28
33 Password NULL 28
34 FromEmail NULL 28
35 Proxy NULL 21
36 UseProxy NULL 35
37 ProxyAddress NULL 35
38 AddressList NULL 35
39 Report NULL 21
40 ApiUrl NULL 39
与嵌套游标、合并和调用过程/函数相比,我更喜欢 CTE 解决方案。我尝试了几个以相似名称列出的解决方案,但没有成功。
编辑 1:
示例数据的格式
编辑 2:
只能克隆根节点,这意味着只有 ParentId = NULL 的条目是克隆选项。
如有任何帮助,我们将不胜感激。
有许多可用的答案显示如何使用递归 CTE 附加一些路径信息。这是一个需要根据您的排序偏好进行调整的示例:
;with cteHierarchy AS (
SELECT ConfigurationId, NAme, Value, ParentId,
CAST(ConfigurationID AS varchar(255)) As HierarchyPath
FROM #Configuration WHERE ParentId IS NULL
UNION ALL
SELECT C.ConfigurationId, C.NAme, C.Value, C.ParentId,
--I prefer CONCAT(), but not sure of your SQL version
CAST(P.HierarchyPath + '.' + CAST(C.ConfigurationID AS varchar(255)) as varchar(255)) As HierarchyPath
FROM #Configuration C
JOIN cteHierarchy P ON C.ParentId = P.ConfigurationId
)
SELECT * FROM cteHierarchy Order By HierarchyPath
以下代码使用 CTE 和 update
来制作指定层次结构的副本。 CTE 递归地从根遍历到叶子,并提供一个 insert
来添加 "copies" 行。 insert
上的 output
子句生成 table 个修正对,其中包含每个新行的旧值和新值 ConfigurationId
。由于 output
子句只能访问插入的列值,我们 "borrow" 一个列 (Value
) 来存储旧的 ConfigurationId
值。然后使用 update
设置两列:更新 ParentId
值以引用复制的行,并从原始行恢复 Value
值。
注意,繁忙的工作应该包裹在事务中。它用于确保复制完成或没有落后者被遗忘,它需要防止其他会话看到不完整的结果或更改完成复制所需的旧 Value
数据。
-- Sample data.
declare @Configuration as Table (
ConfigurationId Int Identity,
Name NVarChar(100),
Value NVarChar(100),
ParentId Int );
insert into @Configuration ( Name, Value, ParentId ) values
( 'prod', NULL, NULL ),
( 'Security', NULL, 1 ),
( 'SecurityKey', NULL, 2 ),
( 'Issuer', NULL, 2 ),
( 'Audience', NULL, 2 ),
( 'SyncServer', NULL, 1 ),
( 'Address', NULL, 6 );
--8 SmtpClient NULL 1
--9 Host NULL 8
--10 Port NULL 8
--11 EnableSsl NULL 8
--12 Username NULL 8
--13 Password NULL 8
--14 FromEmail NULL 8
--15 Proxy NULL 1
--16 UseProxy NULL 15
--17 ProxyAddress NULL 15
--18 AddressList NULL 15
--19 Report NULL 1
--20 ApiUrl NULL 19
-- Raw sample data.
select * from @Configuration;
-- Tree sample data.
with Configuration as (
select ConfigurationId, Name, Value, ParentId,
Cast( Right( '0000' + Cast( ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) ) as Path
from @Configuration
where ParentId is NULL
union all
select CC.ConfigurationId, CC.Name, CC.Value, CC.ParentId,
Cast( Path + N'→' + Right( '0000' + Cast( CC.ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) )
from Configuration as PC inner join
@Configuration as CC on CC.ParentId = PC.ConfigurationId )
select *
from Configuration
order by Path;
-- Copy the tree.
declare @RootConfigurationId as Int = 1;
declare @Fixups as Table ( OriginalConfigurationId NVarChar(10), CopyConfigurationId Int );
-- NB: The isolation level needs to guarantee that the Value in the
-- source rows doesn't get changed whilst we fiddle about, nor do we want anyone else peeking.
begin transaction;
-- Copy the tree and save the new identity values.
-- We cheat and tuck the old ConfigurationId into the Value column so that the
-- output clause can save the original and copy ConfigurationId values for fixup.
with Configuration as (
select ConfigurationId, Name, Value, ParentId
from @Configuration
where ConfigurationId = @RootConfigurationId
union all
select CC.ConfigurationId, CC.Name, CC.Value, CC.ParentId
from Configuration as PC inner join
@Configuration as CC on CC.ParentId = PC.ConfigurationId )
insert into @Configuration ( Name, Value, ParentId )
output inserted.Value, inserted.ConfigurationId into @Fixups
select Name, Cast( ConfigurationId as NVarChar(10) ), ParentId
from Configuration as C;
-- Display the intermediate results.
select * from @Fixups;
select * from @Configuration;
-- Fix up the parentage and replace the original values.
update C
set C.ParentId = F2.CopyConfigurationId, Value = CV.Value
from @Configuration as C inner join -- New rows to be fixed.
@Fixups as F on F.CopyConfigurationId = C.ConfigurationId inner join -- New row identity values.
@Configuration as CV on CV.ConfigurationId = F.OriginalConfigurationId left outer join -- Original Value .
@Fixups as F2 on F2.OriginalConfigurationId = C.ParentId; -- Lookup the new ParentId , if any, for each row.
-- Raw sample data.
select * from @Configuration;
-- Tree sample data.
with Configuration as (
select ConfigurationId, Name, Value, ParentId,
Cast( Right( '0000' + Cast( ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) ) as Path
from @Configuration
where ParentId is NULL
union all
select CC.ConfigurationId, CC.Name, CC.Value, CC.ParentId,
Cast( Path + N'→' + Right( '0000' + Cast( CC.ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) )
from Configuration as PC inner join
@Configuration as CC on CC.ParentId = PC.ConfigurationId )
select *
from Configuration
order by Path;
commit transaction;
我在 table 中有一个层次结构:
Configuration
(
ConfigurationId int identity primary key,
Name nvarchar(100),
Value nvarchar(100),
ParentId` int foreign key referencing ConfigurationId
)
我的任务是克隆一个父级及其所有子级,子级保持结构。请记住,ConfigurationId
是同一性,它需要保持同一性,不一定从 1 开始。我使用与用于插入/更新的过程相同的过程,仅使用 IsClone
参数。
程序如下所示:
ALTER PROCEDURE [dbo].[Configuration_Save]
@ConfigurationId INT,
@Name NVARCHAR(500),
@Value NVARCHAR(500),
@ParentId INT,
@IsClone BIT
AS
BEGIN
IF @IsClone = 0
BEGIN
IF (@ConfigurationId = 0)
BEGIN
INSERT INTO [Configuration]([Name], [Value], [ParentId])
VALUES (@Name, @Value, @ParentId)
END
ELSE
BEGIN
UPDATE [Configuration]
SET [Name] = @Name,
[Value] = @Value,
ParentId = @ParentId
WHERE ConfigurationId = @ConfigurationId
END
END
ELSE -- IF IsClone = 1
BEGIN
DECLARE @SourceConfigid INT
SET @SourceConfigid = @ConfigurationId
DECLARE @ClonedConfigId INT
INSERT INTO [Configuration] ([Name], [Value], ParentId)
VALUES (@Name, @Value, NULL)
SET @ClonedConfigId = SCOPE_IDENTITY()
-- solution goes here
END
SELECT @ConfigurationId
END
当前数据如下所示:
ConfigurationId Name Value ParentId
-------------------------------------------------------
1 prod NULL NULL
2 Security NULL 1
3 SecurityKey NULL 2
4 Issuer NULL 2
5 Audience NULL 2
6 SyncServer NULL 1
7 Address NULL 6
8 SmtpClient NULL 1
9 Host NULL 8
10 Port NULL 8
11 EnableSsl NULL 8
12 Username NULL 8
13 Password NULL 8
14 FromEmail NULL 8
15 Proxy NULL 1
16 UseProxy NULL 15
17 ProxyAddress NULL 15
18 AddressList NULL 15
19 Report NULL 1
20 ApiUrl NULL 19
我希望能够通过插入一个名为 I通过执行存储过程输入并将行复制到当前行,唯一的区别是 ConfigurationId
是标识,ParentId
应根据新的 ConfigurationId
进行更改,同时保持层次结构。
所需数据如下所示:
ConfigurationId Name Value ParentId
------------------------------------------------
1 prod NULL NULL
2 Security NULL 1
3 SecurityKey NULL 2
4 Issuer NULL 2
5 Audience NULL 2
6 SyncServer NULL 1
7 Address NULL 6
8 SmtpClient NULL 1
9 Host NULL 8
10 Port NULL 8
11 EnableSsl NULL 8
12 Username NULL 8
13 Password NULL 8
14 FromEmail NULL 8
15 Proxy NULL 1
16 UseProxy NULL 15
17 ProxyAddress NULL 15
18 AddressList NULL 15
19 Report NULL 1
20 ApiUrl NULL 19
21 prod2 NULL NULL
22 Security NULL 21
23 SecurityKey NULL 22
24 Issuer NULL 22
25 Audience NULL 22
26 SyncServer NULL 21
27 Address NULL 26
28 SmtpClient NULL 21
29 Host NULL 28
30 Port NULL 28
31 EnableSsl NULL 28
32 Username NULL 28
33 Password NULL 28
34 FromEmail NULL 28
35 Proxy NULL 21
36 UseProxy NULL 35
37 ProxyAddress NULL 35
38 AddressList NULL 35
39 Report NULL 21
40 ApiUrl NULL 39
与嵌套游标、合并和调用过程/函数相比,我更喜欢 CTE 解决方案。我尝试了几个以相似名称列出的解决方案,但没有成功。
编辑 1: 示例数据的格式
编辑 2: 只能克隆根节点,这意味着只有 ParentId = NULL 的条目是克隆选项。
如有任何帮助,我们将不胜感激。
有许多可用的答案显示如何使用递归 CTE 附加一些路径信息。这是一个需要根据您的排序偏好进行调整的示例:
;with cteHierarchy AS (
SELECT ConfigurationId, NAme, Value, ParentId,
CAST(ConfigurationID AS varchar(255)) As HierarchyPath
FROM #Configuration WHERE ParentId IS NULL
UNION ALL
SELECT C.ConfigurationId, C.NAme, C.Value, C.ParentId,
--I prefer CONCAT(), but not sure of your SQL version
CAST(P.HierarchyPath + '.' + CAST(C.ConfigurationID AS varchar(255)) as varchar(255)) As HierarchyPath
FROM #Configuration C
JOIN cteHierarchy P ON C.ParentId = P.ConfigurationId
)
SELECT * FROM cteHierarchy Order By HierarchyPath
以下代码使用 CTE 和 update
来制作指定层次结构的副本。 CTE 递归地从根遍历到叶子,并提供一个 insert
来添加 "copies" 行。 insert
上的 output
子句生成 table 个修正对,其中包含每个新行的旧值和新值 ConfigurationId
。由于 output
子句只能访问插入的列值,我们 "borrow" 一个列 (Value
) 来存储旧的 ConfigurationId
值。然后使用 update
设置两列:更新 ParentId
值以引用复制的行,并从原始行恢复 Value
值。
注意,繁忙的工作应该包裹在事务中。它用于确保复制完成或没有落后者被遗忘,它需要防止其他会话看到不完整的结果或更改完成复制所需的旧 Value
数据。
-- Sample data.
declare @Configuration as Table (
ConfigurationId Int Identity,
Name NVarChar(100),
Value NVarChar(100),
ParentId Int );
insert into @Configuration ( Name, Value, ParentId ) values
( 'prod', NULL, NULL ),
( 'Security', NULL, 1 ),
( 'SecurityKey', NULL, 2 ),
( 'Issuer', NULL, 2 ),
( 'Audience', NULL, 2 ),
( 'SyncServer', NULL, 1 ),
( 'Address', NULL, 6 );
--8 SmtpClient NULL 1
--9 Host NULL 8
--10 Port NULL 8
--11 EnableSsl NULL 8
--12 Username NULL 8
--13 Password NULL 8
--14 FromEmail NULL 8
--15 Proxy NULL 1
--16 UseProxy NULL 15
--17 ProxyAddress NULL 15
--18 AddressList NULL 15
--19 Report NULL 1
--20 ApiUrl NULL 19
-- Raw sample data.
select * from @Configuration;
-- Tree sample data.
with Configuration as (
select ConfigurationId, Name, Value, ParentId,
Cast( Right( '0000' + Cast( ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) ) as Path
from @Configuration
where ParentId is NULL
union all
select CC.ConfigurationId, CC.Name, CC.Value, CC.ParentId,
Cast( Path + N'→' + Right( '0000' + Cast( CC.ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) )
from Configuration as PC inner join
@Configuration as CC on CC.ParentId = PC.ConfigurationId )
select *
from Configuration
order by Path;
-- Copy the tree.
declare @RootConfigurationId as Int = 1;
declare @Fixups as Table ( OriginalConfigurationId NVarChar(10), CopyConfigurationId Int );
-- NB: The isolation level needs to guarantee that the Value in the
-- source rows doesn't get changed whilst we fiddle about, nor do we want anyone else peeking.
begin transaction;
-- Copy the tree and save the new identity values.
-- We cheat and tuck the old ConfigurationId into the Value column so that the
-- output clause can save the original and copy ConfigurationId values for fixup.
with Configuration as (
select ConfigurationId, Name, Value, ParentId
from @Configuration
where ConfigurationId = @RootConfigurationId
union all
select CC.ConfigurationId, CC.Name, CC.Value, CC.ParentId
from Configuration as PC inner join
@Configuration as CC on CC.ParentId = PC.ConfigurationId )
insert into @Configuration ( Name, Value, ParentId )
output inserted.Value, inserted.ConfigurationId into @Fixups
select Name, Cast( ConfigurationId as NVarChar(10) ), ParentId
from Configuration as C;
-- Display the intermediate results.
select * from @Fixups;
select * from @Configuration;
-- Fix up the parentage and replace the original values.
update C
set C.ParentId = F2.CopyConfigurationId, Value = CV.Value
from @Configuration as C inner join -- New rows to be fixed.
@Fixups as F on F.CopyConfigurationId = C.ConfigurationId inner join -- New row identity values.
@Configuration as CV on CV.ConfigurationId = F.OriginalConfigurationId left outer join -- Original Value .
@Fixups as F2 on F2.OriginalConfigurationId = C.ParentId; -- Lookup the new ParentId , if any, for each row.
-- Raw sample data.
select * from @Configuration;
-- Tree sample data.
with Configuration as (
select ConfigurationId, Name, Value, ParentId,
Cast( Right( '0000' + Cast( ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) ) as Path
from @Configuration
where ParentId is NULL
union all
select CC.ConfigurationId, CC.Name, CC.Value, CC.ParentId,
Cast( Path + N'→' + Right( '0000' + Cast( CC.ConfigurationId as NVarChar(4) ), 4 ) as NVarChar(1024) )
from Configuration as PC inner join
@Configuration as CC on CC.ParentId = PC.ConfigurationId )
select *
from Configuration
order by Path;
commit transaction;