これは別のオプションです。複数行の更新を許可し、サイクルを強制しないトリガーです。ルート要素(親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;
更新
挿入されたテーブルへの余分な結合を回避する方法を見つけました。NULLを含まないものを検出するためにGROUP BYを行うより良い方法を誰かが見た場合は、私に知らせてください。
また、現在のセッションがSNAPSHOT ISOLATIONレベルの場合、READ COMMITTEDへのスイッチを追加しました。これにより不整合が防止されますが、残念ながらブロッキングが増加します。それは、目の前の仕事ではやむを得ないことです。