414 votes

Présenter une contrainte de clé étrangère peut provoquer des cycles ou plusieurs chemins de cascade - pourquoi?

Je me suis débattu avec cela depuis un certain temps et je n'arrive pas à comprendre ce qui se passe. J'ai une entité Card qui contient des Sides (généralement 2) - et à la fois les Cards et les Sides ont un Stage. J'utilise des migrations EF Codefirst et les migrations échouent avec cette erreur :

L'introduction de la contrainte de clé étrangère 'FK_dbo.Sides_dbo.Cards_CardId' sur la table 'Sides' peut entraîner des cycles ou des chemins de cascade multiples. Spécifiez ON DELETE NO ACTION ou ON UPDATE NO ACTION, ou modifiez d'autres contraintes de clé étrangère.

Voici mon entité Card :

public class Card
{
    public Card()
    {
        Sides = new Collection();
        Stage = Stage.ONE;
    }

    [Key]
    [Required]
    public virtual int CardId { get; set; }

    [Required]
    public virtual Stage Stage { get; set; }

    [Required]
    [ForeignKey("CardId")]
    public virtual ICollection Sides { get; set; }
}

Voici mon entité Side :

public class Side
{
    public Side()
    {
        Stage = Stage.ONE;
    }

    [Key]
    [Required]     
    public virtual int SideId { get; set; } 

    [Required]
    public virtual Stage Stage { get; set; }

    [Required]
    public int CardId { get; set; }

    [ForeignKey("CardId")]
    public virtual Card Card { get; set; }

}

Et voici mon entité Stage :

public class Stage
{
    // Zero
    public static readonly Stage ONE = new Stage(new TimeSpan(0, 0, 0), "ONE");
    // Ten seconds
    public static readonly Stage TWO = new Stage(new TimeSpan(0, 0, 10), "TWO");

    public static IEnumerable Values
    {
        get
        {
            yield return ONE;
            yield return TWO;
        }

    }

    public int StageId { get; set; }
    private readonly TimeSpan span;
    public string Title { get; set; }

    Stage(TimeSpan span, string title)
    {
        this.span = span;
        this.Title = title;
    }

    public TimeSpan Span { get { return span; } }
}

Le problème est que si j'ajoute ce qui suit à ma classe Stage :

    public int? SideId { get; set; }
    [ForeignKey("SideId")]
    public virtual Side Side { get; set; }

La migration s'exécute avec succès. Si j'ouvre SSMS et regarde les tables, je peux voir que Stage_StageId a été ajouté à Cards (comme prévu/souhaité), cependant Sides ne contient aucune référence à Stage (pas attendue).

Si j'ajoute ensuite :

    [Required]
    [ForeignKey("StageId")]
    public virtual Stage Stage { get; set; }
    public int StageId { get; set; }

À ma classe Side, je vois que la colonne StageId est ajoutée à ma table Side.

Cela fonctionne, mais maintenant dans toute mon application, toute référence à Stage contient un SideId, qui est dans certains cas totalement inapproprié. J'aimerais simplement donner à mes entités Card et Side une propriété Stage basée sur la classe Stage citée ci-dessus sans polluer la classe Stage avec des propriétés de référence si possible... que suis-je en train de faire de mal ?

8 votes

Désactiver la suppression en cascade en autorisant les valeurs nulles dans les références... donc dans la classe Side ajouter un entier Nullable et supprimer l'attribut [Required] => public int? CardId { get; set; }

4 votes

Dans EF Core, vous devez désactiver la suppression en cascade avec DeleteBehavior.Restrict ou DeleteBehavior.SetNull.

7 votes

La réponse acceptée est la seule réponse correcte. La question est : comment empêcher un chemin de cascade circulaire si je veux une relation obligatoire. Une seule instruction de mappage suffit. Donc, ne suggérez pas de rendre la relation optionnelle, ou pire, de modifier le fichier de migration généré (introduisant une divergence entre le modèle de base de données et le modèle conceptuel), ou pire encore, de désactiver toutes les suppressions en cascade.

483voto

Slauma Points 76561

Parce que Stage est required, toutes les relations un-à-plusieurs où Stage est impliqué auront la suppression en cascade activée par défaut. Cela signifie que si vous supprimez une entité Stage

  • la suppression se propagera directement à Side
  • la suppression se propagera directement à Card et parce que Card et Side ont une relation un-à-plusieurs requise avec la suppression en cascade activée par défaut, cela se propagera ensuite de Card à Side

Ainsi, vous avez deux chemins de suppression en cascade de Stage vers Side - ce qui provoque l'exception.

Vous devez rendre Stage facultatif dans au moins l'une des entités (c'est-à-dire supprimer l'attribut [Required] des propriétés Stage) ou désactiver la suppression en cascade avec Fluent API (pas possible avec les annotations de données):

modelBuilder.Entity()
    .HasRequired(c => c.Stage)
    .WithMany()
    .WillCascadeOnDelete(false);

modelBuilder.Entity()
    .HasRequired(s => s.Stage)
    .WithMany()
    .WillCascadeOnDelete(false);

3 votes

Merci Slauma. Si j'utilise l'API fluide comme vous l'avez démontré ci-dessus, est-ce que les autres champs conserveront leur comportement de suppression en cascade ? J'ai toujours besoin que les côtés soient supprimés lorsque les cartes sont supprimées, par exemple.

1 votes

@SB2055 : Oui, cela n'affectera que les relations de Stage. Les autres relations restent inchangées.

3 votes

Y a-t-il un moyen de savoir quelles propriétés sont à l'origine de l'erreur? J'ai le même problème, et en regardant mes classes, je ne vois pas où se trouve le cycle.

101voto

Cem Mutlu Points 839

J'avais une table qui avait une relation circulaire avec d'autres et j'obtenais la même erreur. Il s'avère que c'est lié à la clé étrangère qui n'était pas nullable. Si la clé n'est pas nullable, l'objet lié doit être supprimé, et les relations circulaires ne le permettent pas. Utilisez donc une clé étrangère nullable.

[ForeignKey("StageId")]
public virtual Stage Stage { get; set; }
public int? StageId { get; set; }

52voto

Nexus23 Points 1205

Toute personne se demandant comment le faire dans EF Core :

      protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
                {
                    relationship.DeleteBehavior = DeleteBehavior.Restrict;
                }
           ..... reste du code.....

27voto

Sean Points 1468

J'obtenais cette erreur pour de nombreuses entités lorsque je migrais d'un modèle EF7 vers une version EF6. Je ne voulais pas devoir passer par chaque entité une par une, donc j'ai utilisé :

builder.Conventions.Remove();
builder.Conventions.Remove();

25voto

Musakkhir Sayyed Points 1679

Vous pouvez définir cascadeDelete sur false ou true (dans votre méthode de migration Up()). Cela dépend de vos besoins.

AddForeignKey("dbo.Stories", "StatusId", "dbo.Status", "StatusID", cascadeDelete: false);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X