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

Eu estou recebendo esse erro quando eu GetById () em uma entidade e, em seguida, defina 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 propriedades de chave estrangeira não são anuláveis. Quando uma alteração é feita em um relacionamento, a propriedade de chave estrangeira relacionada é configurada para um valor nulo. Se a chave estrangeira não suportar valores nulos, um novo relacionamento deve ser definido, a propriedade de chave externa deve receber outro valor não nulo ou o object não relacionado deve ser excluído.

Eu não entendo muito bem essa linha:

O relacionamento não pôde ser alterado porque uma ou mais propriedades de chave estrangeira não sã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 classs filho modificadas em uma coleção à class pai existente. Espera-se que isso resolva a remoção de classs filhas, a 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(); 

Você deve excluir itens filho antigos thisParent.ChildItems um a um manualmente. O Entity Framework não faz isso por você. Ele finalmente não pode decidir o que você quer fazer com os itens filhos antigos – se quiser jogá-los fora ou se quiser mantê-los e atribuí-los a outras entidades pai. Você deve informar ao Entity Framework sua decisão. Mas uma dessas duas decisões que você TEM de tomar, já que as entidades filho não podem viver sozinhas sem uma referência a qualquer pai no database (devido à restrição de chave estrangeira). Isso é basicamente o que a exceção diz.

Editar

O que eu faria se itens filhos 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 é testado. Está assumindo que a coleção de itens filhos é do tipo ICollection . (Eu geralmente tenho IList e, em seguida, o código parece um pouco diferente.) Eu também tirei todas as abstrações do repository para mantê-lo simples.

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

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

Na composição, o object filho é criado quando o pai é criado e é destruído quando seu pai é destruído . Portanto, seu tempo de vida é controlado pelo pai. Por exemplo, uma postagem no 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 pedido.

Na agregação, o object filho pode existir independentemente de seu pai . Se o pai for destruído, o object filho ainda pode existir, pois pode ser adicionado a um pai diferente posteriormente. por exemplo: a relação entre uma lista de reprodução e as músicas dessa 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: ele espera que o object filho tenha uma chave primária composta (ParentID, ChildID). Isso ocorre porque os IDs das crianças devem estar dentro do escopo de seus pais.

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

Então, o motivo pelo qual você está tendo esse problema é por causa de como você definiu sua chave primária em sua tabela filha. 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 desmarca os objects filhos, não é necessário excluir os registros filhos. Ele simplesmente removerá a associação e definirá a coluna de chave estrangeira correspondente como NULL (para que os registros filhos possam ser associados posteriormente a um pai diferente). Como sua coluna não permite NULL, você recebe a exceção mencionada.

Soluções:

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

 context.Children.RemoveRange(parent.Children); 

2- Caso contrário, definindo a chave primária adequada em sua tabela filha, seu código parecerá mais significativo:

 parent.Children.Clear(); 

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

  • Você carrega o Parent do database e obtém uma entidade conectada
  • Você substitui sua coleção infantil por uma nova coleção de crianças separadas
  • Você salva as alterações, mas durante esta operação todas as crianças são consideradas como adicionadas, porque a EF não as conhecia até esse momento. Então, o EF tenta definir nulo para a chave estrangeira de filhos antigos e inserir todos os novos filhos => linhas duplicadas.

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

Se você estiver usando o asp.net MVC, você pode tentar usar UpdateModel ou TryUpdateModel .

Se você quer apenas atualizar os filhos existentes manualmente, você pode simplesmente 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 como Modified também appendá a entidade), mas eu gosto disso porque torna o processo mais óbvio.

Se você quiser modificar existente, excluir existente e inserir novos filhos, você 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(); 

Achei essa resposta muito mais útil para o mesmo erro. Parece que a EF não gosta quando você remove, ele 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 detalhes atualizados, como parte de uma atualização de pedido)

Eu não tenho idéia porque as outras duas respostas são tão populares!

Eu acredito que você estava certo em assumir que o framework 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ê configurar corretamente as configurações em cascata. No Entity Framework também é possível, eles apenas esperam que você siga padrões melhores ao configurar seu modelo de database, especialmente quando eles têm que inferir o que deve ser feito em cascata:

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

Se você fizer isso, o Entity Framework saberá que o object filho é identificado pelo pai e, portanto, deve ser uma situação “cascade-delete-orphans”.

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

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

