La contrainte de clé étrangère peut provoquer des cycles ou plusieurs chemins en cascade?

176

J'ai un problème lorsque j'essaye d'ajouter des contraintes à mes tables. J'obtiens l'erreur:

L'introduction de la contrainte FOREIGN KEY 'FK74988DB24B3C886' sur la table 'Employee' peut provoquer des cycles ou plusieurs chemins en cascade. Spécifiez ON DELETE NO ACTION ou ON UPDATE NO ACTION, ou modifiez d'autres contraintes FOREIGN KEY.

Ma contrainte est entre une Codetable et une employeetable. Le Codetableau contient Id, Name, FriendlyName, Typeet Value. Le employeea un certain nombre de champs qui référencent des codes, de sorte qu'il puisse y avoir une référence pour chaque type de code.

J'ai besoin que les champs soient définis sur null si le code référencé est supprimé.

Des idées comment je peux faire cela?

Ricardo Altamirano
la source
Une des solutions est ici
IsmailS

Réponses:

180

SQL Server fait un simple comptage des chemins en cascade et, plutôt que d'essayer de déterminer si des cycles existent réellement, il suppose le pire et refuse de créer les actions référentielles (CASCADE): vous pouvez et devez toujours créer les contraintes sans les actions référentielles. Si vous ne pouvez pas modifier votre conception (ou si cela compromettrait les choses), vous devriez envisager d'utiliser des déclencheurs en dernier recours.

La résolution des chemins en cascade par FWIW est un problème complexe. D'autres produits SQL ignoreront simplement le problème et vous permettront de créer des cycles, auquel cas ce sera une course pour voir laquelle écrasera la valeur en dernier, probablement à l'ignorance du concepteur (par exemple ACE / Jet fait cela). Je comprends que certains produits SQL tenteront de résoudre des cas simples. Le fait demeure, SQL Server n'essaye même pas, le joue en toute sécurité en interdisant plus d'un chemin et au moins il vous le dit.

Microsoft lui-même conseille l'utilisation de déclencheurs au lieu de contraintes FK.

un jour quand
la source
2
une chose que je ne comprends toujours pas, c'est que si ce "problème" peut être résolu en utilisant un déclencheur, alors comment se fait-il qu'un déclencheur ne "cause pas de cycles ou de multiples chemins en cascade ..."?
armen
5
@armen: parce que votre déclencheur fournira explicitement la logique que le système ne pourrait pas implicitement comprendre par lui-même, par exemple s'il existe plusieurs chemins pour une action référentielle de suppression, votre code de déclencheur définira quelles tables sont supprimées et dans quel ordre.
jour du
6
Et aussi le déclencheur s'exécute une fois la première opération terminée, il n'y a donc pas de course en cours.
Bon
2
@dumbledad: Je veux dire, n'utilisez les déclencheurs que lorsque les contraintes (peut-être sur la combinaison) ne peuvent pas faire le travail. Les contraintes sont déclaratives et leurs implémentations sont sous la responsabilité du système. Les déclencheurs sont du code procédural et vous devez coder (et déboguer) l'implémentation et endurer leurs inconvénients (moins bonnes performances, etc.).
jour du
1
Le problème avec cela est que le déclencheur ne fonctionne que tant que vous supprimez la contrainte de clé étrangère, ce qui signifie que vous n'avez alors aucune vérification d'intégrité référentielle sur les insertions de base de données et que vous avez donc besoin d'encore plus de déclencheurs pour gérer cela. La solution de déclenchement est un terrier de lapin menant à une conception de base de données dégénérée.
Neutrino le
99

Une situation typique avec plusieurs chemins en cascade sera la suivante: Une table maître avec deux détails, disons "Master" et "Detail1" et "Detail2". Les deux détails sont supprimés en cascade. Jusqu'à présent, aucun problème. Mais que se passe-t-il si les deux détails ont une relation un-à-plusieurs avec une autre table (dites "SomeOtherTable"). SomeOtherTable a une colonne Detail1ID ET une colonne Detail2ID.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

