Fremdschlüsseleinschränkung kann Zyklen oder mehrere Kaskadenpfade verursachen?


176

Ich habe ein Problem, wenn ich versuche, meinen Tabellen Einschränkungen hinzuzufügen. Ich bekomme den Fehler:

Die Einführung der FOREIGN KEY-Einschränkung 'FK74988DB24B3C886' in Tabelle 'Employee' kann zu Zyklen oder mehreren Kaskadenpfaden führen. Geben Sie ON DELETE NO ACTION oder ON UPDATE NO ACTION an oder ändern Sie andere FOREIGN KEY-Einschränkungen.

Meine Einschränkung liegt zwischen einer CodeTabelle und einer employeeTabelle. Die CodeTabelle enthält Id, Name, FriendlyName, Typeund ein Value. Das employeehat eine Reihe von Feldern, die auf Codes verweisen, so dass es für jeden Codetyp eine Referenz geben kann.

Ich muss die Felder auf null setzen, wenn der Code, auf den verwiesen wird, gelöscht wird.

Irgendwelche Ideen, wie ich das machen kann?


Eine der Lösungen ist hier
IsmailS

Antworten:


180

SQL Server zählt einfach Kaskadenpfade und versucht nicht herauszufinden, ob tatsächlich Zyklen vorhanden sind, sondern nimmt das Schlimmste an und weigert sich, die referenziellen Aktionen (CASCADE) zu erstellen: Sie können und sollten die Einschränkungen auch ohne die referenziellen Aktionen erstellen. Wenn Sie Ihr Design nicht ändern können (oder dies die Dinge gefährden würde), sollten Sie in Betracht ziehen, Trigger als letzten Ausweg zu verwenden.

Das Auflösen von Kaskadenpfaden durch FWIW ist ein komplexes Problem. Andere SQL-Produkte ignorieren das Problem einfach und ermöglichen es Ihnen, Zyklen zu erstellen. In diesem Fall ist es ein Wettlauf, zu sehen, welche den Wert zuletzt überschreiben, wahrscheinlich aus Unwissenheit des Designers (z. B. ACE / Jet tut dies). Ich verstehe, dass einige SQL-Produkte versuchen werden, einfache Fälle zu lösen. Tatsache bleibt, SQL Server versucht es nicht einmal, geht auf Nummer sicher, indem mehr als ein Pfad nicht zugelassen wird, und zumindest sagt es Ihnen dies.

Microsoft selbst empfiehlt die Verwendung von Triggern anstelle von FK-Einschränkungen.


2
Eine Sache, die ich immer noch nicht verstehen kann, ist, dass, wenn dieses "Problem" mit einem Trigger gelöst werden kann, warum ein Trigger dann nicht "Zyklen oder mehrere Kaskadenpfade verursacht ..."?
Armen

5
@armen: Da Ihr Trigger explizit die Logik liefert, die das System nicht implizit selbst herausfinden konnte, z. B. wenn es mehrere Pfade für eine referenzielle Löschaktion gibt, definiert Ihr Triggercode, welche Tabellen in welcher Reihenfolge gelöscht werden.
Tag, wenn

6
Außerdem wird der Auslöser nach Abschluss der ersten Operation ausgeführt, sodass kein Rennen stattfindet.
Bon

2
@dumbledad: Ich meine, verwenden Sie Trigger nur, wenn Einschränkungen (möglicherweise in Kombination) den Job nicht erledigen können. Einschränkungen sind deklarativ und ihre Implementierung liegt in der Verantwortung des Systems. Trigger sind prozeduraler Code, und Sie müssen die Implementierung codieren (und debuggen) und ihre Nachteile (schlechtere Leistung usw.) ertragen.
Tag, wenn

1
Das Problem dabei ist, dass der Trigger nur funktioniert, solange Sie die Fremdschlüsseleinschränkung entfernen. Dies bedeutet, dass Sie dann keine referenzielle Integritätsprüfung für Datenbankeinfügungen durchführen und daher noch mehr Trigger benötigen, um dies zu handhaben. Die Triggerlösung ist ein Kaninchenbau, der zu einem entarteten Datenbankdesign führt.
Neutrino

99

Eine typische Situation mit mehreren Kaskadenpfaden ist folgende: Eine Mastertabelle mit zwei Details, beispielsweise "Master" und "Detail1" und "Detail2". Beide Details werden kaskadiert gelöscht. Bisher keine Probleme. Was aber, wenn beide Details eine Eins-zu-Viele-Beziehung zu einer anderen Tabelle haben (sagen Sie "SomeOtherTable")? SomeOtherTable hat eine Detail1ID-Spalte UND eine Detail2ID-Spalte.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

