【问题标题】:Prevent circular reference in MS-SQL table防止 MS-SQL 表中的循环引用
【发布时间】:2019-04-01 13:19:49
【问题描述】:

我有一个包含 ID 和 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 时,如果这会导致循环引用,那么在更新之前需要确定它。

大家有什么想法!!

【问题讨论】:

  • 您使用的是 MySQL 还是 MS SQL Server?
  • @jarlh MS SQL 服务器
  • @YvetteColomb 感谢您的介入。Mark 最终是对的,但我认为应该避免以使所有答案无效的方式更改问题。
  • 你有没有考虑过这种建模你的hierarchy的方式是否正确?如果您改用hierarchyid,您会发现循环性首先是不可能。然而,就其对其他查询的影响而言,这是一种权衡。

标签: sql sql-server sql-server-2008 circular-reference


【解决方案1】:

您似乎已经为您的表定义了一些业务规则:

  • 所有链必须以顶级帐户结束
  • 链可能没有循环引用

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

您可以在数据库中创建trigger,并检查触发器中的逻辑。这具有在数据库内部运行的好处,因此它适用于每个事务,无论客户端如何。但是,数据库触发器并不总是很流行。我将它们视为side effect,它们可能很难调试。触发器作为 SQL 的一部分运行,因此如果它们很慢,您的 SQL 也会很慢。

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      在 SQL 中处理自引用表/递归关系并不简单。我想这可以通过以下事实得到证明:多人无法通过仅检查单深度循环来解决问题。

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

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

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

      同样,使用基于服务的方法可以让您使用过程逻辑来查找循环,并且您可以用更适合这种逻辑的语言编写它。这里的问题是,如果服务不是您架构的一部分,那么引入一个全新的层就有点重了。但是,服务层可能被认为比通过存储过程汇集更新更现代/更流行(至少目前)。

      考虑到这些方法 - 并了解数据库中的过程和递归语法都是 DBMS 特定的 - 有太多可能的语法选项需要真正进入。但想法是:

      • 检查拟议的父母。
      • 递归检查它的父级
      • 在达到顶级帐户之前,您是否曾联系过被提议的孩子?如果没有,请允许更新

      【讨论】:

        【解决方案4】:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-03-12
          • 1970-01-01
          • 2011-03-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多