Обмеження зовнішнього ключа може спричинити цикли чи кілька каскадних шляхів?


176

У мене виникають проблеми, коли я намагаюся додати обмеження до своїх таблиць. Я отримую помилку:

Введення обмеження ЗОВНІШНЯ КЛЮЧ "FK74988DB24B3C886" в таблиці "Співробітник" може спричинити цикли або кілька каскадних шляхів. Вкажіть УВІДКЛЮЧАТИ НЕ ДІЙ чи НА ОНОВЛЕННЯ НЕ ДІЇ, або змініть інші обмеження ЗОВНІШНЬОГО КЛЮЧА.

Моє обмеження - між Codeстолом і employeeстолом. CodeТаблиця містить Id, Name, FriendlyName, Typeі Value. employeeМає ряд полів, посилальні коди, так що може бути посилання для кожного типу коду.

Мені потрібно, щоб поля були встановлені на нуль, якщо код, на який посилається, буде видалений.

Будь-які ідеї, як я можу це зробити?


Одне з варіантів тут
Ісмаїл

Відповіді:


180

SQL Server робить простий підрахунок каскадних шляхів і, замість того, щоб намагатися розібратися, чи існують якісь цикли, він припускає найгірше і відмовляється створювати референтні дії (CASCADE): ви можете і повинні все-таки створювати обмеження без референтних дій. Якщо ви не можете змінити свій дизайн (або це зробить компрометацію), тоді слід розглянути можливість використання тригерів в крайньому випадку.

Вирішення каскадних шляхів FWIW є складною проблемою. Інші продукти SQL просто ігнорують проблему і дозволять створювати цикли, і в цьому випадку це буде гонка, побачити яку буде замінено значення останнього, ймовірно, на незнання дизайнера (наприклад, це робить ACE / Jet). Я розумію, що деякі продукти SQL намагаються вирішити прості випадки. Факт залишається фактом, SQL Server навіть не намагається, він грає ультрабезпечно, забороняючи більше одного шляху, і, принаймні, це говорить вам.

Самі Microsoft радять використовувати тригери замість обмежень FK.


2
одна річ, яку я досі не можу зрозуміти, це те, що якщо цю "проблему" можна вирішити за допомогою тригера, то як же привести, що тригер не "спричинить цикли чи кілька каскадних шляхів ..."?
армен

5
@armen: оскільки ваш тригер буде явно подавати логіку, яку система не могла неявно з'ясувати самостійно, наприклад, якщо є кілька шляхів для видалення еталонної дії, то ваш код тригера визначатиме, які таблиці видаляються та в якому порядку.
onedaywhen

6
А також спусковий механізм виконується після того, як перша операція завершиться, тому гонки не відбувається.
Бон

2
@dumbledad: Я маю на увазі, використовуйте тригери лише тоді, коли обмеження (можливо, в поєднанні) не можуть виконати роботу. Обмеження є декларативними, а їх реалізація - відповідальність системи. Тригери - це процедурний код, і ви повинні кодувати (і налагоджувати) реалізацію та переносити їх недоліки (гірша продуктивність тощо).
одного дня, коли

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

99

Типовою ситуацією з кількома каскадними шляхами буде така: Основна таблиця з двома деталями, скажімо, "Master" і "Detail1" і "Detail2". Обидві деталі каскадно видаляють. Поки проблем немає. Але що робити, якщо обидві деталі мають відношення один до багатьох з якоюсь іншою таблицею (скажімо, "SomeOtherTable"). У SomeOtherTable є стовпець Detail1ID І стовпець Detail2ID.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

Іншими словами: деякі записи в SomeOtherTable пов'язані з Detail1-записами, а деякі записи в SomeOtherTable пов'язані із записами Detail2. Навіть якщо гарантується, що записи SomeOtherTable ніколи не належать до обох деталей, тепер неможливо зробити записи SomeOhterTable каскадом видалення для обох деталей, оскільки є кілька каскадних шляхів від Master до SomeOtherTable (один через Detail1 і один через Detail2). Тепер ви, можливо, це вже зрозуміли. Ось можливе рішення:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Усі поля ідентифікатора - це ключі та автоматичне збільшення. Суть лежить у полях DetailMainId таблиць Detail. Ці поля є як ключовими, так і референтними контрагентами. Тепер можна каскадно видалити все, лише видаливши головні записи. Мінусом є те, що для кожного деталі1-запису ТА для кожного запису деталі2 також повинен бути запис DetailMain (який фактично створюється спочатку для отримання правильного та унікального ідентифікатора).


