【问题标题】:Preserve parent-child relationships when copying hierarchical data复制分层数据时保留父子关系
【发布时间】:2015-04-17 14:15:23
【问题描述】:

我们有一个表,表示与实体相关联的值树(称为项目),其中 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 的比较做出假设;例如,该解决方案原则上应适用于唯一 ID 的 ID/ParentID。
  • 我们不希望在表格中添加额外的列。 (我当前的解决方案添加了一个“OldId”列,复制过程在复制行时设置该列。所以我目前正在使用 INSERT-SELECT 和 UPDATE-FROM 的组合,加入 ParentID 列上的 OldId 列以获取新 id .) 我们宁愿不要仅仅为了支持这种复制操作而在所有分层表中添加 OldId 列。
  • 解决方案必须具有合理的性能;我最初的解决方案是一组复杂的递归函数调用和循环,一次处理一个项目。我很快就放弃了那条路线!

【问题讨论】:

  • CTE 从 SS 2005 开始提供。使用 CTE 和 INSERT 上的 OUTPUT 子句复制树,以获取 id 的修复表(临时或变量),然后应用修复。
  • @HABO 我希望看看我能做到这一点……我听说过 CTE,但不确定如何使用它们来解决这个问题。
  • 在考虑使用 CTE 之后,我意识到如果没有 MERGE,它真的无济于事。我添加了一个使用INSERT 和一个UPDATE 的答案,没有循环,也不需要遍历层次结构。因此,性能应该不会太差。唯一的关键因素是新的层次结构在两个语句之间处于不一致的状态,您需要防止其他用户访问它们,例如使用隔离级别足以满足您的应用程序的事务。

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


【解决方案1】:

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;

【讨论】:

  • 不错的解决方案。关键是将旧 ID 暂时存储在 ParentID 列中!非常聪明。 CS 教授注意:这对于 DB 课程来说是一个很大的额外学分问题......
  • @System.Cats.Lol - 有时我们这些老人在我们的紧身衣上有一两招。您可以通过将错误数据存储在列中来将其视为非规范化,或者将其视为使列可用于OUTPUT 子句的技巧,同时覆盖我们将很快修复的列。另一个关键是您的表包含ProjectId,因此无需遍历层次结构来收集整棵树。请注意,事务还需要防止原始层次结构的视图发生变化,因为UPDATE 在修复期间使用它。很高兴我能帮上忙!
【解决方案2】:

您可以通过将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

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

【讨论】:

  • 我对添加试图用 COUNT 猜测生成的 id 的代码持谨慎态度...并发呢?
  • 只要确保你使用了一个事务。
  • 好的——但是选择不是假设我们将按照示例中给出的顺序获取行吗?
  • 什么意思?添加ORDER BY 以指定顺序。
  • 如果 id 有间隙,这个解决方案就不起作用,这是典型的;更新了问题以使这一点更清楚。
猜你喜欢
  • 1970-01-01
  • 2016-01-09
  • 2020-11-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-15
相关资源
最近更新 更多