A restrição de chave estrangeira pode causar ciclos ou vários caminhos em cascata?

176

Tenho um problema ao tentar adicionar restrições às minhas tabelas. Eu recebo o erro:

A introdução da restrição FOREIGN KEY 'FK74988DB24B3C886' na tabela 'Funcionário' pode causar ciclos ou vários caminhos em cascata. Especifique ON DELETE NO ACTION ou ON UPDATE NO ACTION ou modifique outras restrições de FOREIGN KEY.

Minha restrição é entre uma Codemesa e uma employeemesa. A Codetabela contém Id, Name, FriendlyName, Typee Value. O employeepossui um número de campos que referenciam códigos, para que possa haver uma referência para cada tipo de código.

Eu preciso que os campos sejam definidos como nulos se o código referenciado for excluído.

Alguma idéia de como posso fazer isso?

Ricardo Altamirano
fonte
Uma das soluções está aqui
IsmailS 7/12/12

Respostas:

180

O SQL Server faz uma contagem simples de caminhos em cascata e, em vez de tentar descobrir se existe algum ciclo, assume o pior e se recusa a criar as ações referenciais (CASCADE): você pode e ainda deve criar as restrições sem as ações referenciais. Se você não pode alterar seu design (ou isso comprometeria as coisas), considere o uso de gatilhos como último recurso.

A resolução de caminhos em cascata do FWIW é um problema complexo. Outros produtos SQL simplesmente ignoram o problema e permitem criar ciclos. Nesse caso, será uma corrida para ver quem substituirá o valor por último, provavelmente por ignorância do designer (por exemplo, o ACE / Jet faz isso). Entendo que alguns produtos SQL tentarão resolver casos simples. Fato permanece: o SQL Server nem tenta, o reproduz de maneira ultra segura ao proibir mais de um caminho e, pelo menos, diz isso.

A própria Microsoft aconselha o uso de gatilhos em vez de restrições de FK.

um dia quando
fonte
2
Uma coisa que ainda não consigo entender é que, se esse "problema" puder ser resolvido usando um gatilho, como é que um gatilho não "causará ciclos ou vários caminhos em cascata ..."?
armen
5
@armen: porque seu gatilho fornecerá explicitamente a lógica que o sistema não pode implicitamente descobrir por si própria, por exemplo, se houver vários caminhos para uma ação referencial de exclusão, seu código de gatilho definirá quais tabelas serão excluídas e em que ordem.
precisa saber é o seguinte
6
E também o gatilho é executado após a primeira operação, para que não haja corrida.
Bon
2
@ Dumbledad: Quero dizer, use apenas gatilhos quando restrições (talvez em combinação) não puderem fazer o trabalho. As restrições são declarativas e suas implementações são de responsabilidade do sistema. Os gatilhos são códigos procedurais e você deve codificar (e depurar) a implementação e suportar suas desvantagens (pior desempenho, etc.).
onedaywhen
1
O problema é que o gatilho funciona apenas desde que você remova a restrição de chave estrangeira, o que significa que você não tem verificação de integridade referencial nas inserções do banco de dados e, portanto, é necessário ainda mais gatilhos para lidar com isso. A solução de gatilho é uma toca de coelho que leva a um design de banco de dados degenerado.
Neutrino
99

Uma situação típica com vários caminhos de cascasing será a seguinte: Uma tabela mestre com dois detalhes, digamos "Mestre" e "Detalhe1" e "Detalhe2". Ambos os detalhes são exclusão em cascata. Até agora sem problemas. Mas e se os dois detalhes tiverem uma relação um-para-muitos com alguma outra tabela (diga "SomeOtherTable"). SomeOtherTable possui uma coluna Detail1ID E uma coluna Detail2ID.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

Em outras palavras: alguns dos registros em SomeOtherTable estão vinculados aos registros Detail1 e alguns dos registros em SomeOtherTable estão vinculados aos registros Detail2. Mesmo que seja garantido que SomeOtherTable-records nunca pertençam a ambos os Detalhes, agora é impossível fazer com que os registros de SomeOhterTable sejam excluídos em cascata dos dois detalhes, porque existem vários caminhos em cascata do Master para SomeOtherTable (um via Detail1 e outro via Detail2). Agora você já deve ter entendido isso. Aqui está uma solução possível:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Todos os campos de ID são campos-chave e incremento automático. O ponto crucial está nos campos DetailMainId das tabelas Detail. Esses campos são chave e referência referencial. Agora é possível excluir tudo em cascata excluindo apenas registros mestre. A desvantagem é que, para cada registro detail1 E para cada registro detail2, também deve haver um registro DetailMain (que é realmente criado primeiro para obter a identificação correta e exclusiva).

hans riesebos
fonte
1
Seu comentário me ajudou muito a entender o problema que estou enfrentando. Obrigado! Eu preferiria desativar a exclusão em cascata para um dos caminhos e manipular a exclusão de outros registros de outras maneiras (procedimentos armazenados; gatilhos; por código, etc.). Mas eu mantenho sua solução (agrupamento em um caminho) em mente para possíveis aplicações diferentes do mesmo problema ... #
22714
1
Um para uso da palavra crux (e também para explicar)
masterwok
Isso é melhor do que escrever gatilhos? Parece estranho adicionar uma tabela adicional apenas para fazer a cascata funcionar.
dumbledad
Qualquer coisa é melhor do que escrever gatilhos. Sua lógica é opaca e eles são ineficientes em comparação com qualquer outra coisa. Dividir tabelas grandes em tabelas menores para um controle mais refinado é apenas uma consequência natural de um banco de dados melhor normalizado e não é algo em que se preocupar.
Neutrino
12