1
Ваш коментар мені дуже допоміг зрозуміти проблему, з якою я стикаюся. Дякую! Я вважаю за краще вимкнути видалення каскаду для одного з шляху, а потім обробити видалення інших записів деякими іншими способами (збережені процедури; тригери; за кодом тощо). Але я маю на увазі ваше рішення (згрупування в один шлях) для можливих різних застосувань однієї і тієї самої проблеми ...
вільно

1
Один на використання слова crux (а також для пояснення)
masterwok

Це краще, ніж писати тригери? Мабуть, дивним є додавання додаткової таблиці просто для того, щоб каскад працював.
dumbledad

Все краще, ніж писати тригери. Їх логіка непрозора і вони неефективні порівняно з будь-яким іншим. Розбивання великих таблиць на більш дрібні для точного контролю - це лише природний наслідок кращої нормованої бази даних, а сама по собі не повинна бути предметом того, що слід турбувати.
Нейтрино

12

Я зазначу, що (функціонально) існує велика різниця між циклами та / або кількома шляхами в СХЕМІ та ДАНІ. Хоча цикли та, можливо, багатошаровість у DATA, безумовно, можуть ускладнювати обробку та спричиняти проблеми з продуктивністю (вартість "правильного" керування), вартість цих характеристик у схемі повинна бути близькою до нуля.

Оскільки найбільш очевидні цикли в RDB відбуваються в ієрархічних структурах (органна діаграма, частина, підрозділ тощо), невдало SQL Server вважає найгіршим; тобто цикл схем == цикл даних. Насправді, якщо ви використовуєте обмеження RI, ви фактично не можете побудувати цикл у даних!

Я підозрюю, що проблема багатосторонніх схожа; тобто декілька шляхів у схемі не обов'язково мають на увазі декілька шляхів у даних, але у мене є менший досвід з проблемою багатосторонніх маршрутів.

Звичайно , якщо SQL Server був дозволити цикли він все ще піддаватися на глибину 32, але це, ймовірно , досить для більшості випадків. (Шкода, що це не налаштування бази даних!)

Тригери "Замість видалення" також не працюють. Другий раз, коли відвідується таблиця, тригер ігнорується. Отже, якщо ви дійсно хочете імітувати каскад, вам доведеться використовувати збережені процедури за наявності циклів. Тригер "Замість видалення-видалення" буде працювати для випадків з кількома шляхами.

Челько пропонує "кращий" спосіб представити ієрархії, які не вводять циклів, але є компроміси.


"якщо ви використовуєте обмеження RI, ви фактично не можете побудувати цикл у даних!" -- гарна думка!
день, коли

Звичайно, ви можете створити циркулярність даних, але тільки з MSSQL, використовуючи UPDATE. Інші RDBM підтримують відкладені обмеження (цілісність забезпечується під час фіксації, а не під час вставки / оновлення / видалення).
Карл


3

За його звучанням у вас є дія OnDelete / OnUpdate на одному з існуючих зовнішніх ключів, яка змінює вашу таблицю кодів.

Таким чином, створивши цей зовнішній ключ, ви створили б циклічну проблему,

Наприклад, оновлення співробітників, спричиняє зміну кодів дією оновлення оновлення, спричиняє зміну співробітників за допомогою дії оновлення ... тощо ...

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


1
Вони досить довгі, тому я не думаю, що я можу їх розмістити тут, але я дуже вдячний за вашу допомогу - не знаю, чи є якийсь спосіб я можу надіслати їх вам? Спробую описати це: Єдині обмеження, які існують, складаються з 3 таблиць, у яких усі поля, що посилають коди, простою клавішею INT Id. Проблема, здається, полягає в тому, що у співробітника є кілька полів, на які посилається таблиця коду, і я хочу, щоб вони всі каскадували SET NULL. Все, що мені потрібно, - це те, що коли видаляються коди, посилання на них повинні бути встановлені на нуль всюди

опублікуйте їх у будь-якому випадку ... Я не думаю, що тут хтось заперечить, і вікно коду відформатує їх належним чином у прокручувальному блоці :)
Eoin Кемпбелл

2

Це пояснюється тим, що Emplyee може мати колекцію іншої сутності, наприклад, Кваліфікація та кваліфікація можуть мати якусь іншу університетську колекцію, наприклад

public class Employee{
public virtual ICollection<Qualification> Qualifications {get;set;}

}

public class Qualification{

public Employee Employee {get;set;}

public virtual ICollection<University> Universities {get;set;}

}

public class University{

public Qualification Qualification {get;set;}

}

На DataContext це може бути як нижче

protected override void OnModelCreating(DbModelBuilder modelBuilder){

modelBuilder.Entity<Qualification>().HasRequired(x=> x.Employee).WithMany(e => e.Qualifications);
modelBuilder.Entity<University>.HasRequired(x => x.Qualification).WithMany(e => e.Universities);

}

