O relacionamento não pôde ser alterado porque uma ou mais das propriedades de chave estrangeira são não anuláveis

192

Estou recebendo esse erro quando eu GetById () em uma entidade e, em seguida, defino a coleção de entidades filho para minha nova lista que vem da exibição do MVC.

A operação falhou: O relacionamento não pôde ser alterado porque uma ou mais das propriedades de chave estrangeira são não anuláveis. Quando uma alteração é feita em um relacionamento, a propriedade de chave estrangeira relacionada é definida como um valor nulo. Se a chave estrangeira não suportar valores nulos, um novo relacionamento deverá ser definido, a propriedade da chave estrangeira deverá receber outro valor não nulo ou o objeto não relacionado deverá ser excluído.

Não entendo bem essa linha:

O relacionamento não pôde ser alterado porque uma ou mais das propriedades da chave estrangeira são não anuláveis.

Por que eu mudaria o relacionamento entre duas entidades? Ele deve permanecer o mesmo durante toda a vida útil de todo o aplicativo.

O código em que a exceção ocorre é a simples atribuição de classes filho modificadas em uma coleção à classe pai existente. Esperamos que isso atenda à remoção de classes filho, adição de novas e modificações. Eu teria pensado que o Entity Framework lida com isso.

As linhas de código podem ser destiladas para:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
jaffa
fonte
Encontrei minha resposta de compra usando a solução nº 2 no artigo abaixo. Basicamente, criei uma chave primária na tabela filho para referência à tabela pai (portanto, ela tem 2 chaves primárias (a chave estrangeira na tabela pai e o ID). para a tabela filho) c-sharpcorner.com/UploadFile/ff2f08/…
yougotiger 1/19/19
@jaffa, eu encontrei minha resposta aqui stackoverflow.com/questions/22858491/…
antonio

Respostas:

159

Você deve excluir itens filhos antigos thisParent.ChildItemsum por um manualmente. O Entity Framework não faz isso por você. Finalmente, não é possível decidir o que você deseja fazer com os itens filhos antigos - se você deseja descartá-los ou se deseja mantê-los e atribuí-los a outras entidades-pai. Você deve informar ao Entity Framework sua decisão. Mas você deve tomar uma dessas duas decisões, pois as entidades filhas não podem viver sozinhas sem uma referência a nenhum pai no banco de dados (devido à restrição de chave estrangeira). Isso é basicamente o que a exceção diz.

Editar

O que eu faria se itens filho pudessem ser adicionados, atualizados e excluídos:

public void UpdateEntity(ParentItem parent)
{
    // Load original parent including the child item collection
    var originalParent = _dbContext.ParentItems
        .Where(p => p.ID == parent.ID)
        .Include(p => p.ChildItems)
        .SingleOrDefault();
    // We assume that the parent is still in the DB and don't check for null

    // Update scalar properties of parent,
    // can be omitted if we don't expect changes of the scalar properties
    var parentEntry = _dbContext.Entry(originalParent);
    parentEntry.CurrentValues.SetValues(parent);

    foreach (var childItem in parent.ChildItems)
    {
        var originalChildItem = originalParent.ChildItems
            .Where(c => c.ID == childItem.ID && c.ID != 0)
            .SingleOrDefault();
        // Is original child item with same ID in DB?
        if (originalChildItem != null)
        {
            // Yes -> Update scalar properties of child item
            var childEntry = _dbContext.Entry(originalChildItem);
            childEntry.CurrentValues.SetValues(childItem);
        }
        else
        {
            // No -> It's a new child item -> Insert
            childItem.ID = 0;
            originalParent.ChildItems.Add(childItem);
        }
    }

    // Don't consider the child items we have just added above.
    // (We need to make a copy of the list by using .ToList() because
    // _dbContext.ChildItems.Remove in this loop does not only delete
    // from the context but also from the child collection. Without making
    // the copy we would modify the collection we are just interating
    // through - which is forbidden and would lead to an exception.)
    foreach (var originalChildItem in
                 originalParent.ChildItems.Where(c => c.ID != 0).ToList())
    {
        // Are there child items in the DB which are NOT in the
        // new child item collection anymore?
        if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID))
            // Yes -> It's a deleted child item -> Delete
            _dbContext.ChildItems.Remove(originalChildItem);
    }

    _dbContext.SaveChanges();
}

