Таблиці з ієрархією: створити обмеження для запобігання циркулярності за допомогою сторонніх клавіш


10

Припустимо, у нас є таблиця, яка має обмеження для зовнішнього ключа, як така:

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     ParentFooId BIGINT,
     FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )

INSERT INTO Foo (FooId, ParentFooId) 
VALUES (1, NULL), (2, 1), (3, 2)

UPDATE Foo SET ParentFooId = 3 WHERE FooId = 1

Ця таблиця матиме такі записи:

FooId  ParentFooId
-----  -----------
1      3
2      1
3      2

Бувають випадки, коли подібний дизайн може мати сенс (наприклад, типові стосунки «працівник-начальник-співробітник»), і в будь-якому випадку: я знаходжусь у ситуації, коли це є в моїй схемі.

На жаль, такий дизайн дозволяє забезпечити циркулярність записів даних, як показано в прикладі вище.

Моє питання тоді:

  1. Чи можна написати обмеження, яке перевіряє це? і
  2. Чи можливо написати обмеження, яке перевіряє це? (якщо потрібно лише на певну глибину)

У частині (2) цього питання може бути доречним зазначити, що я очікую лише сотні, а можливо, в деяких випадках тисячі записів у моїй таблиці, як правило, не вкладених глибше, ніж приблизно від 5 до 10 рівнів.

PS. MS SQL Server 2008


Оновити 14 березня 2012 р.
Було кілька хороших відповідей. Зараз я прийняв той, який допоміг мені зрозуміти згадану можливість / доцільність. Однак є кілька інших чудових відповідей, деякі з пропозиціями щодо імплементації, тому якщо ви приземлилися тут із тим самим питанням, ознайомтеся з усіма відповідями;)

Відповіді:


6

Ви використовуєте модель Списку суміжності , де важко застосувати таке обмеження.

Ви можете вивчити модель вкладеного набору , де можуть бути представлені лише справжні ієрархії (відсутні кругові контури). Однак у нього є й інші недоліки, як повільні вставки / оновлення.


+1 чудові посилання, і коханий, я б хотів, щоб я міг перейти і спробувати вкладену модель набору, а потім прийняти цю відповідь як ту, яка працювала на мене.
Jeroen

Я приймаючи цю відповідь, тому що це був один , який допоміг мені зрозуміти можливості і здійсненності , тобто відповів на питання для мене. Однак кожен, хто заходить на це питання, повинен ознайомитись з відповіддю @ a1ex07 на обмеження, яке працює в простих випадках, і на відповідь @ JohnGietzen за великі посилання, на HIERARCHYIDякі, здається, є вроджена реалізація вкладеної моделі MSSQL2008.
Єроен

7

Я бачив 2 основні способи забезпечення цього:

1, СТАРИЙ спосіб:

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     ParentFooId BIGINT,
     FooHierarchy VARCHAR(256),
     FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )

Стовпець FooHierarchy міститиме таке значення:

"|1|27|425"

Де числа відображаються у стовпці FooId. Потім ви примусите, що стовпець Ієрархії закінчується символом "| id", а решта рядка відповідає FooHieratchy РОБИТТЯ.

2, НОВИЙ спосіб:

SQL Server 2008 має новий тип даних під назвою HierarchyID , який робить все це для вас.

Він працює на тому самому принципі, що і СТАРИЙ спосіб, але він обробляється ефективно SQL сервером і підходить для використання як ЗАМІНА для стовпця "ParentID".

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     FooHierarchy HIERARCHYID )

1
Чи є у вас джерело або короткий демонстраційний демонстратор, який HIERARCHYIDперешкоджає створенню циклів ієрархії?
Нік Чаммас

6

Це можливо: ви можете викликати скалярний UDF від вас ПЕРЕВІРИТИ обмеження, і він може виявити цикли будь-якої довжини. На жаль, такий підхід надзвичайно повільний і ненадійний: у вас можуть бути помилкові позитиви та хибні негативи.

Натомість я б використав матеріалізований шлях.

Ще один спосіб уникнути циклів - це перевірити (ID> ParentID), що, мабуть, теж не дуже можливо.

