Запрет циклической ссылки в таблице MS-SQL - PullRequest
3 голосов
/ 01 апреля 2019

У меня есть таблица учетных записей с идентификатором и ParentAccountID. Вот сценарии для воспроизведения шагов.

Если ParentAccountID имеет значение NULL, это считается учетной записью верхнего уровня. Наконец, каждая учетная запись должна заканчиваться учетной записью верхнего уровня, т.е. ParentAccountID имеет значение NULL

    Declare @Accounts table (ID INT, ParentAccountID INT )    


    INSERT INTO @Accounts values (1,NULL), (2,1), (3,2) ,(4,3), (5,4), (6,5)

    select * from @Accounts

     -- Request to update ParentAccountID to 6 for the ID 3
    update @Accounts  
    set ParentAccountID = 6
    where ID = 3

    -- Now the above update will cause circular reference 
    select * from @Accounts

Когда приходит запрос на обновление ParentAccountID учетной записи, если это вызывает циклическую ссылку, то перед обновлением его необходимо идентифицировать.

Любая идея, люди !!

Ответы [ 4 ]

3 голосов
/ 01 апреля 2019

Кажется, у вас есть некоторые бизнес-правила, определенные для вашей таблицы:

  • Вся цепочка должна заканчиваться учетной записью верхнего уровня
  • Цепочка может не иметь круговой ссылки

У вас есть два способа обеспечить это.

Вы можете создать триггер в своей базе данных и проверить логику в триггере. Преимущество в том, что он работает внутри базы данных, поэтому он применяется к каждой транзакции независимо от клиента. Однако триггеры базы данных не всегда популярны. Я вижу их как побочный эффект , и их может быть трудно отладить. Триггеры запускаются как часть вашего SQL, поэтому, если они медленные, ваш SQL будет медленным.

Альтернативой является применение этой логики на прикладном уровне - что бы вы ни говорили с вашей базой данных. Это проще в отладке и делает вашу бизнес-логику явной для новых разработчиков - но она не работает внутри базы данных, поэтому вы можете в конечном итоге воспроизвести логику, если у вас несколько клиентских приложений.

2 голосов
/ 01 апреля 2019

Вот пример, который вы могли бы использовать в качестве основы для реализации ограничения базы данных, которое должно предотвращать циклические ссылки при обновлении отдельных строк;Я не верю, что это будет работать для предотвращения циклической ссылки, если несколько строк обновляются.

/*
ALTER TABLE dbo.Test  DROP CONSTRAINT chkTest_PreventCircularRef
GO
DROP FUNCTION dbo.Test_PreventCircularRef 
GO
DROP TABLE dbo.Test 
GO
*/

CREATE TABLE dbo.Test (TestID INT PRIMARY KEY,TestID_Parent INT)
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 1 AS TestID,NULL  AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 2 AS TestID,1     AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 3 AS TestID,2     AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 4 AS TestID,3     AS TestID_Parent
INSERT INTO dbo.Test(TestID,TestID_Parent) SELECT 5 AS TestID,4     AS TestID_Parent
GO

GO
CREATE FUNCTION dbo.Test_PreventCircularRef (@TestID INT,@TestID_Parent INT)
RETURNS INT
BEGIN
    --FOR TESTING:
    --SELECT * FROM dbo.Test;DECLARE @TestID INT=3,@TestID_Parent INT=4

    DECLARE @ParentID INT=@TestID
    DECLARE @ChildID INT=NULL
    DECLARE @RetVal INT=0
    DECLARE @Ancestors TABLE(TestID INT)
    DECLARE @Descendants TABLE(TestID INT)

    --Get all descendants
    INSERT INTO @Descendants(TestID) SELECT TestID FROM dbo.Test WHERE TestID_Parent=@TestID
    WHILE (@@ROWCOUNT>0)
    BEGIN
        INSERT INTO @Descendants(TestID)
            SELECT t1.TestID
            FROM dbo.Test t1
            LEFT JOIN @Descendants relID ON relID.TestID=t1.TestID
            WHERE relID.TestID IS NULL
            AND t1.TestID_Parent IN (SELECT TestID FROM @Descendants)
    END

    --Get all ancestors
    --INSERT INTO @Ancestors(TestID) SELECT TestID_Parent FROM dbo.Test WHERE TestID=@TestID
    --WHILE (@@ROWCOUNT>0)
    --BEGIN
    --  INSERT INTO @Ancestors(TestID)
    --      SELECT t1.TestID_Parent
    --      FROM dbo.Test t1
    --      LEFT JOIN @Ancestors relID ON relID.TestID=t1.TestID_Parent
    --      WHERE relID.TestID IS NULL
    --      AND t1.TestID_Parent IS NOT NULL
    --      AND t1.TestID IN (SELECT TestID FROM @Ancestors)
    --END

    --FOR TESTING:
    --SELECT TestID AS [Ancestors] FROM @Ancestors;SELECT TestID AS [Descendants] FROM @Descendants;

    IF EXISTS (
        SELECT *
        FROM @Descendants
        WHERE TestID=@TestID_Parent
    )
    BEGIN
        SET @RetVal=1
    END

    RETURN @RetVal