Nota: Isso não foi testado. Supõe-se que a coleção de itens filho seja do tipo ICollection. (Normalmente, eu tenho IListe, em seguida, o código parece um pouco diferente.) Também removi todas as abstrações do repositório para simplificar.

Não sei se essa é uma boa solução, mas acredito que algum tipo de trabalho duro nesse sentido deve ser feito para cuidar de todos os tipos de alterações na coleção de navegação. Eu também ficaria feliz em ver uma maneira mais fácil de fazer isso.

Slauma
fonte
E daí se alguns são alterados apenas? Isso significa que ainda tenho que removê-los e adicioná-los novamente?
jaffa
@ Jon: Não, você também pode atualizar os itens existentes, é claro. Adicionei um exemplo de como provavelmente atualizaria a coleção filho, consulte a seção Editar acima.
Slauma
@Slauma: Lol, se eu soubesse que você está indo para modificar a sua resposta eu não iria escrever a minha resposta ...
Ladislav Mrnka
@Ladislav: Não, não, estou feliz que você tenha escrito sua própria resposta. Agora, pelo menos, eu sei que não é bobagem completa e muito complicado o que fiz acima.
Slauma
1
Eu adicionaria uma condição ao recuperar o originalChildItem no foreach: ... Where (c => c.ID == childItem.ID && c.ID! = 0) caso contrário, ele retornará os filhos recém-adicionados se childItem.ID == 0.
perfect_element
116

A razão pela qual você está enfrentando isso é devido à diferença entre composição e agregação .

Na composição, o objeto filho é criado quando o pai é criado e é destruído quando seu pai é destruído . Portanto, sua vida útil é controlada pelo pai. por exemplo, uma postagem de blog e seus comentários. Se uma postagem for excluída, seus comentários deverão ser excluídos. Não faz sentido ter comentários para uma postagem que não existe. O mesmo para pedidos e itens de pedidos.

Na agregação, o objeto filho pode existir independentemente de seu pai . Se o pai for destruído, o objeto filho ainda poderá existir, pois poderá ser adicionado a um pai diferente posteriormente. por exemplo: o relacionamento entre uma lista de reprodução e as músicas nessa lista de reprodução. Se a lista de reprodução for excluída, as músicas não deverão ser excluídas. Eles podem ser adicionados a uma lista de reprodução diferente.

A maneira como o Entity Framework diferencia os relacionamentos de agregação e composição é a seguinte:

  • Para composição: espera que o objeto filho tenha uma chave primária composta (ParentID, ChildID). Isso ocorre por design, já que os IDs das crianças devem estar dentro do escopo de seus pais.

  • Para agregação: espera que a propriedade da chave estrangeira no objeto filho seja anulável.

Portanto, o motivo pelo qual você está tendo esse problema é o motivo pelo qual você definiu sua chave primária na tabela filho. Deve ser composto, mas não é. Portanto, o Entity Framework vê essa associação como agregação, o que significa que, quando você remove ou limpa os objetos filho, ele não exclui os registros filhos. Ele simplesmente remove a associação e define a coluna de chave estrangeira correspondente como NULL (para que esses registros filhos possam ser associados posteriormente a um pai diferente). Como sua coluna não permite NULL, você obtém a exceção mencionada.

Soluções:

1- Se você tiver um motivo forte para não querer usar uma chave composta, precisará excluir os objetos filhos explicitamente. E isso pode ser feito de maneira mais simples do que as soluções sugeridas anteriormente:

context.Children.RemoveRange(parent.Children);

2- Caso contrário, ao definir a chave primária adequada na tabela filho, seu código parecerá mais significativo:

parent.Children.Clear();
Mosh
fonte
9
Achei esta explicação muito útil.
Booji Boy
7
Boa explicação para composição versus agregação e como a estrutura da entidade está relacionada a ela.
Crisálida
O número 1 foi a menor quantidade de código necessária para corrigir o problema. Obrigado!
ryanulit
73

Este é um problema muito grande. O que realmente acontece no seu código é o seguinte:

  • Você carrega Parentdo banco de dados e obtém uma entidade anexada
  • Você substitui sua coleção filho por uma nova coleção de filhos desanexados
  • Você salva as alterações, mas durante esta operação todos os filhos são considerados como adicionados porque a EF não os conhecia até esse momento. Portanto, o EF tenta definir chave nula para estrangeira de filhos antigos e inserir todos os filhos novos => linhas duplicadas.

Agora a solução realmente depende do que você quer fazer e como gostaria de fazê-lo?

