A introdução da restrição FOREIGN KEY pode causar ciclos ou vários caminhos em cascata - por quê?

295

Estou lutando com isso há um tempo e não consigo entender o que está acontecendo. Eu tenho uma entidade de cartão que contém lados (geralmente 2) - e ambos os cartões e lados têm um estágio. Estou usando as migrações EF Codefirst e as migrações estão falhando com este erro:

A introdução da restrição FOREIGN KEY 'FK_dbo.Sides_dbo.Cards_CardId' na tabela 'Lados' pode causar ciclos ou vários caminhos em cascata. Especifique ON DELETE NO ACTION ou ON UPDATE NO ACTION ou modifique outras restrições de FOREIGN KEY.

Aqui está a entidade do meu cartão :

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

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

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

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

Aqui está minha entidade 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; }

}

E aqui está minha entidade 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<Stage> 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; } }
}

O que é estranho é que se eu adicionar o seguinte à minha classe Stage:

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

A migração é executada com sucesso. Se eu abrir o SSMS e olhar para as tabelas, posso ver que Stage_StageIdfoi adicionado a Cards(como esperado / desejado), mas Sidesnão contém nenhuma referência a Stage(não esperado).

Se eu adicionar

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

Na minha classe Side, vejo a StageIdcoluna adicionada à minha Sidetabela.

Isso está funcionando, mas agora, em toda a minha aplicação, qualquer referência a Stagecontém a SideId, que em alguns casos é totalmente irrelevante. Eu gostaria de apenas dar às minhas entidades Carde Sideuma Stagepropriedade com base na classe Stage acima sem poluir a classe stage com propriedades de referência, se possível ... o que estou fazendo de errado?

SB2055
fonte
7
Desative a exclusão em cascata, permitindo valores nulos nas referências ... para que, em SideClass, inclua número inteiro nulo e remova o [Required]atributo =>public int? CardId { get; set; }
Jaider
2
No EF Core, você deve desativar a exclusão em cascata com DeleteBehavior.Restrictou DeleteBehavior.SetNull.
Sina Lotfi

Respostas:

371

Como Stageé necessário , todos os relacionamentos um para muitos Stageenvolvidos estão com a exclusão em cascata ativada por padrão. Isso significa que, se você excluir umStage entidade

  • a exclusão entrará em cascata diretamente para Side
  • a exclusão entrará em cascata diretamente para Carde porque, Carde Sideterá um relacionamento um-para-muitos necessário com a exclusão em cascata ativada por padrão novamente, ela será cascateada de CardparaSide

Portanto, você tem dois caminhos de exclusão em cascata de StageparaSide - o que causa a exceção.

Você deve tornar o Stageopcional em pelo menos uma das entidades (por exemplo, remover o [Required]atributo das Stagepropriedades) ou desativar a exclusão em cascata com a API Fluent (não é possível com anotações de dados):

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

modelBuilder.Entity<Side>()
    .HasRequired(s => s.Stage)
    .WithMany()
    .WillCascadeOnDelete(false);
Slauma
fonte
2
Obrigado Slauma. Se eu usar a API fluente, como você demonstrou acima, outros campos manterão o comportamento de exclusão em cascata? Ainda preciso que os Lados sejam excluídos quando os cartões são excluídos, por exemplo.
SB2055
1
@ SB2055: Sim, isso afetará apenas os relacionamentos de Stage. Outros relacionamentos permanecem inalterados.
Slauma
2
Existe alguma maneira de saber quais propriedades estão causando o erro? Estou com o mesmo problema e, olhando as minhas aulas, não consigo ver onde está o ciclo
Rodrigo Juarez
4
Isso é uma limitação na sua implementação? Parece-me muito bem para uma Stagedeleção em cascata para baixo para Sidetanto directamente como através de umCard
aaaaaa
1
Suponha que definimos CascadeOnDelete como false. Em seguida, removemos um registro de estágio relacionado a um dos registros do cartão. O que acontece com o Card.Stage (FK)? Permanece o mesmo? ou está definido como Nulo?
Ninbit
61

