La relazione non può essere modificata perché una o più proprietà della chiave esterna non sono annullabili

Sto ricevendo questo errore quando ho GetById () su un’entity framework e poi ho impostato la raccolta di entity framework figlio sul mio nuovo elenco che proviene dalla vista MVC.

L’operazione non è riuscita: la relazione non può essere modificata perché una o più proprietà della chiave esterna non sono annullabili. Quando viene apportata una modifica a una relazione, la relativa proprietà della chiave esterna viene impostata su un valore nullo. Se la chiave esterna non supporta valori nulli, deve essere definita una nuova relazione, la proprietà chiave esterna deve essere assegnata a un altro valore non nullo o l’object non correlato deve essere eliminato.

Non capisco bene questa linea:

La relazione non può essere modificata perché una o più proprietà della chiave esterna non sono annullabili.

Perché dovrei cambiare la relazione tra 2 quadro? Dovrebbe rimanere lo stesso per tutta la durata dell’intera applicazione.

Il codice su cui si verifica l’eccezione è l’assegnazione semplice di classi child modificate in una raccolta alla class genitore esistente. Si spera che questo provveda alla rimozione di classi di bambini, aggiunte di nuove e modifiche. Avrei pensato che Entity Framework gestisse questo.

Le linee di codice possono essere distillate per:

var thisParent = _repo.GetById(1); thisParent.ChildItems = modifiedParent.ChildItems(); _repo.Save(); 

È necessario eliminare manualmente gli elementi figlio precedente thisParent.ChildItems uno alla volta. Entity Framework non lo fa per te. Alla fine non è in grado di decidere cosa vuoi fare con i vecchi oggetti figlio – se vuoi buttarli via o se vuoi mantenerli e assegnarli ad altre quadro genitore. Devi dire a Entity Framework la tua decisione. Ma una di queste due decisioni che devi prendere in quanto le quadro figlio non possono vivere da sole senza un riferimento a qualsiasi genitore nel database (a causa del vincolo di chiave esterna). Questo è fondamentalmente ciò che dice l’eccezione.

modificare

Cosa farei se si potessero aggiungere, aggiornare ed eliminare articoli secondari:

 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: questo non è testato. Si suppone che la raccolta di elementi figlio sia di tipo ICollection . (Di solito ho IList e quindi il codice sembra un po ‘diverso.) Ho anche rimosso tutte le astrazioni del repository per mantenerlo semplice.

Non so se questa sia una buona soluzione, ma credo che si debba fare una sorta di duro lavoro lungo queste linee per prendersi cura di tutti i tipi di cambiamenti nella raccolta di navigazione. Sarei anche felice di vedere un modo più semplice per farlo.

Il motivo per cui stai affrontando questo è dovuto alla differenza tra composizione e aggregazione .

In composizione, l’object figlio viene creato quando il genitore viene creato e distrutto quando il genitore viene distrutto . Quindi la sua durata è controllata dal suo genitore. ad esempio un post sul blog e i suoi commenti. Se un post viene cancellato, i suoi commenti dovrebbero essere cancellati. Non ha senso avere commenti per un post che non esiste. Lo stesso per ordini e articoli dell’ordine.

In aggregazione, l’object figlio può esistere indipendentemente dal suo genitore . Se il genitore è distrutto, l’object figlio può ancora esistere, in quanto può essere aggiunto a un genitore diverso in seguito. es .: la relazione tra una playlist e le canzoni in quella playlist. Se la playlist è stata cancellata, i brani non dovrebbero essere cancellati. Possono essere aggiunti a una playlist diversa.

Il modo in cui Entity Framework distingue le relazioni di aggregazione e composizione è il seguente:

  • Per la composizione: si aspetta che l’object figlio abbia una chiave primaria composta (ParentID, ChildID). Questo è designato in quanto gli ID dei bambini dovrebbero essere nel raggio d’azione dei loro genitori.

  • Per l’aggregazione: si aspetta che la proprietà della chiave esterna nell’object figlio sia annullabile.

Pertanto, il motivo per cui hai riscontrato questo problema è dovuto al modo in cui hai impostato la tua chiave primaria nella tabella figlio. Dovrebbe essere composito, ma non lo è. Quindi, Entity Framework vede questa associazione come aggregazione, il che significa che quando rimuovi o cancelli gli oggetti figli, non cancellerà i record figli. Rimuove semplicemente l’associazione e imposta la colonna della chiave esterna corrispondente su NULL (in modo che quei record figlio possano essere successivamente associati a un altro genitore). Poiché la tua colonna non consente NULL, ottieni l’eccezione che hai menzionato.

