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.

3voto

public partial class recommended_books : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.RecommendedBook",
            c => new
                {
                    RecommendedBookID = c.Int(nullable: false, identity: true),
                    CourseID = c.Int(nullable: false),
                    DepartmentID = c.Int(nullable: false),
                    Title = c.String(),
                    Author = c.String(),
                    PublicationDate = c.DateTime(nullable: false),
                })
            .PrimaryKey(t => t.RecommendedBookID)
            .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: false) // était vrai lors de la migration
            .ForeignKey("dbo.Department", t => t.DepartmentID, cascadeDelete: false) // était vrai lors de la migration
            .Index(t => t.CourseID)
            .Index(t => t.DepartmentID);

    }

    public override void Down()
    {
        DropForeignKey("dbo.RecommendedBook", "DepartmentID", "dbo.Department");
        DropForeignKey("dbo.RecommendedBook", "CourseID", "dbo.Course");
        DropIndex("dbo.RecommendedBook", new[] { "DepartmentID" });
        DropIndex("dbo.RecommendedBook", new[] { "CourseID" });
        DropTable("dbo.RecommendedBook");
    }
}

Quand votre migration échoue, vous avez quelques options : 'L'introduction d'une contrainte FOREIGN KEY 'FK_dbo.RecommendedBook_dbo.Department_DepartmentID' sur la table 'RecommendedBook' peut entraîner des cycles ou plusieurs chemins de cascade. Spécifiez ON DELETE NO ACTION or ON UPDATE NO ACTION, ou modifiez d'autres contraintes FOREIGN KEY. Impossible de créer une contrainte ou un index. Voir les erreurs précédentes.'

Voici un exemple d'utilisation de la 'modification d'autres contraintes FOREIGN KEY' en définissant 'cascadeDelete' à false dans le fichier de migration puis exécute 'update-database'.

2voto

Umair Javed Points 21

Rendez vos attributs de clé étrangère nullable. Cela fonctionnera.

2voto

Ogglas Points 1

Dans .NET 5 < et .NET Core 2.0 < vous pouvez utiliser .OnDelete(DeleteBehavior.Restrict) dans OnModelCreating comme @Nexus23 répond mais vous n'avez pas besoin de désactiver la cascade pour chaque modèle.

Exemple avec la configuration des entités de jointure many-to-many :

internal class MyContext : DbContext
{
    public MyContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet Posts { get; set; }
    public DbSet Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity()
            .HasMany(p => p.Tags)
            .WithMany(p => p.Posts)
            .UsingEntity(
                j => j
                    .HasOne(pt => pt.Tag)
                    .WithMany(t => t.PostTags)
                    .HasForeignKey(pt => pt.TagId)
                    .OnDelete(DeleteBehavior.Restrict),
                j => j
                    .HasOne(pt => pt.Post)
                    .WithMany(p => p.PostTags)
                    .HasForeignKey(pt => pt.PostId)
                    .OnDelete(DeleteBehavior.Restrict),
                j =>
                {
                    j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
                    j.HasKey(t => new { t.PostId, t.TagId });
                });
    }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public ICollection Tags { get; set; }
    public List PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public ICollection Posts { get; set; }
    public List PostTags { get; set; }
}

public class PostTag
{
    public DateTime PublicationDate { get; set; }

    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}

Sources :

https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#join-entity-type-configuration

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.deletebehavior?view=efcore-5.0

Cela nécessite que vous supprimiez vous-même la relation many-to-many ou vous recevrez l'erreur suivante lorsque vous supprimez une entité parent :

L'association entre les types d'entité '' et '' a été rompue, mais la relation est soit marquée comme requise, soit est implicitement requise car la clé étrangère n'est pas nullable. Si le entité dépendante/enfant doit être supprimée lorsque qu'une relation requise est rompue, configurez la relation pour utiliser les suppressions en cascade. Pensez à utiliser 'DbContextOptionsBuilder.EnableSensitiveDataLogging' pour voir les valeurs des clés

Vous pouvez résoudre cela en utilisant DeleteBehavior.ClientCascade à la place, ce qui permettra à EF d'exécuter les suppressions en cascade sur les entités chargées.

internal class MyContext : DbContext
{
    public MyContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet Posts { get; set; }
    public DbSet Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity()
            .HasMany(p => p.Tags)
            .WithMany(p => p.Posts)
            .UsingEntity(
                j => j
                    .HasOne(pt => pt.Tag)
                    .WithMany(t => t.PostTags)
                    .HasForeignKey(pt => pt.TagId)
                    .OnDelete(DeleteBehavior.Cascade),
                j => j
                    .HasOne(pt => pt.Post)
                    .WithMany(p => p.PostTags)
                    .HasForeignKey(pt => pt.PostId)
                    .OnDelete(DeleteBehavior.ClientCascade),
                j =>
                {
                    j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
                    j.HasKey(t => new { t.PostId, t.TagId });
                });
    }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public ICollection Tags { get; set; }
    public List PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public ICollection Posts { get; set; }
    public List PostTags { get; set; }
}

public class PostTag
{
    public DateTime PublicationDate { get; set; }

    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.deletebehavior?view=efcore-5.0

1voto

Marco Alves Points 160

Cela semble étrange et je ne sais pas pourquoi, mais dans mon cas, cela se produisait car ma chaîne de connexion utilisait "." dans l'attribut "source de données". Une fois que je l'ai changé en "localhost", tout a fonctionné comme sur des roulettes. Aucun autre changement n'était nécessaire.

1voto

yourmother Points 157

Les réponses existantes sont formidables, je voulais juste ajouter que j'ai rencontré cette erreur en raison d'une raison différente. Je voulais créer une migration EF initiale sur une base de données existante mais je n'ai pas utilisé le drapeau -IgnoreChanges et j'ai appliqué la commande Update-Database sur une base de données vide (également sur les échecs existants).

Au lieu de cela, j'ai dû exécuter cette commande lorsque la structure de la base de données actuelle est la bonne :

Add-Migration Initial -IgnoreChanges

Il y a probablement un problème réel dans la structure de la base de données mais sauvons le monde un pas à la fois...

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