防止MS-SQL表中的循环引用

问题描述 投票:3回答:4

我有一个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时,如果导致循环引用,则在更新之前需要识别。

任何想法的人!

sql sql-server sql-server-2008 circular-reference
4个回答
3
投票

看来你已经为你的表定义了一些业务规则:

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

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

您可以在数据库中创建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

1
投票

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

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

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

使用存储过程将类似于CHECK约束,除了您可以使用过程(迭代)逻辑在执行更新之前查找周期。然而,在存储过程中放置​​太多逻辑已经变得不受欢迎,并且是否应该进行这种类型的检查是从团队到团队/组织到组织的判断调用。

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

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

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

0
投票

最后,我在一些失败后创建了脚本,它对我来说很好。

   -- 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
© www.soinside.com 2019 - 2024. All rights reserved.