Eu tinha uma tabela que tinha um relacionamento circular com os outros e estava recebendo o mesmo erro. Acontece que é sobre a chave estrangeira que não foi anulável. Se a chave não for anulável, o objeto relacionado deve ser excluído e as relações circulares não permitem isso. Portanto, use chave estrangeira anulável.

[ForeignKey("StageId")]
public virtual Stage Stage { get; set; }
public int? StageId { get; set; }
Cem Mutlu
fonte
5
Eu removi a tag [Obrigatório], mas outra coisa importante era usar em int?vez de intdeixá-la anulável.
VSB 28/03
1
Tentei várias maneiras diferentes de desativar a exclusão em cascata e nada funcionou - isso foi corrigido!
ambog36
5
Você não deve fazer isso se não quiser permitir que o Stage seja definido como nulo (o Stage era um campo obrigatório na pergunta original).
cfwall
35

Alguém quer saber como fazer isso no núcleo da EF:

      protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
                {
                    relationship.DeleteBehavior = DeleteBehavior.Restrict;
                }
           ..... rest of the code.....
Nexus23
fonte
3
Isso desativaria a exclusão em cascata em todos os relacionamentos. A exclusão em cascata pode ser um recurso desejado para alguns casos de uso.
Chama
15
Como alternativa,builder.HasOne(x => x.Stage).WithMany().HasForeignKey(x => x.StageId).OnDelete(DeleteBehavior.Restrict);
Biscoitos
@Biscoitos Os métodos de extensão mudaram ao longo do tempo ou você esqueceu o builder _ .Entity<TEntity>() _anterior HasOne() pode ser chamado ...
ViRuSTriNiTy
1
@ViRuSTriNiTy, meu snippet tem 2 anos. Mas acho que você está certo - hoje em dia seria quando você optar por implementar IEntityTypeConfiguration<T>. Não me lembro de ter visto o builder.Entity<T>método naqueles dias, mas posso estar errado. No entanto, os dois vão trabalhar :)
Biscoitos
21

Eu estava recebendo esse erro para muitas entidades quando estava migrando de um modelo EF7 para uma versão EF6. Como não queria passar por cada entidade, uma de cada vez, usei:

builder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
builder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
Sean
fonte
2
Isso deve ser adicionado nas classes herdadas do DbContext, por exemplo, no método OnModelCreating. O construtor é do tipo DbModelBuilder
CodingYourLife
Isso funcionou para mim; .NET 4.7, EF 6. Um obstáculo foi o erro; portanto, quando eu regenerei pelo script de migração com essas convenções removidas, ele não pareceu ajudar. A execução do "Add-Migration" com "-Force" limpou tudo e o reconstruiu, incluindo as convenções acima. Problema resolvido ...
James Joyce
Aqueles não existem no núcleo .net, há algum equivalente lá?
Jjxtra 29/07/19
20

Você pode definir cascadeDelete como false ou true (no método Up () de migração). Depende de sua exigência.

AddForeignKey("dbo.Stories", "StatusId", "dbo.Status", "StatusID", cascadeDelete: false);
Musakkhir Sayyed
fonte
2
@Mussakkhir obrigado por sua resposta. Seu caminho é muito elegante e mais acabado - é mais preciso e direcionado diretamente para o problema que enfrentei!
Nozim Turakulov
Só não esqueça que o UPmétodo pode ser modificado por operações externas.
Dementic
8

No .NET Core, alterei a opção onDelete para ReferencialAction.NoAction

         constraints: table =>
            {
                table.PrimaryKey("PK_Schedule", x => x.Id);
                table.ForeignKey(
                    name: "FK_Schedule_Teams_HomeId",
                    column: x => x.HomeId,
                    principalTable: "Teams",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.NoAction);
                table.ForeignKey(
                    name: "FK_Schedule_Teams_VisitorId",
                    column: x => x.VisitorId,
                    principalTable: "Teams",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.NoAction);
            });
Mike Jones
fonte
7

Eu também tive esse problema, resolvi-o instantaneamente com esta resposta de um tópico semelhante

No meu caso, não quis excluir o registro dependente na exclusão da chave. Se este for o seu caso, basta alterar o valor booleano na migração para false:

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