Se você estiver usando o ASP.NET MVC, poderá tentar usar UpdateModel ou o TryUpdateModel .

Se você deseja apenas atualizar manualmente os filhos existentes, basta fazer algo como:

foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();

Anexar não é realmente necessário (definir o estado para Modifiedtambém anexará a entidade), mas eu gosto disso porque torna o processo mais óbvio.

Se você deseja modificar os existentes, excluir os existentes e inserir novos filhos, deve fazer algo como:

var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well
foreach(var child in modifiedParent.ChildItems)
{
    var attachedChild = FindChild(parent, child.Id);
    if (attachedChild != null)
    {
        // Existing child - apply new values
        context.Entry(attachedChild).CurrentValues.SetValues(child);
    }
    else
    {
        // New child
        // Don't insert original object. It will attach whole detached graph
        parent.ChildItems.Add(child.Clone());
    }
}

// Now you must delete all entities present in parent.ChildItems but missing
// in modifiedParent.ChildItems
// ToList should make copy of the collection because we can't modify collection
// iterated by foreach
foreach(var child in parent.ChildItems.ToList())
{
    var detachedChild = FindChild(modifiedParent, child.Id);
    if (detachedChild == null)
    {
        parent.ChildItems.Remove(child);
        context.Childs.Remove(child); 
    }
}

context.SaveChanges();
Ladislav Mrnka
fonte
1
Mas há uma observação interessante sobre o uso .Clone(). Você tem em mente que a ChildItempossui outras propriedades de navegação secundárias? Mas, nesse caso, não queremos que todo o subgráfico esteja anexado ao contexto, já que esperamos que todos os subfilhos sejam novos objetos se o próprio filho for novo? (Bem, pode ser diferente de modelo para modelo, mas vamos supor que o caso que os sub-criança são "dependentes" da criança como as da criança são dependentes do pai.)
Slauma
Provavelmente exigiria um clone "inteligente".
Ladislav Mrnka
1
E se você não quiser ter uma coleção de crianças no seu contexto? http://stackoverflow.com/questions/20233994/do-i-need-to-create-a-dbset-for-every-table-so-that-i-can-persist-child-entitie
precisa
1
parent.ChildItems.Remove (child); context.Childs.Remove (child); Esta remoção dupla corrigida pode emitir, OBRIGADO. Por que precisamos das duas remoções? Por que remover apenas dos parent.ChildItems não é suficiente, pois os filhos vivem apenas como filhos?
Fernando Torres
40

Achei esta resposta muito mais útil para o mesmo erro. Parece que a EF não gosta quando você remove, prefere Excluir.

Você pode excluir uma coleção de registros anexados a um registro como este.

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);

No exemplo, todos os registros de detalhes anexados a um pedido têm seu estado definido como Excluir. (Em preparação para adicionar novamente detalhes atualizados, como parte de uma atualização de pedido)

Greg Little
fonte
Eu acredito que é a resposta correta.
Desmati
solução lógica e direta.
SaidaFan
19

Não faço ideia por que as outras duas respostas são tão populares!

Acredito que você estava certo ao supor que a estrutura ORM deveria lidar com isso - afinal, é isso que promete entregar. Caso contrário, seu modelo de domínio será corrompido por preocupações de persistência. O NHibernate gerencia isso felizmente se você definir as configurações em cascata corretamente. No Entity Framework, também é possível, eles esperam que você siga melhores padrões ao configurar seu modelo de banco de dados, especialmente quando eles precisam inferir o que a cascata deve ser feita:

Você precisa definir o relacionamento pai-filho corretamente usando um " relacionamento de identificação ".

Se você fizer isso, o Entity Framework saberá que o objeto filho é identificado pelo pai e, portanto, deve ser uma situação "em cascata-excluir-órfão".

Além do acima, você pode precisar (da experiência do NHibernate)

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

em vez de substituir completamente a lista.

ATUALIZAR

O comentário de @ Slauma me lembrou que as entidades desanexadas são outra parte do problema geral. Para resolver isso, é possível usar o fichário de modelo personalizado que constrói seus modelos, tentando carregá-lo a partir do contexto. Esta postagem do blog mostra um exemplo do que quero dizer.

