Ось ще один варіант: тригер, який дозволяє оновлювати багато рядків і не застосовувати циклів. Він працює шляхом проходження ланцюга предків, поки не знайде кореневий елемент (з батьківським 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;
Оновлення
Я придумав, як уникнути зайвого приєднання назад до вставленої таблиці. Якщо хтось бачить кращий спосіб зробити GROUP BY для виявлення тих, які не містять NULL, будь ласка, дайте мені знати.
Я також додав перемикач на «ПРОЧИТАТИ ЗАВЕРШЕНО», якщо поточний сеанс знаходиться на рівні ІЗОЛЯЦІЇ SNAPSHOT. Це запобіжить невідповідності, хоча, на жаль, призведе до посилення блокування. Це неминуче для завдання, яке існує.