Вот еще один вариант: триггер, который разрешает многострочные обновления и обеспечивает отсутствие циклов. Он работает путем обхода цепочки предков, пока не найдет корневой элемент (с родительским 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, пожалуйста, дайте мне знать.
Я также добавил переключатель READ COMMITTED, если текущий сеанс находится на уровне SNAPSHOT ISOLATION. Это предотвратит несоответствия, хотя, к сожалению, приведет к усилению блокировки. Это как бы неизбежно для поставленной задачи.