As chances são de que, se você estiver criando relacionamentos que geram esse erro do compilador, mas desejam manter a exclusão em cascata; você tem um problema com seus relacionamentos.

jonc.js
fonte
6

Eu consertei isso. Quando você adiciona a migração, no método Up () haverá uma linha como esta:

.ForeignKey("dbo.Members", t => t.MemberId, cascadeDelete:True)

Se você apenas excluir o cascadeDelete do final, ele funcionará.

Usman Khan
fonte
5

Apenas para fins de documentação, para alguém que vem no futuro, isso pode ser resolvido de maneira simples e, com esse método, você pode executar um método que desativou uma vez e acessar seu método normalmente.

Adicione este método à classe de banco de dados de contexto:

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
    modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}
sgrysoft
fonte
1

Isso parece estranho e não sei por que, mas no meu caso isso estava acontecendo porque meu ConnectionString estava usando "." no atributo "fonte de dados". Depois que eu mudei para "localhost", funcionou como um encanto. Nenhuma outra mudança foi necessária.

Marco Alves
fonte
1

No .NET Core, eu brinquei com todas as respostas superiores - mas sem sucesso. Fiz muitas alterações na estrutura do banco de dados e sempre adicionava nova migração tentandoupdate-database , mas recebia o mesmo erro.

Então comecei remove-migrationum por um até o Console do Gerenciador de Pacotes me lançar uma exceção:

A migração '20170827183131 _ ***' já foi aplicada ao banco de dados

Depois disso, adicionei nova migração ( add-migration) e update-database obtive sucesso

Portanto, minha sugestão seria: limpar todas as suas migrações temporárias, até o estado atual do banco de dados.

rock_walker
fonte
1

As respostas existentes são ótimas. Só queria acrescentar que encontrei esse erro por um motivo diferente. Eu queria criar uma migração EF inicial em um banco de dados existente, mas não usei o -IgnoreChanges sinalizador e apliquei o comando Update-Database em um banco de dados vazio (também nas falhas existentes).

Em vez disso, tive que executar este comando quando a estrutura db atual é a atual:

Add-Migration Initial -IgnoreChanges

Provavelmente, existe um problema real na estrutura do banco de dados, mas salve o mundo um passo de cada vez ...

CodingYourLife
fonte
1

A maneira mais simples é, Editar seu arquivo de migração (cascadeDelete: true)em (cascadeDelete: false)seguida, depois de atribuir o comando Update-banco de dados no seu Gerenciador de Pacotes Console.if-lo do problema com a sua última migração então tudo bem. Caso contrário, verifique seu histórico de migração anterior, copie essas coisas, cole no seu último arquivo de migração e depois faça o mesmo. funciona perfeitamente para mim.

Niroshan Kumarasamy
fonte
1
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) // was true on migration
            .ForeignKey("dbo.Department", t => t.DepartmentID, cascadeDelete: false) // was true on 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");
    }
}

Quando sua migração falha, você recebe algumas opções: 'Introdução à restrição FOREIGN KEY' FK_dbo.RecommendedBook_dbo.Department_DepartmentID 'na tabela' RecommendedBook 'pode causar ciclos ou vários caminhos em cascata. Especifique ON DELETE NO ACTION ou ON UPDATE NO ACTION ou modifique outras restrições de FOREIGN KEY. Não foi possível criar restrição ou índice. Veja erros anteriores.

Aqui está um exemplo do uso de 'modificar outras restrições FOREIGN KEY', configurando 'cascadeDelete' como false no arquivo de migração e, em seguida, execute 'update-database'.

Christopher Govender
fonte
0

Nenhuma das soluções mencionadas funcionou para mim. O que eu tive que fazer foi usar um int nulo (int?) Na chave estrangeira que não era necessária (ou não uma chave de coluna não nula) e, em seguida, excluir algumas das minhas migrações.

Comece excluindo as migrações e tente o int nulo.

O problema era tanto uma modificação quanto um design de modelo. Nenhuma alteração de código foi necessária.

Ayson Baxter
fonte
-1

Torne seus atributos de chave estrangeira anuláveis. Isso vai funcionar.

Umair Javed
fonte
1
que a resposta nos comentários sob perguntas, por favor, elabore lá
Kostia Mololkin