Andre Luus
fonte
A configuração como relacionamento de identificação não ajudará aqui porque o cenário da pergunta precisa lidar com entidades desanexadas ( "minha nova lista que vem da exibição do MVC" ). Você ainda precisa carregar os filhos originais do banco de dados, localizar os itens removidos nessa coleção com base na coleção desanexada e depois remover do banco de dados. A única diferença é que, com um relacionamento de identificação, você pode ligar em parent.ChildItems.Removevez de _dbContext.ChildItems.Remove. Ainda existe (EF <= 6) nenhum suporte interno da EF para evitar códigos longos, como o das outras respostas.
Slauma
Eu entendo o seu ponto. No entanto, acredito que com um fichário de modelo personalizado que carrega a entidade do contexto ou retorna uma nova instância, a abordagem acima funcionaria. Vou atualizar minha resposta para sugerir essa solução.
Andre Luus
Sim, você pode usar um fichário de modelo, mas agora precisa fazer as coisas das outras respostas no fichário de modelo. Apenas move o problema da camada de repo / service para o fichário do modelo. Pelo menos, não vejo uma simplificação real.
Slauma
A simplificação é a exclusão automática de entidades órfãs. Tudo que você precisa no fichário de modelo é um equivalente genérico dereturn context.Items.Find(id) ?? new Item()
Andre Luus
Bom feedback para a equipe da EF, mas sua solução proposta não resolve nada no país da EF, infelizmente.
31514 Chris Moschini
9

Se você estiver usando o AutoMapper com Entity Framework na mesma classe, poderá encontrar esse problema. Por exemplo, se sua turma é

class A
{
    public ClassB ClassB { get; set; }
    public int ClassBId { get; set; }
}

AutoMapper.Map<A, A>(input, destination);

Isso tentará copiar as duas propriedades. Nesse caso, o ClassBId não é anulável. Como o AutoMapper copiará, destination.ClassB = input.ClassB;isso causará um problema.

Defina seu AutoMapper para ignorar a ClassBpropriedade.

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
jsgoupil
fonte
Estou enfrentando um problema semelhante com o AutoMapper, mas isso não funciona para mim :( Veja stackoverflow.com/q/41430679/613605
J86
4

Eu apenas tive o mesmo erro. Eu tenho duas tabelas com um relacionamento pai-filho, mas configurei uma cascata "ao excluir" na coluna de chave estrangeira na definição da tabela filha. Portanto, quando eu excluo manualmente a linha pai (via SQL) no banco de dados, ela exclui automaticamente as linhas filho.

No entanto, isso não funcionou no EF, o erro descrito neste segmento apareceu. O motivo disso foi que, no meu modelo de dados da entidade (arquivo edmx), as propriedades da associação entre o pai e a tabela filho não estavam corretas. A End1 OnDeleteopção foi configurada para ser none("End1" no meu modelo é o final que possui uma multiplicidade de 1).

Mudei manualmente a End1 OnDeleteopção para Cascadee do que funcionou. Não sei por que a EF não consegue captar isso quando atualizo o modelo do banco de dados (eu tenho um primeiro modelo de banco de dados).

Para ser completo, é assim que meu código para excluir se parece:

   public void Delete(int id)
    {
        MyType myObject = _context.MyTypes.Find(id);

        _context.MyTypes.Remove(myObject);
        _context.SaveChanges(); 
   }    

Se eu não tivesse definido uma exclusão em cascata, teria que excluir manualmente as linhas filhas antes de excluir a linha pai.

Martin
fonte
4

Isso acontece porque a Entidade Filha está marcada como Modificada em vez de Excluída.

E a modificação que EF faz na Entidade Filha quando parent.Remove(child)é executada é simplesmente definir a referência ao pai null.

Você pode verificar o EntityState da criança digitando o seguinte código na Janela Imediata do Visual Studio quando a exceção ocorrer, após a execução SaveChanges():

_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity

onde X deve ser substituído pela entidade excluída.

Se você não tiver acesso à ObjectContextexecução _context.ChildEntity.Remove(child), poderá resolver esse problema, tornando a chave estrangeira parte da chave primária na tabela filho.

Parent
 ________________
| PK    IdParent |
|       Name     |
|________________|

Child
 ________________
| PK    IdChild  |
| PK,FK IdParent |
|       Name     |
|________________|

Dessa forma, se você executar parent.Remove(child), a EF marcará corretamente a Entidade como Excluída.

Mauricio Ramalho
fonte
2

Esse tipo de solução fez o truque para mim:

Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID);
db.Childs.RemoveRange(original.Childs);
updated.Childs.ToList().ForEach(c => original.Childs.Add(c));
db.Entry<Parent>(original).CurrentValues.SetValues(updated);