En d'autres termes: certains des enregistrements de SomeOtherTable sont liés aux enregistrements Detail1 et certains des enregistrements de SomeOtherTable sont liés aux enregistrements Detail2. Même s'il est garanti que les enregistrements SomeOtherTable n'appartiennent jamais aux deux détails, il est désormais impossible de supprimer en cascade les enregistrements de SomeOhterTable pour les deux détails, car il existe plusieurs chemins en cascade de Master à SomeOtherTable (un via Detail1 et un via Detail2). Vous l'avez peut-être déjà compris. Voici une solution possible:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Tous les champs d'ID sont des champs de clé et incrémentés automatiquement. Le point crucial réside dans les champs DetailMainId des tables Detail. Ces champs sont à la fois des contraint clés et référentiels. Il est désormais possible de tout supprimer en cascade en supprimant uniquement les fiches. L'inconvénient est que pour chaque enregistrement detail1 ET pour chaque enregistrement detail2, il doit également y avoir un enregistrement DetailMain (qui est en fait créé en premier pour obtenir l'identifiant correct et unique).

hans riesebos
la source
1
Votre commentaire m'a beaucoup aidé à comprendre le problème auquel je suis confronté. Je vous remercie! Je préférerais désactiver la suppression en cascade pour l'un des chemins, puis gérer la suppression d'autres enregistrements par d'autres moyens (procédures stockées; déclencheurs; par code, etc.). Mais je garde votre solution (regroupement en un seul chemin) à l'esprit pour d'éventuelles applications différentes du même problème ...
freewill
1
Un pour utiliser le mot crux (et aussi pour expliquer)
masterwok
Est-ce mieux que d'écrire des déclencheurs? Il semble étrange d'ajouter une table supplémentaire juste pour faire fonctionner la cascade.
dumbledad le
Tout est mieux que d'écrire des déclencheurs. Leur logique est opaque et ils sont inefficaces par rapport à toute autre chose. Décomposer de grandes tables en plus petites pour un contrôle plus fin est juste une conséquence naturelle d'une meilleure base de données normalisée et pas en soi quelque chose qui doit être concerné.
Neutrino le
12

Je voudrais souligner que (fonctionnellement) il y a une GRANDE différence entre les cycles et / ou plusieurs chemins dans le SCHEMA et les DATA. Alors que les cycles et peut-être les trajets multiples dans les DONNÉES pourraient certainement compliquer le traitement et causer des problèmes de performances (coût d'une manipulation «correcte»), le coût de ces caractéristiques dans le schéma devrait être proche de zéro.

Étant donné que la plupart des cycles apparents dans les RDB se produisent dans des structures hiérarchiques (organigramme, partie, sous-partie, etc.), il est malheureux que SQL Server suppose le pire; c'est-à-dire, cycle de schéma == cycle de données. En fait, si vous utilisez des contraintes RI, vous ne pouvez pas réellement créer un cycle dans les données!

Je soupçonne que le problème des trajets multiples est similaire; c'est-à-dire que plusieurs chemins dans le schéma n'impliquent pas nécessairement plusieurs chemins dans les données, mais j'ai moins d'expérience avec le problème des trajets multiples.

Bien sûr, si SQL Server l'a fait permet cycles , il serait encore faire l' objet d'une profondeur de 32, mais qui est probablement suffisant pour la plupart des cas. (Dommage que ce ne soit pas un paramètre de base de données cependant!)

Les déclencheurs "au lieu de supprimer" ne fonctionnent pas non plus. La deuxième fois qu'une table est visitée, le déclencheur est ignoré. Donc, si vous voulez vraiment simuler une cascade, vous devrez utiliser des procédures stockées en présence de cycles. Cependant, le déclencheur au lieu de supprimer fonctionnerait pour les cas de trajets multiples.

Celko suggère une «meilleure» façon de représenter les hiérarchies qui n'introduit pas de cycles, mais il y a des compromis.

Bill Cohagan
la source
"si vous utilisez des contraintes RI, vous ne pouvez pas réellement créer un cycle dans les données!" -- bon point!
jour du
Bien sûr, vous pouvez créer une circularité des données, mais avec MSSQL uniquement en utilisant UPDATE. D'autres RDBM prennent en charge les contraintes différées (intégrité assurée au moment de la validation, pas au moment de l'insertion / mise à jour / suppression).
Carl Krig
3

Par ses sons, vous avez une action OnDelete / OnUpdate sur l'une de vos clés étrangères existantes, qui modifiera votre table de codes.

Donc, en créant cette clé étrangère, vous créeriez un problème cyclique,

Par exemple, la mise à jour des employés entraîne la modification des codes par une action de mise à jour, la modification des employés par une action de mise à jour ... etc ...

Si vous publiez vos définitions de table pour les deux tables, et vos définitions de clé étrangère / contrainte, nous devrions être en mesure de vous dire où se situe le problème ...

Eoin Campbell
la source
1
Ils sont assez longs, donc je ne pense pas pouvoir les poster ici, mais j'apprécierais beaucoup votre aide - je ne sais pas s'il y a un moyen de vous les envoyer? Je vais essayer de le décrire: Les seules contraintes qui existent proviennent de 3 tables qui ont toutes des champs qui référencent des codes par une simple clé INT Id. Le problème semble être que l'employé a plusieurs champs qui font référence à la table de codes et que je veux qu'ils tous en cascade à SET NULL. Tout ce dont j'ai besoin, c'est que lorsque les codes sont supprimés, les références à ceux-ci doivent être définies sur null partout.
postez-les quand même ... Je pense que personne ici ne s'en souciera, et la fenêtre de code les formatera correctement dans un bloc de défilement :)
Eoin Campbell
2

En effet, l'Employyee pourrait avoir la collection d'une autre entité, dire que les qualifications et les qualifications pourraient avoir une autre collection Universités, par exemple

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

}

