复制分层数据时保留父子关系
Preserve parent-child relationships when copying hierarchical data
我们有一个 table 表示与实体(称之为项目)关联的值树,其中 ParentID 列指的是一行父项的 id 列。 id 列是一个自动递增的 IDENTITY 列和主键。根节点的 ParentID 为 0。
我们希望能够克隆给定项目的数据,并让生成的 ParentID 引用复制值的适当新 ID,其方式满足示例下方描述的限制。
例如复制下面ProjectID 611的数据table:
id ProjectID Value ParentID
--------------------------------------------------
1 611 Animal 0
2 611 Frog 1
13 611 Cow 1
14 611 Jersey Cow 13
25 611 Plant 0
29 611 Tree 25
31 611 Oak 29
应该导致:
id ProjectID Value ParentID
--------------------------------------------------
1 611 Animal 0
2 611 Frog 1
13 611 Cow 1
14 611 Jersey Cow 13
25 611 Plant 0
29 611 Tree 25
31 611 Oak 29
32 612 Animal 0
33 612 Frog 32
34 612 Cow 32
35 612 Jersey Cow 34
36 612 Plant 0
37 612 Tree 36
38 612 Oak 37
限制:
- 解决方案必须适用于 SQL Server 2005。也就是说,我们不能使用 MERGE(唉)。
- 我们不会对 table 做出关于 ID 或它们与 ParentID 的比较的假设;例如,该解决方案原则上应适用于 ids/ParentIDs 是 uniqueid。
- 我们不想在 table 中添加额外的列。 (我当前的解决方案添加了一个 "OldId" 列,复制过程在复制行时设置了该列。所以我目前正在使用 INSERT-SELECT 和 UPDATE-FROM 的组合,加入 ParentID 上的 OldId 列列以获取新 ID。)我们宁愿不使用 OldId 列来填充我们所有的分层 table 只是为了支持此复制操作。
- 解决方案必须具有合理的性能;我最初的解决方案是一组复杂的递归函数调用和循环,一次处理一个项目。我很快放弃了那条路线!
您可以通过将 MAX(ID)
添加到旧的 ParentID
来获得 ParentID
。
DECLARE @projectID INT
SET @projectID = 611
SET IDENTITY_INSERT YourTable ON
BEGIN TRANSACTION
DECLARE @maxID INT
SELECT @maxID= MAX(ID) FROM YourTable WITH (UPDLOCK,HOLDLOCK)
INSERT INTO YourTable(ID, ProjectID, Value, ParentID)
SELECT
ID + @maxID,
ProjectId + 1,
Value,
CASE
WHEN ParentID > 0 THEN ParentID + @maxID
ELSE 0
END
FROM YourTable WITH (UPDLOCK,HOLDLOCK)
WHERE
ProjectID = @projectID
COMMIT TRANSACTION
SET IDENTITY_INSERT YourTable OFF
您应该使用事务来锁定 table。您还可以添加 locking hints.
CTE 与 MERGE
配合使用效果很好,但在 SQL Server 2005 中存在问题。对于之前的误导性评论深表歉意。
下面展示了如何克隆一个项目(有多棵树)并修复父系以将新林与旧林分开。请注意,它不依赖于 Id 的任何特定排列,例如它们不需要是密集的、单调递增的,...。
-- Sample data.
declare @Projects as Table
( Id Int Identity, ProjectId Int, Value VarChar(16), ParentId Int Null );
insert into @Projects ( ProjectId, Value, ParentId ) values
( 611, 'Animal', 0 ),
( 611, 'Frog', 1 ),
( 611, 'Cow', 1 ),
( 611, 'Jersey Cow', 3 ),
( 611, 'Plant', 0 ),
( 611, 'Tree', 5 ),
( 611, 'Oak', 6 );
-- Display the raw data.
select * from @Projects;
-- Display the forest.
with IndentedProjects ( Id, ProjectId, Value, ParentId, Level, Path ) as
( -- Start with the top level rows.
select Id, ProjectId, Value, ParentId, 0, Convert( VarChar(1024), Right( '000' + Convert( VarChar(4), Id ), 4 ) )
from @Projects
where ParentId = 0
union all
-- Add the children one level at a time.
select P.Id, P.ProjectId, P.Value, P.ParentId, IP.Level + 1, Convert( VarChar(1024), IP.Path + '<' + Right( '000' + Convert( VarChar(4), P.Id ), 4 ) )
from IndentedProjects as IP inner join
@Projects as P on P.ParentId = IP.Id
)
select Space( Level * 2 ) + Value as [IndentedValue], Id, ProjectId, Value, ParentId, Level, Path
from IndentedProjects
order by Path;
-- Clone the project.
declare @OldProjectId as Int = 611;
declare @NewProjectId as Int = 42;
declare @Fixups as Table ( OldId Int, [NewId] Int );
begin transaction -- With suitable isolation since the hierarchy will be invalid until we apply the fixups!
insert into @Projects
output Inserted.ParentId, Inserted.Id
into @Fixups
select @NewProjectId, Value, Id -- Note that we save the old Id in the new ParentId.
from @Projects as P
where ProjectId = @OldProjectId;
-- Apply the fixups.
update PNew
set ParentId = IsNull( FNew.[NewId], 0 )
-- Output the fixups just to show what is going on.
output Deleted.Id, Deleted.ParentId as [ParentIdBeforeFixup], Inserted.ParentId as [ParentIdAfterFixup]
from @Fixups as F inner join
@Projects as PNew on PNew.Id = F.[NewId] inner join -- Rows we need to fix.
@Fixups as FOld on FOld.OldId = PNew.ParentId inner join
@Projects as POld on POld.Id = FOld.OldId left outer join
@Fixups as FNew on FNew.OldId = POld.ParentId;
commit transaction;
-- Display the forest.
with IndentedProjects ( Id, ProjectId, Value, ParentId, Level, Path ) as
( -- Start with the top level rows.
select Id, ProjectId, Value, ParentId, 0, Convert( VarChar(1024), Right( '000' + Convert( VarChar(4), Id ), 4 ) )
from @Projects
where ParentId =0
union all
-- Add the children one level at a time.
select P.Id, P.ProjectId, P.Value, P.ParentId, IP.Level + 1, Convert( VarChar(1024), IP.Path + '<' + Right( '000' + Convert( VarChar(4), P.Id ), 4 ) )
from IndentedProjects as IP inner join
@Projects as P on P.ParentId = IP.Id
)
select Space( Level * 2 ) + Value as [IndentedValue], Id, ProjectId, Value, ParentId, Level, Path
from IndentedProjects
order by Path;
我们有一个 table 表示与实体(称之为项目)关联的值树,其中 ParentID 列指的是一行父项的 id 列。 id 列是一个自动递增的 IDENTITY 列和主键。根节点的 ParentID 为 0。
我们希望能够克隆给定项目的数据,并让生成的 ParentID 引用复制值的适当新 ID,其方式满足示例下方描述的限制。
例如复制下面ProjectID 611的数据table:
id ProjectID Value ParentID
--------------------------------------------------
1 611 Animal 0
2 611 Frog 1
13 611 Cow 1
14 611 Jersey Cow 13
25 611 Plant 0
29 611 Tree 25
31 611 Oak 29
应该导致:
id ProjectID Value ParentID
--------------------------------------------------
1 611 Animal 0
2 611 Frog 1
13 611 Cow 1
14 611 Jersey Cow 13
25 611 Plant 0
29 611 Tree 25
31 611 Oak 29
32 612 Animal 0
33 612 Frog 32
34 612 Cow 32
35 612 Jersey Cow 34
36 612 Plant 0
37 612 Tree 36
38 612 Oak 37
限制:
- 解决方案必须适用于 SQL Server 2005。也就是说,我们不能使用 MERGE(唉)。
- 我们不会对 table 做出关于 ID 或它们与 ParentID 的比较的假设;例如,该解决方案原则上应适用于 ids/ParentIDs 是 uniqueid。
- 我们不想在 table 中添加额外的列。 (我当前的解决方案添加了一个 "OldId" 列,复制过程在复制行时设置了该列。所以我目前正在使用 INSERT-SELECT 和 UPDATE-FROM 的组合,加入 ParentID 上的 OldId 列列以获取新 ID。)我们宁愿不使用 OldId 列来填充我们所有的分层 table 只是为了支持此复制操作。
- 解决方案必须具有合理的性能;我最初的解决方案是一组复杂的递归函数调用和循环,一次处理一个项目。我很快放弃了那条路线!
您可以通过将 MAX(ID)
添加到旧的 ParentID
来获得 ParentID
。
DECLARE @projectID INT
SET @projectID = 611
SET IDENTITY_INSERT YourTable ON
BEGIN TRANSACTION
DECLARE @maxID INT
SELECT @maxID= MAX(ID) FROM YourTable WITH (UPDLOCK,HOLDLOCK)
INSERT INTO YourTable(ID, ProjectID, Value, ParentID)
SELECT
ID + @maxID,
ProjectId + 1,
Value,
CASE
WHEN ParentID > 0 THEN ParentID + @maxID
ELSE 0
END
FROM YourTable WITH (UPDLOCK,HOLDLOCK)
WHERE
ProjectID = @projectID
COMMIT TRANSACTION
SET IDENTITY_INSERT YourTable OFF
您应该使用事务来锁定 table。您还可以添加 locking hints.
CTE 与 MERGE
配合使用效果很好,但在 SQL Server 2005 中存在问题。对于之前的误导性评论深表歉意。
下面展示了如何克隆一个项目(有多棵树)并修复父系以将新林与旧林分开。请注意,它不依赖于 Id 的任何特定排列,例如它们不需要是密集的、单调递增的,...。
-- Sample data.
declare @Projects as Table
( Id Int Identity, ProjectId Int, Value VarChar(16), ParentId Int Null );
insert into @Projects ( ProjectId, Value, ParentId ) values
( 611, 'Animal', 0 ),
( 611, 'Frog', 1 ),
( 611, 'Cow', 1 ),
( 611, 'Jersey Cow', 3 ),
( 611, 'Plant', 0 ),
( 611, 'Tree', 5 ),
( 611, 'Oak', 6 );
-- Display the raw data.
select * from @Projects;
-- Display the forest.
with IndentedProjects ( Id, ProjectId, Value, ParentId, Level, Path ) as
( -- Start with the top level rows.
select Id, ProjectId, Value, ParentId, 0, Convert( VarChar(1024), Right( '000' + Convert( VarChar(4), Id ), 4 ) )
from @Projects
where ParentId = 0
union all
-- Add the children one level at a time.
select P.Id, P.ProjectId, P.Value, P.ParentId, IP.Level + 1, Convert( VarChar(1024), IP.Path + '<' + Right( '000' + Convert( VarChar(4), P.Id ), 4 ) )
from IndentedProjects as IP inner join
@Projects as P on P.ParentId = IP.Id
)
select Space( Level * 2 ) + Value as [IndentedValue], Id, ProjectId, Value, ParentId, Level, Path
from IndentedProjects
order by Path;
-- Clone the project.
declare @OldProjectId as Int = 611;
declare @NewProjectId as Int = 42;
declare @Fixups as Table ( OldId Int, [NewId] Int );
begin transaction -- With suitable isolation since the hierarchy will be invalid until we apply the fixups!
insert into @Projects
output Inserted.ParentId, Inserted.Id
into @Fixups
select @NewProjectId, Value, Id -- Note that we save the old Id in the new ParentId.
from @Projects as P
where ProjectId = @OldProjectId;
-- Apply the fixups.
update PNew
set ParentId = IsNull( FNew.[NewId], 0 )
-- Output the fixups just to show what is going on.
output Deleted.Id, Deleted.ParentId as [ParentIdBeforeFixup], Inserted.ParentId as [ParentIdAfterFixup]
from @Fixups as F inner join
@Projects as PNew on PNew.Id = F.[NewId] inner join -- Rows we need to fix.
@Fixups as FOld on FOld.OldId = PNew.ParentId inner join
@Projects as POld on POld.Id = FOld.OldId left outer join
@Fixups as FNew on FNew.OldId = POld.ParentId;
commit transaction;
-- Display the forest.
with IndentedProjects ( Id, ProjectId, Value, ParentId, Level, Path ) as
( -- Start with the top level rows.
select Id, ProjectId, Value, ParentId, 0, Convert( VarChar(1024), Right( '000' + Convert( VarChar(4), Id ), 4 ) )
from @Projects
where ParentId =0
union all
-- Add the children one level at a time.
select P.Id, P.ProjectId, P.Value, P.ParentId, IP.Level + 1, Convert( VarChar(1024), IP.Path + '<' + Right( '000' + Convert( VarChar(4), P.Id ), 4 ) )
from IndentedProjects as IP inner join
@Projects as P on P.ParentId = IP.Id
)
select Space( Level * 2 ) + Value as [IndentedValue], Id, ProjectId, Value, ParentId, Level, Path
from IndentedProjects
order by Path;