É importante dizer que isso exclui todos os registros e os insere novamente. Mas para o meu caso (menos de 10) está tudo bem.

Espero que ajude.

Wagner Bertolini Junior
fonte
A reinserção acontece com novos IDs ou mantém os IDs da criança que eles tinham em primeiro lugar?
Pepito Fernandez 5/05
2

Encontrei esse problema hoje e queria compartilhar minha solução. No meu caso, a solução foi excluir os itens filhos antes de obter o pai do banco de dados.

Anteriormente, eu estava fazendo isso como no código abaixo. Em seguida, receberei o mesmo erro listado nesta pergunta.

var Parent = GetParent(parentId);
var children = Parent.Children;
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

O que funcionou para mim é obter os itens filhos primeiro, usando o parentId (chave estrangeira) e depois excluir esses itens. Depois, posso obter o pai do banco de dados e, nesse ponto, ele não deve ter mais itens filhos e posso adicionar novos itens filhos.

var children = GetChildren(parentId);
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

var Parent = GetParent(parentId);
Parent.Children = //assign new entities/items here
Dino Bansigan
fonte
2

Você deve limpar manualmente a coleção ChildItems e anexar novos itens nela:

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

Depois disso, você pode chamar o método de extensão DeleteOrphans, que será tratado com entidades órfãs (ele deve ser chamado entre os métodos DetectChanges e SaveChanges).

public static class DbContextExtensions
{
    private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>();

    public static void DeleteOrphans( this DbContext source )
    {
        var context = ((IObjectContextAdapter)source).ObjectContext;
        foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
        {
            var entityType = entry.EntitySet.ElementType as EntityType;
            if (entityType == null)
                continue;

            var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap);
            var props = entry.GetModifiedProperties().ToArray();
            foreach (var prop in props)
            {
                NavigationProperty navProp;
                if (!navPropMap.TryGetValue(prop, out navProp))
                    continue;

                var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
                var enumerator = related.GetEnumerator();
                if (enumerator.MoveNext() && enumerator.Current != null)
                    continue;

                entry.Delete();
                break;
            }
        }
    }

    private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type )
    {
        var result = type.NavigationProperties
            .Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
            .Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType()))
            .Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() })
            .Where(v => v.DependentProperties.Length == 1)
            .ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty);

        return new ReadOnlyDictionary<string, NavigationProperty>(result);
    }
}
Sentou
fonte
Isto funcionou bem para mim. Eu só precisava adicionar context.DetectChanges();.
Andy Edinborough
1

Eu tentei essas soluções e muitas outras, mas nenhuma delas funcionou. Como esta é a primeira resposta no google, adicionarei minha solução aqui.

O método que funcionou bem para mim foi tirar os relacionamentos de cena durante as confirmações, portanto não havia nada para a EF estragar. Eu fiz isso re-encontrando o objeto pai no DBContext e excluindo isso. Como as propriedades de navegação do objeto re-encontrado são todas nulas, os relacionamentos das crianças são ignorados durante a confirmação.

var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();

Observe que isso pressupõe que as chaves estrangeiras estejam configuradas com ON DELETE CASCADE; portanto, quando a linha pai for removida, os filhos serão limpos pelo banco de dados.

Steve
fonte
1

Usei a solução de Mosh , mas não me era óbvio como implementar a chave de composição corretamente no código primeiro.

Então, aqui está a solução:

public class Holiday
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int HolidayId { get; set; }
    [Key, Column(Order = 1), ForeignKey("Location")]
    public LocationEnum LocationId { get; set; }

    public virtual Location Location { get; set; }

    public DateTime Date { get; set; }
    public string Name { get; set; }
}
PeterB
fonte
1

Eu tinha o mesmo problema, mas sabia que funcionava bem em outros casos, então reduzi o problema para isso:

parent.OtherRelatedItems.Clear();  //this worked OK on SaveChanges() - items were being deleted from DB
parent.ProblematicItems.Clear();   // this was causing the mentioned exception on SaveChanges()
  • OtherRelatedItems tinha uma Chave Primária composta (parentId + alguma coluna local) e funcionou OK
  • ProblematicItems tinham sua própria chave primária de coluna única e o parentId era apenas um FK. Isso estava causando a exceção após Clear ().

