Il vincolo di chiave esterna può causare cicli o percorsi multipli in cascata?


177

Ho un problema quando provo ad aggiungere vincoli alle mie tabelle. Ottengo l'errore:

L'introduzione del vincolo FOREIGN KEY 'FK74988DB24B3C886' nella tabella 'Employee' può causare cicli o percorsi multipli in cascata. Specificare ON DELETE NO AZIONE o ON UPDATE NO AZIONE o modificare altri vincoli ESTERI CHIAVE.

Il mio vincolo è tra una Codetabella e una employeetabella. La Codetabella contiene Id, Name, FriendlyName, Typee Value. Il employeeha un certo numero di campi che codici di riferimento, in modo che ci può essere un riferimento per ogni tipo di codice.

Devo impostare i campi su null se il codice a cui viene fatto riferimento viene eliminato.

Qualche idea su come posso farlo?


Una delle soluzioni è qui
IsmailS

Risposte:


180

SQL Server esegue un semplice conteggio dei percorsi in cascata e, anziché cercare di capire se esistono effettivamente dei cicli, assume il peggio e si rifiuta di creare le azioni referenziali (CASCADE): è possibile e continuare a creare i vincoli senza le azioni referenziali. Se non riesci a modificare il tuo design (o farlo comprometterebbe le cose), dovresti considerare l'utilizzo dei trigger come ultima risorsa.

FWIW risolvere i percorsi in cascata è un problema complesso. Altri prodotti SQL semplicemente ignoreranno il problema e ti permetteranno di creare cicli, nel qual caso sarà una gara vedere quale sovrascriverà il valore per ultimo, probabilmente all'ignoranza del progettista (ad esempio ACE / Jet fa questo). Comprendo che alcuni prodotti SQL tenteranno di risolvere casi semplici. Il fatto rimane, SQL Server non ci prova nemmeno, lo gioca in modo ultra sicuro impedendo più di un percorso e almeno te lo dice.

Microsoft stessa consiglia l'uso di trigger anziché di vincoli FK.


2
una cosa che ancora non riesco a capire è che, se questo "problema" può essere risolto utilizzando un trigger, come mai un trigger non "causerà cicli o percorsi multipli in cascata ..."?
armen,

5
@armen: poiché il trigger fornirà esplicitamente la logica che il sistema non è in grado di capire implicitamente da solo, ad esempio se ci sono più percorsi per un'azione referenziale di eliminazione, il codice del trigger definirà quali tabelle vengono eliminate e in quale ordine.
giorno

6
Inoltre, il trigger viene eseguito al termine della prima operazione, quindi non è in corso alcuna gara.
Bon

2
@dumbledad: Voglio dire, utilizzare i trigger solo quando i vincoli (forse sulla combinazione) non riescono a fare il lavoro. I vincoli sono dichiarativi e le loro implementazioni sono di responsabilità del sistema. I trigger sono codici procedurali ed è necessario codificare (ed eseguire il debug) dell'implementazione e sopportarne gli svantaggi (prestazioni peggiori, ecc.).
onedayquando il

1
Il problema è che il trigger funziona solo fino a quando si rimuove il vincolo di chiave esterna, il che significa che non si ha alcun controllo di integrità referenziale sugli inserimenti del database e quindi sono necessari ancora più trigger per gestirlo. La soluzione trigger è una tana di coniglio che porta a un design degenerato del database.
Neutrino,

99

Una situazione tipica con più percorsi a cascata sarà questa: una tabella principale con due dettagli, diciamo "Master" e "Dettaglio1" e "Dettaglio2". Entrambi i dettagli sono eliminati in cascata. Finora nessun problema. Ma cosa succede se entrambi i dettagli hanno una relazione uno-a-molti con un'altra tabella (dire "SomeAltherTable"). SomeOtherTable ha una colonna Detail1ID E una colonna Detail2ID.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

