防止 MS-SQLtable 中的循环引用

Prevent circular reference in MS-SQL table

我有一个带有 ID 和 ParentAccountID 的帐户 table。这是重现这些步骤的脚本。

如果 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,如果这导致循环引用,那么在更新之前需要确定它。

伙计们有什么想法!!

您似乎已经为 table:

定义了一些业务规则
  • 所有链必须以顶级账户结束
  • 链不能有循环引用

您有两种方法可以执行此操作。

您可以创建一个 trigger in your database, and check the logic in the trigger. This has the benefit of running inside the database, so it applies to every transaction, regardless of the client. However, database triggers are not always popular. I see them as a side effect,它们可能很难调试。触发器 运行 作为 SQL 的一部分,因此如果它们很慢,您的 SQL 也会很慢。

另一种方法是在应用程序层中强制执行此逻辑 - 无论与您的数据库对话的是什么。这更容易调试,并使您的业务逻辑对新开发人员来说是明确的 - 但它不会 运行 在数据库中,因此如果您有多个客户端应用程序,您最终可能会复制逻辑。

处理 self-referencing tables / SQL 中的递归关系并不简单。我想这可以通过以下事实得到证明:多个人仅通过检查 single-depth 个周期就无法解决问题。

要使用 table 约束强制执行此操作,您需要基于递归查询的检查约束。充其量是 DBMS-specific 支持,如果每次更新都必须 运行,它可能表现不佳。

我的建议是让包含 UPDATE 语句的代码强制执行此操作。这可以采取几种形式。在任何情况下,如果需要严格执行,可能需要将对 table 的更新访问限制为存储过程或外部服务使用的服务帐户。

使用存储过程的方式与 CHECK 约束类似,不同之处在于您可以使用过程(迭代)逻辑在执行更新之前查找循环。但是,在存储过程中放置​​过多逻辑已变得不受欢迎,是否应该进行此类检查是团队与团队/组织与组织之间的判断电话。

同样,使用 service-based 方法可以让您使用过程逻辑来查找循环,并且您可以用更适合这种逻辑的语言来编写它。这里的问题是,如果服务不是您的体系结构的一部分,那么引入一个全新的层有点 heavy-weight。但是,与通过存储过程汇集更新相比,服务层可能被认为更多 modern/popular(至少目前)。

考虑到这些方法 - 并理解数据库中的过程语法和递归语法都是 DBMS-specific - 有太多可能的语法选项无法真正进入。但思路是:

  • 检查提议的parent。
  • 递归检查parent
  • 您是否曾在到达 top-level 帐户之前到达建议的 child?如果没有,允许更新

这是一个示例,您可以将其用作实施数据库约束的基础,该约束应防止单行更新中的循环引用;如果更新了多行,我认为这不会阻止循环引用。

/*
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

最后,我在经历了一些失败之后创建了脚本,它对我来说工作正常。

   -- 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