Tudo o que eu precisava fazer era tornar o ParentId parte da PK composta para indicar que os filhos não podem existir sem um pai. Usei o modelo DB-first, adicionei a PK e marquei a coluna parentId como EntityKey (então, tive que atualizá-la no DB e EF - não tenho certeza se a EF seria suficiente).

Tornei RequestId parte do PK E, em seguida, atualizei o modelo EF E defina a outra propriedade como parte da Chave da entidade

Depois que você pensa sobre isso, é uma distinção muito elegante que a EF usa para decidir se os filhos "fazem sentido" sem um pai (neste caso, Clear () não os excluirá e emitirá uma exceção, a menos que você defina o ParentId como algo diferente / especial ) ou, como na pergunta original, esperamos que os itens sejam excluídos assim que forem removidos do pai.

Ekus
fonte
0

Esse problema ocorre porque tentamos excluir os dados da tabela pai ainda estão presentes na tabela pai. Resolvemos o problema com a ajuda da exclusão em cascata.

No modelo, crie o método na classe dbcontext.

 modelBuilder.Entity<Job>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                .WithRequired(C => C.Job)
                .HasForeignKey(C => C.JobId).WillCascadeOnDelete(true);
            modelBuilder.Entity<Sport>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                  .WithRequired(C => C.Sport)
                  .HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);

Depois disso, em nossa chamada à API

var JobList = Context.Job                       
          .Include(x => x.JobSportsMappings)                                     .ToList();
Context.Job.RemoveRange(JobList);
Context.SaveChanges();

Excluir em cascata opção de a tabela pai e a tabela filho relacionada ao pai com este código simples. Faça com que tente desta maneira simples.

Remover intervalo usado para excluir a lista de registros no banco de dados

Sowmiya V
fonte
0

Também resolvi meu problema com a resposta de Mosh e achei que a resposta de PeterB era um pouco, já que ela usava um enum como chave estrangeira. Lembre-se de que você precisará adicionar uma nova migração após adicionar esse código.

Também posso recomendar esta postagem no blog para outras soluções:

http://www.kianryan.co.uk/2013/03/orphaned-child/

Código:

public class Child
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Heading { get; set; }
    //Add other properties here.

    [Key, Column(Order = 1)]
    public int ParentId { get; set; }

    public virtual Parent Parent { get; set; }
}
Ogglas
fonte
0

Usando a solução do Slauma, criei algumas funções genéricas para ajudar a atualizar objetos filho e coleções de objetos filho.

Todos os meus objetos persistentes implementam essa interface

/// <summary>
/// Base interface for all persisted entries
/// </summary>
public interface IBase
{
    /// <summary>
    /// The Id
    /// </summary>
    int Id { get; set; }
}

Com isso, implementei essas duas funções no meu Repositório

    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public T AddOrUpdateEntry<T>(DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.Id == 0 || orgEntry == null)
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            Context.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public void AddOrUpdateCollection<T>(DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }

Para usá-lo, faço o seguinte:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

Espero que isto ajude


EXTRA: Você também pode criar uma classe DbContextExtentions (ou sua própria interface de contexto) separada:

public static void DbContextExtentions {
    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public static T AddOrUpdateEntry<T>(this DbContext _dbContext, DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.IsNew || orgEntry == null) // New or not found in context
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            _dbContext.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public static void AddOrUpdateCollection<T>(this DbContext _dbContext, DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(_dbContext, set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }
}

e use-o como:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = _dbContext.AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);
Bluemoon74
fonte
Você também pode criar uma classe de extensão para o seu contexto com estas funções:
Bluemoon74
0

Eu estava enfrentando o mesmo problema ao excluir meu registro do que ocorreu algum problema, pois essa solução é que quando você exclui seu registro do que está perdendo alguma coisa antes de excluir o cabeçalho / registro mestre, deve escrever no código para exclua seus detalhes antes do cabeçalho / mestre Espero que o problema seja resolvido.

Ghazi Hur
fonte
-1

Encontrei esse problema antes de várias horas e tentei de tudo, mas no meu caso, a solução era diferente da listada acima.

Se você usar a entidade já recuperada do banco de dados e tentar modificá-lo, o erro ocorrerá, mas se você obter uma cópia nova da entidade do banco de dados, não haverá problemas. Não use isso:

 public void CheckUsersCount(CompanyProduct companyProduct) 
 {
     companyProduct.Name = "Test";
 }

Usa isto:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
Tanyo Ivanov
fonte