In altre parole: alcuni dei record in SomeOtherTable sono collegati ai record Detail1 e alcuni dei record in SomeOtherTable sono collegati ai record Detail2. Anche se è garantito che i record SomeOtherTable non appartengono mai a entrambi i dettagli, ora è impossibile fare in modo che i record di SomeOhterTable vengano eliminati in cascata per entrambi i dettagli, poiché esistono più percorsi a cascata dal master a SomeOtherTable (uno tramite Detail1 e uno tramite Detail2). Ora potresti averlo già capito. Ecco una possibile soluzione:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Tutti i campi ID sono campi chiave e incremento automatico. Il punto cruciale risiede nei campi DetailMainId delle tabelle Detail. Questi campi sono contrapposti chiave e referenziale. Ora è possibile eliminare a cascata tutto cancellando solo i record master. Il rovescio della medaglia è che per ogni record detail1 E per ogni record detail2, deve esserci anche un record DetailMain (che viene effettivamente creato per primo per ottenere l'id corretto e univoco).


1
Il tuo commento mi ha aiutato molto a capire il problema che sto affrontando. Grazie! Preferirei disattivare l'eliminazione a cascata per uno dei percorsi, quindi gestire la cancellazione di altri record in altri modi (procedure memorizzate; trigger; per codice ecc.). Ma tengo a mente la tua soluzione (raggruppando in un percorso) per possibili diverse applicazioni dello stesso problema ...
Freewill

1
Uno su per l'uso della parola chiave (e anche per spiegare)
masterwok l'

È meglio che scrivere trigger? Sembra strano aggiungere una tabella aggiuntiva solo per far funzionare la cascata.
stupido il

Tutto è meglio della scrittura di trigger. La loro logica è opaca e sono inefficienti rispetto a qualsiasi altra cosa. Suddividere le tabelle di grandi dimensioni in tabelle più piccole per un controllo più preciso è solo una conseguenza naturale di un database meglio normalizzato e non di per sé qualcosa di cui preoccuparsi.
Neutrino,

12

Vorrei sottolineare che (funzionalmente) esiste una GRANDE differenza tra cicli e / o percorsi multipli in SCHEMA e DATA. Mentre i cicli e forse i multipath nei DATI potrebbero certamente complicare l'elaborazione e causare problemi di prestazioni (costo della gestione "corretta"), il costo di queste caratteristiche nello schema dovrebbe essere vicino allo zero.

Poiché i cicli più evidenti negli RDB si verificano in strutture gerarchiche (organigramma, parte, sottoparte, ecc.) È un peccato che SQL Server assuma il peggio; vale a dire, ciclo di schema == ciclo di dati. In effetti, se stai usando i vincoli RI non puoi effettivamente costruire un ciclo nei dati!

Sospetto che il problema multipath sia simile; vale a dire che più percorsi nello schema non implicano necessariamente più percorsi nei dati, ma ho meno esperienza con il problema multipath.

Naturalmente se SQL Server ha permettono cicli che sarebbe comunque soggetto ad una profondità di 32, ma questo è probabilmente sufficiente per la maggior parte dei casi. (Peccato che non sia un'impostazione del database!)

I trigger "Invece di Elimina" non funzionano neanche. La seconda volta che viene visitata una tabella, il trigger viene ignorato. Quindi, se vuoi davvero simulare una cascata, dovrai usare le procedure memorizzate in presenza di cicli. Il trigger Invece di Elimina funzionerebbe comunque per i casi multipath.

Celko suggerisce un modo "migliore" per rappresentare le gerarchie che non introducono cicli, ma ci sono compromessi.


"se stai usando i vincoli RI non puoi effettivamente costruire un ciclo nei dati!" - buon punto!
Onedayquando il

Sicuro che puoi costruire la circolarità dei dati, ma con MSSQL solo usando UPDATE. Altri RDBM supportano vincoli differiti (integrità garantita al momento del commit, non al momento dell'inserimento / aggiornamento / eliminazione).
Carl Krig,


3

A quanto pare, hai un'azione OnDelete / OnUpdate su una delle tue chiavi esterne esistenti, che modificherà la tabella dei codici.

Quindi creando questa chiave esterna, creeresti un problema ciclico,

Ad es. Aggiornamento dei dipendenti, causa la modifica dei codici mediante un'azione all'aggiornamento, causa la modifica dei dipendenti da un'azione all'aggiornamento ... ecc.

Se pubblichi le definizioni delle tabelle per entrambe le tabelle e le definizioni di chiave esterna / vincolo dovremmo essere in grado di dirti dove si trova il problema ...


1
Sono abbastanza lunghi, quindi non credo di poterli pubblicare qui, ma apprezzerei molto il tuo aiuto - non sai se c'è un modo per inviarli a te? Cercherò di descriverlo: gli unici vincoli che esistono sono da 3 tabelle che hanno tutti campi che fanno riferimento a codici mediante una semplice chiave ID INT. Il problema sembra essere che il Dipendente ha diversi campi che fanno riferimento alla tabella dei codici e che li voglio tutti in cascata a SET NULL. Tutto ciò di cui ho bisogno è che quando i codici vengono eliminati, i riferimenti ad essi dovrebbero essere impostati su null ovunque.

pubblicali comunque ... Non credo che a nessuno qui dispiacerà, e la finestra del codice li formatterà correttamente in un blocco a scorrimento :)
Eoin Campbell,

2

Questo perché Emplyee potrebbe avere Raccolta di altre entità dire Qualifiche e Qualificazione potrebbe avere alcune altre università di raccolta ad 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;}

}

