复制分层数据时保留父子关系

时间:2015-02-16 23:23:32

标签: sql sql-server tsql sql-server-2005

我们有一个表,表示与实体关联的值树(称为项目),其中ParentID列引用行父项的id列。 id列是自动递增的IDENTITY列和主键。根节点的ParentID为0。

我们希望能够克隆给定项目的数据,并让生成的ParentID以符合示例下面描述的限制的方式引用复制值的相应新ID。

例如,复制下表中ProjectID 611的数据:

    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(唉)。
  • 我们不习惯对id进行假设或与ParentID进行比较;原则上,该解决方案应适用于uniqueid的ids / ParentID,例如。
  • 我们不想在表格中添加其他列。 (我当前的解决方案添加了一个“OldId”列,复制过程在复制行时设置。所以我当前正在使用INSERT-SELECT和UPDATE-FROM的组合,加入ParentID列上的OldId列以获取新的id 。)我们宁愿不将所有的分层表与OldId列一起用于支持这种复制操作。
  • 解决方案必须具有合理的性能;我的初始解决方案是一组复杂的递归函数调用和循环,一次处理一个项目。我很快就放弃了那条路!

2 个答案:

答案 0 :(得分:3)

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;

答案 1 :(得分:2)

您可以通过将ParentID添加到旧版MAX(ID)来获取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

您应该使用事务来锁定表。您还可以添加locking hints.