Sur DataContext, cela pourrait être comme ci-dessous

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

}

dans ce cas, il y a une chaîne de l'employé à la qualification et de la qualification aux universités. Donc ça me faisait la même exception.

Ça a marché pour moi quand j'ai changé

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

À

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

Trigger est la solution à ce problème:

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
Ton Škoda
la source
0

Il s'agit d'une erreur de type stratégies de déclenchement de base de données. Un déclencheur est du code et peut ajouter des intelligences ou des conditions à une relation en cascade comme la suppression en cascade. Vous devrez peut-être spécialiser les options de tables associées autour de cela, comme Désactiver CascadeOnDelete :

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

Ou désactivez complètement cette fonctionnalité:

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

Ma solution à ce problème rencontré à l'aide d'ASP.NET Core 2.0 et d'EF Core 2.0 consistait à effectuer les opérations suivantes dans l'ordre:

  1. Exécutez la update-databasecommande dans la console de gestion de package (PMC) pour créer la base de données (cela entraîne l'erreur «L'introduction de la contrainte FOREIGN KEY ... peut provoquer des cycles ou plusieurs chemins en cascade.»)

  2. Exécutez la script-migration -Idempotentcommande dans PMC pour créer un script qui peut être exécuté indépendamment des tables / contraintes existantes

  3. Prenez le script résultant et recherchez ON DELETE CASCADEet remplacez parON DELETE NO ACTION

  4. Exécuter le SQL modifié sur la base de données

Désormais, vos migrations doivent être à jour et les suppressions en cascade ne doivent pas se produire.

Dommage que je n'ai pas pu trouver un moyen de le faire dans Entity Framework Core 2.0.

Bonne chance!

user1477388
la source
Vous pouvez modifier votre fichier de migration pour ce faire (sans changer de script sql), c'est-à-dire que dans votre fichier de migration, vous pouvez définir l'action onDelete sur Restreindre de Cascade
Rushi Soni
Il est préférable de le spécifier en utilisant des annotations fluides afin de ne pas avoir à vous souvenir de le faire si vous finissez par supprimer et recréer votre dossier de migrations.
Allen Wang
D'après mon expérience, les annotations fluides peuvent être utilisées et devraient être utilisées (je les utilise) mais elles sont souvent assez boguées. Le simple fait de les spécifier dans le code ne fonctionne pas toujours pour produire le résultat attendu.
user1477388