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

177

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

Введение ограничения FOREIGN KEY «FK74988DB24B3C886» в таблицу «Сотрудник» может привести к возникновению циклов или нескольких каскадных путей. Укажите ON DELETE NO ACTION или ON UPDATE NO ACTION или измените другие ограничения FOREIGN KEY.

Мое ограничение между Codeтаблицей и employeeтаблицей. CodeТаблица содержит Id, Name, FriendlyName, Typeи Value. employeeИмеет ряд полей, ссылочные коды, так что может быть ссылка для каждого типа кода.

Мне нужно, чтобы поля были установлены в нуль, если код, на который ссылается, удален.

Есть идеи, как я могу это сделать?

Рикардо Альтамирано
источник
Одно из решений здесь
IsmailS

Ответы:

180

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

FWIW разрешение каскадных путей - сложная проблема. Другие продукты SQL просто проигнорируют проблему и позволят вам создавать циклы, и в этом случае будет гонка, чтобы увидеть, кто перезапишет значение последним, вероятно, по незнанию дизайнера (например, ACE / Jet делает это). Я понимаю, что некоторые продукты SQL будут пытаться разрешить простые случаи. Факт остается фактом, SQL Server даже не пытается, он играет очень безопасно, запретив более одного пути и, по крайней мере, говорит вам об этом.

Сами Microsoft советует использовать триггеры вместо ограничений FK.

onedaywhen
источник
2
Одна вещь, которую я до сих пор не могу понять, это то, что, если эту «проблему» можно решить с помощью триггера, то почему триггер не «вызовет циклы или несколько каскадных путей ...»?
армен
5
@armen: поскольку ваш триггер будет явно предоставлять логику, которую система не могла бы самостоятельно определить, например, если существует несколько путей для удаления ссылочного действия, тогда ваш код триггера определит, какие таблицы будут удалены и в каком порядке.
понедельник,
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 таблиц деталей. Эти поля являются ключевыми и ссылочными противоречиями. Теперь можно каскадно удалять все, удаляя только основные записи. Недостатком является то, что для каждой записи detail1 И для каждой записи detail2 также должна существовать запись DetailMain (которая фактически создается первой, чтобы получить правильный и уникальный идентификатор).

Ханс Ризебос
источник
1
Ваш комментарий очень помог мне понять проблему, с которой я столкнулся. Спасибо! Я бы предпочел отключить каскадное удаление для одного из путей, а затем обработать удаление других записей другими способами (хранимые процедуры; триггеры; по коду и т. Д.). Но я продолжаю свое решение (группировка в одном пути) в виде для различных возможных применений того же проблем ...
доброхотный
1
Один за использование слова суть (а также для объяснения)
masterwok
Это лучше, чем писать триггеры? Кажется странным добавлять дополнительную таблицу только для того, чтобы заставить работать каскад.
Дамблдад
Все лучше, чем писать триггеры. Их логика непрозрачна, и они неэффективны по сравнению с чем-либо еще. Разбиение больших таблиц на более мелкие для более точного управления - это просто естественное следствие лучшей нормализации базы данных, а не само по себе то, о чем следует беспокоиться.
Нейтрино
12

Я хотел бы отметить, что (функционально) существует большая разница между циклами и / или несколькими путями в SCHEMA и DATA. Хотя циклы и, возможно, многолучевое распространение в DATA могут, безусловно, усложнять обработку и вызывать проблемы с производительностью (стоимость «правильной» обработки), стоимость этих характеристик в схеме должна быть близка к нулю.

Поскольку наиболее очевидные циклы в RDB происходят в иерархических структурах (оргструктура, деталь, подраздел и т. Д.), К сожалению, SQL Server предполагает худшее; т.е. цикл схемы == цикл данных. На самом деле, если вы используете ограничения RI, вы не можете построить цикл в данных!

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

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

Триггеры «вместо удаления» тоже не работают. При втором посещении таблицы триггер игнорируется. Итак, если вы действительно хотите смоделировать каскад, вам придется использовать хранимые процедуры при наличии циклов. Вместо этого триггер вместо удаления будет работать для случаев многолучевого распространения.

Celko предлагает «лучший» способ представления иерархий, в котором нет циклов, но есть компромиссы.

Билл Кохаган
источник
«Если вы используете ограничения RI, вы не можете построить цикл в данных!» -- хорошая точка зрения!
понедельник,
Конечно, вы можете построить цикличность данных, но с MSSQL только с использованием UPDATE. Другие RDBM поддерживают отложенные ограничения (целостность обеспечивается во время фиксации, а не во время вставки / обновления / удаления).
Карл Криг
3

Судя по всему, у вас есть действие OnDelete / OnUpdate для одного из ваших существующих внешних ключей, которое изменит вашу таблицу кодов.

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

Например, при обновлении сотрудников происходит изменение кодов с помощью действия при обновлении, при изменении действия сотрудников с помощью действия при обновлении ... и т. Д ...

Если вы опубликуете свои определения таблиц для обеих таблиц, а также определения внешнего ключа / ограничения, мы сможем сообщить вам, в чем проблема ...

Эойн Кэмпбелл
источник
1
Они довольно длинные, поэтому я не думаю, что смогу опубликовать их здесь, но я был бы очень признателен за вашу помощь - не знаете, есть ли способ, которым я могу отправить их вам? Я попытаюсь описать это: единственные существующие ограничения - это 3 таблицы, в каждой из которых есть поля, которые ссылаются на коды с помощью простого ключа INT Id. Кажется, проблема в том, что у Employee есть несколько полей, которые ссылаются на таблицу кодов, и что я хочу, чтобы все они каскадно указывали на SET NULL. Все, что мне нужно, это то, что при удалении кодов ссылки на них должны быть везде равны нулю.
отправлять их в любом случае ... Я не думаю, что кто-то здесь будет возражать, и окно кода отформатирует их должным образом в блоке прокрутки :)
Eoin Campbell
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);
Rajnikant
источник
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

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

Удачи!

user1477388
источник
Для этого вы можете изменить файл миграции (без изменения сценария sql), т. Е. В файле миграции вы можете установить для действия onDelete Ограничение от каскада
Rushi Soni
Лучше указать это, используя текущие аннотации, чтобы вам не приходилось делать это, если вы в конечном итоге удалите и заново создадите папку миграции.
Аллен Ван
По моему опыту, текущие аннотации могут использоваться и должны использоваться (я использую их), но они часто бывают довольно глючными. Простое указание их в коде не всегда дает ожидаемый результат.
user1477388