我有一个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时,如果导致循环引用,则在更新之前需要识别。
任何想法的人!
看来你已经为你的表定义了一些业务规则:
您有两种方法来强制执行此操作。
您可以在数据库中创建trigger,并检查触发器中的逻辑。这有利于在数据库内部运行,因此无论客户端如何,它都适用于每个事务。但是,数据库触发器并不总是很受欢迎。我认为它们是side effect,它们很难调试。触发器作为SQL的一部分运行,因此如果它们很慢,则SQL会很慢。
另一种方法是在应用程序层中强制执行此逻辑 - 无论与数据库进行通信。这更容易调试,并使您的业务逻辑对新开发人员显而易见 - 但它不在数据库内运行,因此如果您有多个客户端应用程序,最终可能会复制逻辑。
下面是一个示例,您可以将其用作实现数据库约束的基础,该数据库约束应阻止单行更新中的循环引用;如果更新多行,我不相信这会阻止循环引用。
/*
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
在SQL中处理自引用表/递归关系并不简单。我想这可以通过以下事实得到证明:多人无法通过检查单深度循环来解决问题。
要使用表约束强制执行此操作,您需要基于递归查询的检查约束。充其量只是DBMS特定的支持,如果必须在每次更新时运行,它可能表现不佳。
我的建议是让包含UPDATE语句的代码强制执行此操作。这可能需要几种形式。在任何情况下,如果需要严格执行,则可能需要将对表的UPDATE访问限制为存储过程或外部服务使用的服务帐户。
使用存储过程将类似于CHECK约束,除了您可以使用过程(迭代)逻辑在执行更新之前查找周期。然而,在存储过程中放置太多逻辑已经变得不受欢迎,并且是否应该进行这种类型的检查是从团队到团队/组织到组织的判断调用。
同样地,使用基于服务的方法可以让您使用过程逻辑来查找周期,并且可以用更适合这种逻辑的语言编写它。这里的问题是,如果服务不是你的架构的一部分,那么引入一个全新的层就有点重要了。但是,服务层可能被认为比通过存储过程漏斗更新更现代/更受欢迎(至少目前)。
考虑到这些方法 - 并且理解数据库中的过程语法和递归语法都是特定于DBMS的 - 有太多可能的语法选项要真正进入。但这个想法是:
最后,我在一些失败后创建了脚本,它对我来说很好。
-- 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