Su DataContext potrebbe essere come sotto

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

}

in questo caso esiste una catena da Dipendente a Qualifica e Da Qualifica a Università. Quindi stava gettando la stessa eccezione per me.

Ha funzionato per me quando ho cambiato

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

Per

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

1

Il trigger è la soluzione per questo 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

0

Questo è un errore di tipo criteri di attivazione del database. Un trigger è un codice e può aggiungere alcune informazioni o condizioni a una relazione Cascade come Eliminazione in cascata. Potrebbe essere necessario specializzare le opzioni relative alle tabelle correlate attorno a questo, come Disattivare CascadeOnDelete :

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

Oppure disattiva completamente questa funzione:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

-2

La mia soluzione a questo problema riscontrato utilizzando ASP.NET Core 2.0 ed EF Core 2.0 era di eseguire le seguenti operazioni nell'ordine:

  1. Eseguire il update-databasecomando in Package Management Console (PMC) per creare il database (ciò si traduce nell'errore "Presentazione del vincolo FOREIGN KEY ... può causare cicli o più percorsi a cascata.")

  2. Esegui il script-migration -Idempotentcomando in PMC per creare uno script che può essere eseguito indipendentemente dalle tabelle / vincoli esistenti

  3. Prendi lo script risultante e trova ON DELETE CASCADEe sostituisci conON DELETE NO ACTION

  4. Eseguire l'SQL modificato sul database

Ora, le migrazioni dovrebbero essere aggiornate e le eliminazioni a cascata non dovrebbero avvenire.

Peccato che non sono riuscito a trovare un modo per farlo in Entity Framework Core 2.0.

In bocca al lupo!


È possibile modificare il file di migrazione per farlo (senza modificare lo script sql), ovvero nel file di migrazione è possibile impostare l'
opzione Elimina

È meglio specificarlo utilizzando annotazioni fluide in modo da non dover ricordare di farlo se si finisce per eliminare e ricreare la cartella delle migrazioni.
Allen Wang

Nella mia esperienza, le annotazioni fluide possono essere usate e dovrebbero essere usate (io le uso) ma spesso sono piuttosto buggy. La semplice specificazione nel codice non sempre funziona produce il risultato atteso.
user1477388
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.