em vez de replace a lista completamente.

ATUALIZAR

O comentário de @ Slauma me lembrou que as entidades destacadas são outra parte do problema geral. Para resolver isso, você pode usar a abordagem de usar um 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.

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

 class A { public ClassB ClassB { get; set; } public int ClassBId { get; set; } } AutoMapper.Map(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 propriedade ClassB .

  cfg.CreateMap() .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId 

Isso acontece porque a entidade filho está marcada como modificada em vez de excluída.

E a modificação que EF faz para a Entidade Criança quando parent.Remove(child) é executado, é simplesmente definir a referência para seu pai como null .

Você pode verificar o EntityState da criança digitando o seguinte código na janela Immediate do Visual Studio quando a exceção ocorrer, após executar 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 access ao ObjectContext para executar _context.ChildEntity.Remove(child) , poderá resolver esse problema fazendo da chave estrangeira uma parte da chave primária na tabela filha.

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

Desta forma, se você executar parent.Remove(child) , o EF irá marcar corretamente a Entity como Deleted.

Eu acabei de ter o mesmo erro. Eu tenho duas tabelas com um relacionamento filho pai, mas eu configurei um “em cascata de exclusão” na coluna de chave estrangeira na definição de tabela da tabela filho. Portanto, quando eu excluir manualmente a linha pai (via SQL) no database, excluirá automaticamente as linhas filhas.

No entanto, isso não funcionou no EF, o erro descrito neste segmento apareceu. A razão para isso foi que, no modelo de dados da minha entidade (arquivo edmx), as propriedades da associação entre pai e filho não estavam corretas. A opção End1 OnDelete foi configurada como none (“End1” no meu modelo é o final que tem uma multiplicidade de 1).

Eu mudei manualmente a opção End1 OnDelete para Cascade e que funcionou. Eu não sei porque EF não é capaz de pegar isso, quando eu atualizar o modelo do database (eu tenho um primeiro modelo de database).

Para completar, é assim que meu código para excluir se parece com:

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

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

Eu me deparei com 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 database.

Anteriormente eu estava fazendo isso como no código abaixo. Em seguida, obtenho o mesmo erro listado nessa 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. Então eu posso obter o pai do database e nesse ponto, ele não deve ter mais nenhum item filho e eu 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 

Você deve limpar manualmente a coleção ChildItems e acrescentar novos itens a ela:

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

Depois disso, você pode chamar o método de extensão DeleteOrphans, que tratará de entidades órfãs (deve ser chamado entre os methods 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 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(result); } } 

Eu tentei estas 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 da imagem durante os commits, então não havia nada para a EF estragar. Eu fiz isso re-encontrando o object pai no DBContext e excluindo isso. Como as propriedades de navegação do object reimplantado são todas null, os relacionamentos das childrens são ignorados durante o commit.

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

Observe que isso pressupõe que as foreign keys sejam configuradas com ON DELETE CASCADE, portanto, quando a linha pai for removida, as crianças serão limpas pelo database.

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

 Parent original = db.Parent.SingleOrDefault(t => t.ID == updated.ID); db.Childs.RemoveRange(original.Childs); updated.Childs.ToList().ForEach(c => original.Childs.Add(c)); db.Entry(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.

Eu encontrei esse problema antes de várias horas e tente de tudo, mas no meu caso a solução foi diferente da listada acima.

Se você usar a entidade já recuperada do database e tentar modificá-la, o erro ocorrerá, mas se você obter uma nova cópia da entidade do database, 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"; } 

Este problema surge porque tentamos excluir os dados da tabela pai ainda tabela filho está presente. Nós resolvemos o problema com a ajuda da exclusão em cascata.

No modelo Criar método na class dbcontext.

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

Depois disso, na nossa chamada de API

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

Opção de exclusão em cascata exclua a tabela filha pai e pai relacionada com este código simples. Faça isso de maneira simples.

Remover Range, que usado para excluir a lista de registros no database Obrigado

Eu usei a solução de Mosh , mas não era óbvio para mim 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; } } 

Também resolvi meu problema com a resposta de Mosh e achei que a resposta de PeterB fosse um pouco, já que usava um enum como chave estrangeira. Lembre-se de que você precisará adicionar uma nova migration depois de adicionar esse código.

Também posso recomendar este post 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; } }