в цьому випадку існує ланцюг від працівника до кваліфікації та від кваліфікації до університетів. Тож це кидало той самий виняток для мене.

Це працювало для мене, коли я змінився

    modelBuilder.Entity<Qualification>().**HasRequired**(x=> x.Employee).WithMany(e => e.Qualifications); 

До

    modelBuilder.Entity<Qualification>().**HasOptional**(x=> x.Employee).WithMany(e => e.Qualifications);

1

Тригер є рішенням цієї проблеми:

IF OBJECT_ID('dbo.fktest2', 'U') IS NOT NULL
    drop table fktest2
IF OBJECT_ID('dbo.fktest1', 'U') IS NOT NULL
    drop table fktest1
IF EXISTS (SELECT name FROM sysobjects WHERE name = 'fkTest1Trigger' AND type = 'TR')
    DROP TRIGGER dbo.fkTest1Trigger
go
create table fktest1 (id int primary key, anQId int identity)
go  
    create table fktest2 (id1 int, id2 int, anQId int identity,
        FOREIGN KEY (id1) REFERENCES fktest1 (id)
            ON DELETE CASCADE
            ON UPDATE CASCADE/*,    
        FOREIGN KEY (id2) REFERENCES fktest1 (id) this causes compile error so we have to use triggers
            ON DELETE CASCADE
            ON UPDATE CASCADE*/ 
            )
go

CREATE TRIGGER fkTest1Trigger
ON fkTest1
AFTER INSERT, UPDATE, DELETE
AS
    if @@ROWCOUNT = 0
        return
    set nocount on

    -- This code is replacement for foreign key cascade (auto update of field in destination table when its referenced primary key in source table changes.
    -- Compiler complains only when you use multiple cascased. It throws this compile error:
    -- Rrigger Introducing FOREIGN KEY constraint on table may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, 
    -- or modify other FOREIGN KEY constraints.
    IF ((UPDATE (id) and exists(select 1 from fktest1 A join deleted B on B.anqid = A.anqid where B.id <> A.id)))
    begin       
        update fktest2 set id2 = i.id
            from deleted d
            join fktest2 on d.id = fktest2.id2
            join inserted i on i.anqid = d.anqid        
    end         
    if exists (select 1 from deleted)       
        DELETE one FROM fktest2 one LEFT JOIN fktest1 two ON two.id = one.id2 where two.id is null -- drop all from dest table which are not in source table
GO

insert into fktest1 (id) values (1)
insert into fktest1 (id) values (2)
insert into fktest1 (id) values (3)

insert into fktest2 (id1, id2) values (1,1)
insert into fktest2 (id1, id2) values (2,2)
insert into fktest2 (id1, id2) values (1,3)

select * from fktest1
select * from fktest2

update fktest1 set id=11 where id=1
update fktest1 set id=22 where id=2
update fktest1 set id=33 where id=3
delete from fktest1 where id > 22

select * from fktest1
select * from fktest2

0

Це помилка типу політик запуску бази даних. Тригер є кодом і може додати деякі інтелігенції або умови до каскадного відношення, наприклад, Cascade Deletion. Можливо, вам доведеться спеціалізувати параметри відповідних таблиць навколо цього типу, як Вимкнення CascadeOnDelete :

protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
    modelBuilder.Entity<TableName>().HasMany(i => i.Member).WithRequired().WillCascadeOnDelete(false);
}

Або вимкніть цю функцію повністю:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

-2

Моє рішення цієї проблеми, з якою виникло використання ASP.NET Core 2.0 та EF Core 2.0, полягає в тому, щоб виконати наступне для того, щоб:

  1. Запустіть update-databaseкоманду в консолі управління пакетами (PMC), щоб створити базу даних (це призводить до помилки "Введення обмеження FOREIGN KEY ... може спричинити цикли або кілька каскадних шляхів.")

  2. Запустіть script-migration -Idempotentкоманду в PMC, щоб створити сценарій, який можна запустити незалежно від існуючих таблиць / обмежень

  3. Візьміть отриманий скрипт і знайдіть ON DELETE CASCADEі замінітьON DELETE NO ACTION

  4. Виконати модифікований SQL проти бази даних

Тепер ваші міграції повинні бути сучасними, а каскадні делети не повинні відбуватися.

Шкода, що мені не вдалося знайти способу зробити це в Entity Framework Core 2.0.

Удачі!


Ви можете змінити міграційний файл для цього (не змінюючи скрипт sql), тобто у своєму файлі міграції можна встановити дію onDelete на Обмежити з Каскаду
Руші Соні

Краще вказати це за допомогою вільних анотацій, щоб вам не потрібно було це пам'ятати, якщо ви врешті-решт видалили та відтворили папку міграцій.
Аллен Ван

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