END
GO

ALTER TABLE dbo.Test 
  ADD CONSTRAINT chkTest_PreventCircularRef
  CHECK (dbo.Test_PreventCircularRef(TestID,TestID_Parent) = 0); 
GO

SELECT * FROM dbo.Test

--This is problematic as it creates a circular reference between TestID 3 and 4; it is now prevented
UPDATE dbo.Test SET TestID_Parent=4 WHERE TestID=3
1 голос
/ 01 апреля 2019

Работа с самообращающимися таблицами / рекурсивными связями в SQL - это не просто. Я полагаю, это подтверждается тем фактом, что несколько человек не могут решить проблему, просто проверяя циклы одной глубины.

Чтобы применить это к табличным ограничениям, вам понадобится проверочное ограничение на основе рекурсивного запроса. В лучшем случае это специфичная для СУБД поддержка, и она может работать неэффективно, если ее нужно запускать при каждом обновлении.

Мой совет - использовать код, содержащий инструкцию UPDATE, для обеспечения этого. Это может принять несколько форм. В любом случае, если требуется строгое соблюдение, может потребоваться ограничение доступа UPDATE к таблице для учетной записи службы, используемой хранимым процедурой или внешней службой.

Использование хранимой процедуры может отличаться от ограничения CHECK, за исключением того, что вы можете использовать процедурную (итеративную) логику для поиска циклов перед выполнением обновления. Тем не менее, стало непопулярным помещать слишком много логики в хранимые процессы, и то, следует ли выполнять этот тип проверки, является оценочным призывом от команды к команде / организации / организации.

Аналогично, использование подхода, основанного на сервисе, позволит вам использовать процедурную логику для поиска циклов, и вы можете написать ее на языке, лучше подходящем для такой логики. Проблема здесь в том, что если сервисы не являются частью вашей архитектуры, то немного сложнее представить целый новый уровень. Но уровень обслуживания, вероятно, считается более современным / популярным (на данный момент, по крайней мере), чем передача обновлений через хранимые процессы.

С учетом этих подходов и понимания того, что как процедурный, так и рекурсивный синтаксис в базах данных зависит от СУБД, существует слишком много возможных вариантов синтаксиса, чтобы их можно было реально использовать. Но идея такова:

  • Изучите предполагаемого родителя.
  • Проверить его родительский рекурсивно
  • Достигали ли вы когда-либо предполагаемого ребенка до того, как входили в аккаунт верхнего уровня? Если нет, разрешите обновление
0 голосов
/ 04 апреля 2019

Наконец, я создал сценарии после некоторых сбоев, у меня все работает нормально.

   -- To hold the Account table data
   Declare @Accounts table (ID INT, ParentAccountID INT) 

   -- To be updated 
   Declare @AccountID       int = 4;
   Declare @ParentAccountID int = 7;

   Declare @NextParentAccountID INT = @ParentAccountID

   Declare @IsCircular int = 0

   INSERT INTO @Accounts values (1, NULL), (2,1), (3,1) ,(4,3), (5,4), (6,5), (7,6), (8,7)

   -- No circular reference value
   --Select * from @Accounts

   -- Request to update ParentAccountID to 7 for the Account ID 4
   update @Accounts  
   set ParentAccountID = @ParentAccountID
   where ID = @AccountID

   Select * from @Accounts

   WHILE(1=1)
   BEGIN            
       -- Take the ParentAccountID for @NextParentAccountID
       SELECT @NextParentAccountID = ParentAccountID from @Accounts WHERE ID = @NextParentAccountID  

       -- If the @NextParentAccountID is NULL, then it reaches the top level account, no circular reference hence break the loop 
       IF (@NextParentAccountID IS NULL) 
       BEGIN
        BREAK;
       END

       -- If the @NextParentAccountID is equal to @AccountID (to which the update was done) then its creating circular reference
       -- Then set the @IsCircular to 1 and break the loop
       IF (@NextParentAccountID = @AccountID ) 
       BEGIN
        SET @IsCircular = 1
        BREAK
       END
   END

IF @IsCircular = 1 
    BEGIN 
        select 'CircularReference' as 'ResponseCode'
    END
...