这是另一个选项:允许多行更新并且不强制执行任何周期的触发器。它通过遍历祖先链直到找到根元素(父NULL)来工作,从而证明没有循环。它被限制为10代,因为当然一个循环是无限的。
它仅适用于当前的一组修改后的行,因此,只要更新不涉及表中大量非常深的项目,性能就不会太差。对于每个元素,它确实必须一直沿链向上移动,因此会对性能产生一定的影响。
真正的“智能”触发器将通过检查项目是否到达自身然后进行保释来直接查找周期。但是,这需要在每个循环期间检查所有先前找到的节点的状态,因此需要一个WHILE循环和比我现在想要做的更多的编码。这实际上应该不会花费太多,因为正常的操作将是没有周期,并且在这种情况下,在每个循环中,仅与上一代而不是所有先前的节点一起工作会更快。
我很乐意从@AlexKuznetsov或其他任何人那里获得有关快照隔离如何工作的信息。我怀疑效果不是很好,但想更好地理解它。
CREATE TRIGGER TR_Foo_PreventCycles_IU ON Foo FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF EXISTS (
SELECT *
FROM sys.dm_exec_session
WHERE session_id = @@SPID
AND transaction_isolation_level = 5
)
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
END;
DECLARE
@CycledFooId bigint,
@Message varchar(8000);
WITH Cycles AS (
SELECT
FooId SourceFooId,
ParentFooId AncestorFooId,
1 Generation
FROM Inserted
UNION ALL
SELECT
C.SourceFooId,
F.ParentFooId,
C.Generation + 1
FROM
Cycles C
INNER JOIN dbo.Foo F
ON C.AncestorFooId = F.FooId
WHERE
C.Generation <= 10
)
SELECT TOP 1 @CycledFooId = SourceFooId
FROM Cycles C
GROUP BY SourceFooId
HAVING Count(*) = Count(AncestorFooId); -- Doesn't have a NULL AncestorFooId in any row
IF @@RowCount > 0 BEGIN
SET @Message = CASE WHEN EXISTS (SELECT * FROM Deleted) THEN 'UPDATE' ELSE 'INSERT' END + ' statement violated TRIGGER ''TR_Foo_PreventCycles_IU'' on table "dbo.Foo". A Foo cannot be its own ancestor. Example value is FooId ' + QuoteName(@CycledFooId, '"') + ' with ParentFooId ' + Quotename((SELECT ParentFooId FROM Inserted WHERE FooID = @CycledFooId), '"');
RAISERROR(@Message, 16, 1);
ROLLBACK TRAN;
END;
更新资料
我想出了如何避免多余的联接回到Inserted表的方法。如果有人发现使用GROUP BY来检测不包含NULL的更好的方法,请告诉我。
如果当前会话处于SNAPSHOT ISOLATION级别,我还添加了一个切换到READ COMMITTED的选项。这将防止不一致,尽管不幸的是会导致阻塞增加。这对于手头的任务来说是不可避免的。