Mit anderen Worten: Einige der Datensätze in SomeOtherTable sind mit Detail1-Datensätzen verknüpft, und einige der Datensätze in SomeOtherTable sind mit Detail2-Datensätzen verknüpft. Selbst wenn garantiert ist, dass SomeOtherTable-Datensätze niemals zu beiden Details gehören, ist es jetzt unmöglich, die Datensätze von SomeOhterTable für beide Details zu kaskadieren, da es mehrere Kaskadenpfade von Master zu SomeOtherTable gibt (einen über Detail1 und einen über Detail2). Jetzt haben Sie das vielleicht schon verstanden. Hier ist eine mögliche Lösung:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Alle ID-Felder sind Schlüsselfelder und werden automatisch erhöht. Der Kern liegt in den DetailMainId-Feldern der Detailtabellen. Diese Felder sind sowohl Schlüssel- als auch Referenzkontraint. Es ist jetzt möglich, alles kaskadiert zu löschen, indem nur Stammsätze gelöscht werden. Der Nachteil ist, dass für jeden Detail1-Datensatz UND für jeden Detail2-Datensatz auch ein DetailMain-Datensatz vorhanden sein muss (der tatsächlich zuerst erstellt wird, um die richtige und eindeutige ID zu erhalten).


1
Ihr Kommentar hat mir sehr geholfen, das Problem zu verstehen, mit dem ich konfrontiert bin. Danke dir! Ich würde es vorziehen, die Kaskadenlöschung für einen der Pfade zu deaktivieren und dann das Löschen anderer Datensätze auf andere Weise zu handhaben (gespeicherte Prozeduren, Trigger, Code usw.). Aber ich denke an Ihre Lösung (Gruppierung in einem Pfad) für mögliche unterschiedliche Anwendungen desselben Problems ...
freewill

1
Eine für die Verwendung des Wortes Crux (und auch zur Erklärung)
Masterwok

Ist das besser als das Schreiben von Triggern? Es scheint seltsam, eine zusätzliche Tabelle hinzuzufügen, um die Kaskade zum Laufen zu bringen.
Dumbledad

Alles ist besser als Trigger zu schreiben. Ihre Logik ist undurchsichtig und sie sind im Vergleich zu allem anderen ineffizient. Das Aufteilen großer Tabellen in kleinere Tabellen zur genaueren Kontrolle ist nur eine natürliche Folge einer besser normalisierten Datenbank und an sich kein Problem.
Neutrino

12

Ich möchte darauf hinweisen, dass es (funktional) einen GROSSEN Unterschied zwischen Zyklen und / oder mehreren Pfaden im SCHEMA und in den DATEN gibt. Während Zyklen und möglicherweise Mehrwege in den DATEN die Verarbeitung sicherlich komplizieren und Leistungsprobleme verursachen könnten (Kosten für "ordnungsgemäße" Behandlung), sollten die Kosten für diese Merkmale im Schema nahe Null liegen.

Da die meisten offensichtlichen Zyklen in RDBs in hierarchischen Strukturen (Organigramm, Teil, Unterteil usw.) auftreten, ist es bedauerlich, dass SQL Server das Schlimmste annimmt. dh Schema-Zyklus == Datenzyklus. Wenn Sie RI-Einschränkungen verwenden, können Sie tatsächlich keinen Zyklus in den Daten erstellen!

Ich vermute, dass das Mehrwegproblem ähnlich ist; Das heißt, mehrere Pfade im Schema implizieren nicht unbedingt mehrere Pfade in den Daten, aber ich habe weniger Erfahrung mit dem Multipath-Problem.

Natürlich , wenn SQL Server hat Zyklen erlauben es noch unter dem Vorbehalt einer Tiefe von 32 sein würde, aber das ist wahrscheinlich ausreichend für die meisten Fälle. (Schade, dass dies jedoch keine Datenbankeinstellung ist!)

"Statt zu löschen" -Trigger funktionieren auch nicht. Beim zweiten Besuch einer Tabelle wird der Trigger ignoriert. Wenn Sie also wirklich eine Kaskade simulieren möchten, müssen Sie gespeicherte Prozeduren bei Vorhandensein von Zyklen verwenden. Der Trigger "Statt zu löschen" würde jedoch für Mehrwegfälle funktionieren.

Celko schlägt einen "besseren" Weg vor, um Hierarchien darzustellen, der keine Zyklen einführt, aber es gibt Kompromisse.


"Wenn Sie RI-Einschränkungen verwenden, können Sie keinen Zyklus in den Daten erstellen!" -- guter Punkt!
Tag, wenn

Sicher können Sie Datenzirkularität erstellen, aber mit MSSQL nur mit UPDATE. Andere RDBMs unterstützen verzögerte Einschränkungen (Integrität zum Zeitpunkt des Festschreibens sichergestellt, nicht zum Zeitpunkt des Einfügens / Aktualisierens / Löschens).
Carl Krig