soluzioni:

1- Se si dispone di un motivo valido per non voler utilizzare una chiave composta, è necessario eliminare gli oggetti figlio in modo esplicito. E questo può essere fatto più semplice delle soluzioni suggerite in precedenza:

 context.Children.RemoveRange(parent.Children); 

2- In caso contrario, impostando la chiave primaria corretta sulla tabella figlio, il codice apparirà più significativo:

 parent.Children.Clear(); 

Questo è un grosso problema. Ciò che effettivamente accade nel tuo codice è questo:

  • Si carica Parent dal database e si ottiene un’ quadro collegata
  • Sostituisci la sua collezione di bambini con una nuova collezione di bambini separati
  • Si salvano le modifiche, ma durante questa operazione tutti i bambini sono considerati come aggiunti perché EF non li conosceva fino a quel momento. Quindi EF prova a impostare null su chiave esterna di vecchi bambini e inserisce tutti i nuovi figli => righe duplicate.

Ora la soluzione dipende davvero da cosa vuoi fare e come ti piacerebbe farlo?

Se si utilizza ASP.NET MVC, è ansible provare a utilizzare UpdateModel o TryUpdateModel .

Se vuoi semplicemente aggiornare manualmente i bambini esistenti, puoi semplicemente fare qualcosa del tipo:

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

L’attaccamento non è in realtà necessario (l’impostazione dello stato su Modified comporta anche l’associazione dell’ quadro), ma mi piace perché rende il processo più ovvio.

Se vuoi modificare esistenti, eliminare esistenti e inserire nuovi child devi fare qualcosa del tipo:

 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(); 

Ho trovato questa risposta molto più utile per lo stesso errore. Sembra che EF non gli piaccia quando rimuovi, preferisce Elimina.

È ansible eliminare una raccolta di record allegata a un record come questo.

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

Nell’esempio, tutti i record Dettagli associati a un ordine hanno lo stato impostato su Elimina. (In preparazione per aggiungere dettagli aggiornati, come parte di un aggiornamento dell’ordine)

Non ho idea del perché le altre due risposte siano così popolari!

Credo che tu abbia ragione nel presumere che il framework ORM debba gestirlo – dopotutto, è ciò che promette di fornire. Altrimenti il ​​tuo modello di dominio viene corrotto dai problemi di persistenza. NHibernate gestisce questo felicemente se si impostano correttamente le impostazioni di cascata. In Entity Framework è anche ansible, si aspettano solo che tu segua standard migliori quando imposti il ​​tuo modello di database, specialmente quando devono dedurre quale cascata dovrebbe essere fatta:

Devi definire correttamente la relazione genitore-figlio usando una ” relazione identificativa “.

Se si esegue questa operazione, Entity Framework sa che l’object figlio è identificato dal genitore e, pertanto, deve essere una situazione di “cascata-eliminazione-orfani”.

Oltre a quanto sopra, potrebbe essere necessario (dall’esperienza di NHibernate)

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

invece di sostituire interamente la lista.

AGGIORNARE

@ Il commento di Slauma mi ha ricordato che le quadro distaccate sono un’altra parte del problema generale. Per risolvere ciò, è ansible adottare l’approccio dell’utilizzo di un raccoglitore modello personalizzato che costruisce i modelli tentando di caricarlo dal contesto. Questo post sul blog mostra un esempio di cosa intendo.

Se si utilizza AutoMapper con Entity Framework sulla stessa class, è ansible che si verifichi questo problema. Ad esempio se la tua class è

 class A { public ClassB ClassB { get; set; } public int ClassBId { get; set; } } AutoMapper.Map(input, destination); 

Questo proverà a copiare entrambe le proprietà. In questo caso, ClassBId non è Nullable. Poiché AutoMapper copierà destination.ClassB = input.ClassB; questo causerà un problema.

Imposta il tuo AutoMapper per ignorare la proprietà ClassB .

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

Ciò accade perché l’ quadro figlio viene contrassegnata come Modificata anziché Eliminata.

E la modifica che EF fa parent.Remove(child) quando parent.Remove(child) viene eseguita, sta semplicemente impostando il riferimento al suo genitore su null .

È ansible controllare EntityState del bambino digitando il seguente codice nella finestra immediata di Visual Studio quando si verifica l’eccezione, dopo l’esecuzione di SaveChanges() :

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

dove X dovrebbe essere sostituito dall’ quadro cancellata.

Se non si ha accesso a ObjectContext per eseguire _context.ChildEntity.Remove(child) , è ansible risolvere questo problema rendendo la chiave esterna una parte della chiave primaria nella tabella figlio.

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