Eu apontaria que (funcionalmente) há uma GRANDE diferença entre ciclos e / ou caminhos múltiplos no SCHEMA e nos DATA. Embora os ciclos e talvez os caminhos múltiplos nos DATA certamente possam complicar o processamento e causar problemas de desempenho (custo de manuseio "adequado"), o custo dessas características no esquema deve ser próximo de zero.

Como a maioria dos ciclos aparentes nos RDBs ocorre em estruturas hierárquicas (organograma, parte, subparte etc.), é lamentável que o SQL Server assuma o pior; ou seja, ciclo de esquema == ciclo de dados. De fato, se você estiver usando restrições de RI, não poderá criar um ciclo nos dados!

Suspeito que o problema de caminhos múltiplos seja semelhante; ou seja, vários caminhos no esquema não implicam necessariamente vários caminhos nos dados, mas tenho menos experiência com o problema de caminhos múltiplos.

Claro, se SQL Server que permitem ciclos que ainda estaria sujeita a uma profundidade de 32, mas isso é provavelmente adequado para a maioria dos casos. (Pena que não é uma configuração de banco de dados!)

Os gatilhos "Em vez de excluir" também não funcionam. Na segunda vez que uma tabela é visitada, o gatilho é ignorado. Portanto, se você realmente deseja simular uma cascata, precisará usar procedimentos armazenados na presença de ciclos. No entanto, o gatilho Em vez de excluir excluir funcionaria para casos de caminhos múltiplos.

Celko sugere uma maneira "melhor" de representar hierarquias que não introduzem ciclos, mas existem compensações.

Bill Cohagan
fonte
"se você estiver usando restrições de RI, não poderá criar um ciclo nos dados!" - bom ponto!
onedaywhen
Claro que você pode construir a circularidade dos dados, mas com o MSSQL apenas usando UPDATE. Outros RDBMs suportam restrições adiadas (integridade garantida no momento da confirmação, não no momento da inserção / atualização / exclusão).
22819 Carl Krig
3

Pelo que parece, você tem uma ação OnDelete / OnUpdate em uma de suas chaves estrangeiras existentes, que modificará sua tabela de códigos.

Então, ao criar essa chave estrangeira, você criaria um problema cíclico,

Por exemplo, a atualização de funcionários, faz com que os códigos sejam alterados por uma ação na atualização, faz com que os funcionários sejam alterados por uma ação na atualização ... etc ...

Se você publicar suas Definições de tabela para ambas as tabelas e suas definições de chave estrangeira / restrição, poderemos informar onde está o problema ...

Eoin Campbell
fonte
1
Eles são bastante longos, então não acho que posso publicá-los aqui, mas agradeceria sua ajuda - não sei se há alguma maneira de enviá-los para você? Vou tentar descrevê-lo: As únicas restrições existentes são de 3 tabelas que possuem campos que referenciam códigos por uma simples chave de ID INT. O problema parece ser que Employee possui vários campos que referenciam a tabela de códigos e que eu quero que todos eles façam cascata para SET NULL. Tudo o que preciso é que, quando os códigos forem excluídos, as referências a eles sejam definidas como nulas em qualquer lugar.
publicá-las de qualquer maneira ... Eu não acho que ninguém aqui vai se importar, e a janela de código irá formatá-los corretamente em um bloco de rolagem :)
Eoin Campbell
2

Isso ocorre porque o Emplyee pode ter Coleção de outra entidade, digamos, Qualificações e Qualificação podem ter outras coleções.

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;}

}

No DataContext, pode ser como abaixo

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);

}

nesse caso, existe uma cadeia de funcionário para qualificação e de qualificação para universidades. Então estava lançando a mesma exceção para mim.

Funcionou para mim quando mudei

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

Para

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

O gatilho é a solução para este problema:

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
Tom Škoda
fonte
0

Este é um erro do tipo políticas de gatilho de banco de dados. Um gatilho é um código e pode adicionar algumas inteligências ou condições a uma relação em cascata, como exclusão em cascata. Pode ser necessário especializar as opções de tabelas relacionadas, como Desativar o CascadeOnDelete :

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

Ou desative esse recurso completamente:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
Amirhossein Mehrvarzi
fonte
-2

Minha solução para esse problema encontrada usando o ASP.NET Core 2.0 e o EF Core 2.0 foi executar o seguinte na ordem:

  1. Execute o update-databasecomando no Package Management Console (PMC) para criar o banco de dados (isso resulta no erro "Introdução à restrição FOREIGN KEY ... pode causar ciclos ou vários caminhos em cascata".)

  2. Executar script-migration -Idempotentcomando no PMC para criar um script que possa ser executado independentemente das tabelas / restrições existentes

  3. Pegue o script resultante e encontre ON DELETE CASCADEe substitua porON DELETE NO ACTION

  4. Execute o SQL modificado no banco de dados

Agora, suas migrações devem estar atualizadas e as exclusões em cascata não devem ocorrer.

Pena que não consegui encontrar nenhuma maneira de fazer isso no Entity Framework Core 2.0.

Boa sorte!

user1477388
fonte
Você pode alterar seu arquivo de migração para fazê-lo (sem alterar o script sql), ou seja, no seu arquivo de migração, você pode definir a ação onDelete para Restringir de Cascade
Rushi Soni
É melhor especificar isso usando anotações fluentes, para que você não precise se lembrar de fazer isso se acabar excluindo e recriando sua pasta de migrações.
Allen Wang
Na minha experiência, as anotações fluentes podem ser usadas e devem ser usadas (eu as uso), mas geralmente são bastante complicadas. Simplesmente especificá-los no código nem sempre funciona produz o resultado esperado.
User1477388