Ще один спосіб уникнути циклів - це додавання ще двох стовпців, LevelInHierarchy та ParentLevelInHierarchy, мають (ParentID, ParentLevelInHierarchy) посилання на (ID, LevelInHierarchy) та CHECK (LevelInHierarchy> ParentLevelInHierarchy).


АДС в обмеженнях CHECK НЕ працюють. Ви не можете отримати узгоджене зображення на рівні таблиці запропонованого стану після оновлення з функції, яка працює за одним рядком. Потрібно використати ПІСЛЯ тригера та відкоти назад або ВСТАНОВИТИ тригер та відмовитися від оновлення.
ЕрікЕ

Але тепер я бачу коментарі до іншої відповіді про оновлення з кількома рядами.
ЕрікЕ

@ErikE правильно, UDF в обмеженнях CHECK НЕ працює.
АК

@ Алекс погодився. Мені знадобилося кілька годин, щоб твердо довести це один раз.
ЕрікЕ

4

Я вважаю, що це можливо:

create function test_foo (@id bigint) returns bit
as
begin
declare @retval bit;

with t1 as (select @id as FooId, 0 as lvl  
union all 
 select f.FooId , t1.lvl+1 from t1 
 inner join Foo f ON (f.ParentFooId = t1.FooId)
 where lvl<11) -- you said that max nested level 10, so if there is any circular   
-- dependency, we don't need to go deeper than 11 levels to detect it

 select @retval =
 CASE(COUNT(*)) 
 WHEN 0 THEN 0 -- for records that don't have children
 WHEN 1 THEN 0 -- if a record has children
  ELSE 1 -- recursion detected
 END
 from t1
 where t1.FooId = @id ;

return @retval; 
end;
GO
alter table Foo add constraint CHK_REC1 CHECK (dbo.test_foo(ParentFooId) = 0)

Я, можливо, щось пропустив (вибачте, я не в змозі перевірити це наскрізь), але це, здається, працює.


1
Я погоджуюся, що "це, здається, працює", але воно може вийти з ладу для багаторядкових оновлень, вийти з ладу під ізоляцією знімків і дуже повільно.
АК

@AlexKuznetsov: Я розумію, що рекурсивний запит відносно повільний, і я погоджуюся, що оновлення з кількома рядами можуть бути проблемою (їх можна відключити).
a1ex07

@ a1ex07 Thx для цієї пропозиції. Я спробував це, і в простих випадках, здається, справді добре працює. Ще не впевнений, чи невдача в багаторядкових оновленнях є проблемою (хоча, мабуть, і є). Я не впевнений, що ж ви маєте на увазі під "вони можуть бути відключені"?
Єроен

Наскільки я розумію, завдання передбачає логіку на основі курсору (або рядка). Тому є сенс вимкнути оновлення, що модифікують більше 1 ряду (простий замість тригера оновлення, який викликає помилку, якщо вставлена ​​таблиця має більше 1 ряду).
a1ex07

Якщо ви не можете переробити таблицю, я створив би процедуру, яка перевіряє всі обмеження та додає / оновлює запис. Тоді я переконуюсь, що ніхто, крім цього списку, не зможе вставити / оновити цю таблицю.
a1ex07

3

Ось ще один варіант: тригер, який дозволяє оновлювати багато рядків і не застосовувати циклів. Він працює шляхом проходження ланцюга предків, поки не знайде кореневий елемент (з батьківським 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. Це запобіжить невідповідності, хоча, на жаль, призведе до посилення блокування. Це неминуче для завдання, яке існує.


Вам слід використовувати підказку З (READCOMMITTEDLOCK). Уго Корнеліс написав приклад: sqlblog.com/blogs/hugo_kornelis/archive/2006/09/15/…
АК

Завдяки @Alex ці статті були динамітом і допомогли мені зрозуміти ізоляцію знімків набагато краще. Я додав умовний перемикач, щоб прочитати невільнений мій код.
ЕрікЕ

2

Якщо ваші записи вкладені більше ніж на 1 рівень, обмеження не працюватиме (я припускаю, що ви маєте на увазі, наприклад, запис 1 є батьківським записом 2, а запис 3 - батьківським записом 1). Єдиний спосіб зробити це було б або в батьківському коді, або з тригером, але якщо ви дивитесь на велику таблицю та кілька рівнів, це було б досить інтенсивно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.