web-dev-qa-db-fra.com

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

J'ai un problème lorsque j'essaie d'ajouter des contraintes à mes tables. Je reçois l'erreur:

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

Ma contrainte est entre une table Code et une table employee. La table Code contient Id, Name, FriendlyName, Type et un Value. Le employee comporte un certain nombre de champs qui référencent des codes, de sorte qu’il peut exister 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 ceci?

162

SQL Server effectue un comptage simple des chemins en cascade et, au lieu d'essayer de déterminer s'il existe réellement des cycles, il assume 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 cela compromettrait certaines choses), vous devriez envisager d'utiliser des déclencheurs en dernier recours.

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

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

168
onedaywhen

Une situation typique avec plusieurs chemins de casse sera la suivante: Une table maître avec deux détails, disons "Maître", "Détail1" et "Détail2". Les deux détails sont supprimés en cascade. Jusqu'ici pas de problèmes. 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 à des enregistrements Detail1 et certains des enregistrements de SomeOtherTable sont liés à des enregistrements Detail2. Même s'il est garanti que SomeOtherTable-records n'appartient jamais aux deux détails, il est maintenant impossible de supprimer les enregistrements de SomeOhterTable en cascade pour les deux détails, car il existe plusieurs chemins en cascade de maître à SomeOtherTable (un via Detail1 et un via Detail2). Maintenant, vous avez peut-être déjà compris cela. 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és et à incrémentation automatique. Le noeud réside dans les champs DetailMainId des tables Detail. Ces champs sont à la fois une clé et une référence référentielle. Il est maintenant 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 exister un enregistrement DetailMain (qui est en fait créé en premier pour obtenir l'identifiant unique correct).

86
hans riesebos

Je tiens à souligner qu'il existe (sur le plan fonctionnel) une énorme différence entre les cycles et/ou les chemins multiples dans le SCHEMA et le DATA. Bien que les cycles et peut-être les trajets multiples dans les DONNÉES puissent certainement compliquer le traitement et causer des problèmes de performances (coût de la manipulation "correctement"), le coût de ces caractéristiques dans le schéma devrait être proche de zéro.

Comme la plupart des cycles apparents dans les bases de données relationnelles se produisent dans des structures hiérarchiques (organigramme, partie, sous-partie, etc.), il est malheureux que SQL Server assume 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 créer de cycle dans les données!

Je soupçonne que le problème de 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 de trajets multiples.

Bien sûr, si SQL Server le faisait autorisait les cycles, il serait toujours soumis à une profondeur de 32, mais cela convient probablement dans 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. Le déclencheur au lieu de supprimer fonctionnerait toutefois pour les cas de trajets multiples.

Celko suggère un "meilleur" moyen de représenter les hiérarchies sans introduire de cycles, mais il existe des compromis.

12
Bill Cohagan

Un article est disponible dans lequel il explique comment effectuer plusieurs chemins de suppression à l'aide de déclencheurs. Ceci est peut-être utile pour des scénarios complexes.

http://www.mssqltips.com/sqlservertip/2733/solving-the-sql-server-multiple-cascade-path-issue-with-a-trigger/

7
Javier

Par le son, vous avez une action OnDelete/OnUpdate sur l’une de vos clés étrangères existantes, qui modifiera votre tableau de codes.

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

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

Si vous postez vos définitions de table pour les deux tables et vos définitions de clé étrangère/contrainte, nous devrions pouvoir vous dire où se trouve le problème ...

3
Eoin Campbell

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
1
Tone Škoda

Ceci est dû au fait que Emplyee pourrait avoir une collection d’une autre entité, par exemple Qualifications et que Qualification pourrait avoir une autre collection, par exemple Universités.

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 existe une chaîne allant d'employé à qualification et de qualification à université. Donc, il me lançait la même exception.

Cela a fonctionné 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);
1
RAJ

Il s'agit d'une erreur de type stratégies de déclencheur de base de données. n déclencheur est un code et peut ajouter des intelligences ou des conditions à une relation en cascade telle que la suppression en cascade. Vous devrez peut-être spécialiser les options des 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>();
0