3

Laut Sound haben Sie eine OnDelete / OnUpdate-Aktion auf einem Ihrer vorhandenen Fremdschlüssel, mit der Sie Ihre Codetabelle ändern können.

Wenn Sie also diesen Fremdschlüssel erstellen, entsteht ein zyklisches Problem.

ZB das Aktualisieren von Mitarbeitern, bewirkt, dass Codes durch eine On Update-Aktion geändert werden, dass Mitarbeiter durch eine On Update-Aktion geändert werden ... etc ...

Wenn Sie Ihre Tabellendefinitionen für beide Tabellen und Ihre Fremdschlüssel- / Einschränkungsdefinitionen veröffentlichen, sollten wir Ihnen mitteilen können, wo das Problem liegt ...


1
Sie sind ziemlich lang, daher glaube ich nicht, dass ich sie hier posten kann, aber ich würde mich sehr über Ihre Hilfe freuen - wissen Sie nicht, ob ich sie Ihnen auf irgendeine Weise senden kann? Ich versuche es zu beschreiben: Die einzigen Einschränkungen, die existieren, stammen aus 3 Tabellen, die alle Felder haben, die mit einem einfachen INT-ID-Schlüssel auf Codes verweisen. Das Problem scheint zu sein, dass der Mitarbeiter mehrere Felder hat, die auf die Codetabelle verweisen, und dass ich möchte, dass sie alle zu SET NULL kaskadieren. Ich brauche nur, dass beim Löschen von Codes die Verweise darauf überall auf null gesetzt werden.

poste sie trotzdem ... Ich glaube, niemand hier wird etwas dagegen haben, und das Codefenster wird sie in einem Bildlaufblock richtig formatieren :)
Eoin Campbell

2

Dies liegt daran, dass der Mitarbeiter möglicherweise über eine Sammlung anderer Unternehmen verfügt, z. B. über Qualifikationen und über die Qualifikation über einige andere Sammlungsuniversitäten, z

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

}}

Auf DataContext könnte es wie folgt sein

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 diesem Fall gibt es eine Kette vom Mitarbeiter zur Qualifikation und von der Qualifikation zur Universität. Also warf es mir die gleiche Ausnahme zu.

Es hat bei mir funktioniert, als ich mich verändert habe

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

Zu

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

1

Trigger ist die Lösung für dieses Problem:

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

Dies ist ein Fehler vom Typ Datenbank-Triggerrichtlinien. Ein Trigger ist Code und kann einer Kaskadenbeziehung wie Kaskadenlöschung einige Intelligenzen oder Bedingungen hinzufügen. Möglicherweise müssen Sie die zugehörigen Tabellenoptionen darauf spezialisieren , z. B. Deaktivieren von CascadeOnDelete :

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

Oder Schalten Sie diese Funktion vollständig aus:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

-2

Meine Lösung für dieses Problem mit ASP.NET Core 2.0 und EF Core 2.0 bestand darin, Folgendes in der angegebenen Reihenfolge auszuführen:

  1. Führen Sie den update-databaseBefehl in der Package Management Console (PMC) aus, um die Datenbank zu erstellen (dies führt zu dem Fehler "Einführung der FOREIGN KEY-Einschränkung ... kann Zyklen oder mehrere Kaskadenpfade verursachen").

  2. Führen Sie den script-migration -IdempotentBefehl in PMC aus, um ein Skript zu erstellen, das unabhängig von den vorhandenen Tabellen / Einschränkungen ausgeführt werden kann

  3. Nehmen Sie das resultierende Skript und suchen ON DELETE CASCADEund ersetzen Sie es durchON DELETE NO ACTION

  4. Führen Sie die geänderte SQL für die Datenbank aus

Jetzt sollten Ihre Migrationen auf dem neuesten Stand sein und die kaskadierenden Löschvorgänge sollten nicht stattfinden.

Schade, dass ich in Entity Framework Core 2.0 keine Möglichkeit gefunden habe, dies zu tun.

Viel Glück!


Sie können Ihre Migrationsdatei dazu ändern (ohne das SQL-Skript zu ändern), dh in Ihrer Migrationsdatei können Sie die Aktion onDelete auf Restrict from Cascade
Rushi Soni

Es ist besser, dies mit fließenden Anmerkungen anzugeben, damit Sie nicht daran denken müssen, dies zu tun, wenn Sie Ihren Migrationsordner löschen und neu erstellen.
Allen Wang

Nach meiner Erfahrung können und sollten die fließenden Anmerkungen verwendet werden (ich verwende sie), aber sie sind oft ziemlich fehlerhaft. Die einfache Angabe im Code funktioniert nicht immer und führt zum erwarteten Ergebnis.
user1477388
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.