Рекурсивный клон иерархии в SQL Server - PullRequest
0 голосов
/ 17 октября 2018

У меня есть иерархия в таблице:

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

Я хочу иметь возможность клонировать корневую конфигурацию (одну с ParentId = NULL, в примере выше однус ConfigurationId = 1 и Name = prod), вставив новую корневую конфигурацию с именем, которое я ввожу, выполнив хранимую процедуру и дублируя строки на текущие, с той лишь разницей, что ConfigurationId является идентификатором и ParentId, который должен измениться в соответствии с новым ConfigurationId s при сохранении иерархии.

Требуемые данные будут выглядеть следующим образом:

   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варианты для клонирования.

Любая помощь будет оценена.

Ответы [ 2 ]

0 голосов
/ 18 октября 2018

В следующем коде используется CTE и update для создания копии указанной иерархии.CTE рекурсивно переходит от корня к листьям и передает insert, который добавляет «копии» строк.Предложение output в insert создает таблицу пар исправлений, содержащих старые и новые значения ConfigurationId для каждой новой строки.Поскольку предложение output имеет доступ только к вставленным значениям столбца, мы «заимствуем» столбец (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;
0 голосов
/ 17 октября 2018

Существует несколько доступных ответов, показывающих, как использовать рекурсивный 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
...