In questo modo, se si esegue parent.Remove(child) , EF segnerà correttamente l’ quadro come eliminata.

Ho appena avuto lo stesso errore. Ho due tabelle con una relazione padre figlio, ma ho configurato un “on delete cascade” sulla colonna chiave esterna nella definizione tabella della tabella figlio. Quindi, quando elimino manualmente la riga genitore (tramite SQL) nel database, eliminerà automaticamente le righe figlio.

Tuttavia questo non ha funzionato in EF, l’errore descritto in questo thread è apparso. Il motivo era che, nel mio modello di dati di quadro (file edmx), le proprietà dell’associazione tra la tabella padre e quella figlio non erano corrette. L’opzione End1 OnDelete stata configurata come none (“End1” nel mio modello è la fine che ha una molteplicità di 1).

Ho modificato manualmente l’opzione End1 OnDelete su Cascade e poi ha funzionato. Non so perché EF non sia in grado di capirlo, quando aggiorno il modello dal database (ho un primo modello di database).

Per completezza, questo è il modo in cui il mio codice da eliminare assomiglia a:

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

Se non avessi eliminato l’eliminazione a cascata, dovrei eliminare manualmente le righe figlio prima di eliminare la riga genitore.

Ho incontrato questo problema oggi e volevo condividere la mia soluzione. Nel mio caso, la soluzione era eliminare gli oggetti Child prima di ottenere il Parent dal database.

Precedentemente lo stavo facendo come nel codice qui sotto. In questo caso otterrò lo stesso errore elencato in questa domanda.

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

Ciò che ha funzionato per me, è quello di ottenere gli elementi figli in primo luogo, utilizzando il parentId (chiave esterna) e quindi eliminare tali elementi. Quindi posso ottenere il genitore dal database e, a quel punto, non dovrebbe più avere elementi figli e posso aggiungere nuovi elementi figli.

 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 

È necessario cancellare manualmente la raccolta ChildItems e aggiungere nuovi elementi al suo interno:

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

Dopodiché puoi chiamare il metodo di estensione DeleteOrphans che gestirà con quadro orfane (deve essere chiamato tra i metodi 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); } } 

Ho provato queste soluzioni e molte altre, ma nessuna ha funzionato. Poiché questa è la prima risposta su google, aggiungerò qui la mia soluzione.

Il metodo che ha funzionato bene per me è stato quello di prendere le relazioni fuori dal quadro durante i commit, quindi non c’era nulla che l’EF potesse rovinare. Ho fatto questo ritrovando l’object genitore in DBContext e cancellandolo. Poiché le proprietà di navigazione dell’object re-found sono tutte nulle, le relazioni dei childrens vengono ignorate durante il commit.

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

Si noti che questo presuppone che le chiavi esterne siano impostate con ON DELETE CASCADE, quindi quando la riga padre viene rimossa, i figli verranno eliminati dal database.

Questo tipo di soluzione ha fatto il trucco per me:

 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 dire che questo elimina tutti i record e li inserisce nuovamente. Ma per il mio caso (meno di 10) va bene.

Spero possa essere d’aiuto.

Ho incontrato questo problema prima di diverse ore e provo tutto, ma nel mio caso la soluzione era diversa da quella sopra elencata.

Se si utilizza l’ quadro già recuperata dal database e si tenta di modificarlo, l’errore si verificherà, ma se si ottiene una nuova copia dell’ quadro dal database, non dovrebbero esserci problemi. Non usare questo:

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

Usa questo:

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

Questo problema si verifica perché si tenta di eliminare la tabella padre, mentre i dati della tabella figlio sono presenti. Risolviamo il problema con l’aiuto dell’eliminazione a cascata.

Nel metodo Create del modello nella 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); 

Successivamente, nella nostra chiamata API

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

L’ opzione di eliminazione in cascata elimina la tabella padre genitore e relativa genitore con questo semplice codice. Fallo provare in questo modo semplice.

Rimuovi intervallo che è stato utilizzato per eliminare l’elenco di record nel database Grazie

Ho usato la soluzione di Mosh , ma non era ovvio come implementare correttamente la chiave di composizione nel codice.

Quindi ecco la soluzione:

 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; } } 

Ho anche risolto il mio problema con la risposta di Mosh e ho pensato che la risposta di PeterB era un po ‘da quando usava un enum come chiave straniera. Ricorda che dovrai aggiungere una nuova migrazione dopo aver aggiunto questo codice.

Posso anche raccomandare questo post sul blog per altre soluzioni:

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

Codice:

 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; } }