entity-framework - progetto - entity framework guida




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

Sto ricevendo questo errore quando ho GetById () su un'entità e poi ho impostato la raccolta di entità 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'oggetto 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 entità? 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 classe 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 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 entità 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<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);
    }
}

È 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 entità genitore. Devi dire a Entity Framework la tua decisione. Ma una di queste due decisioni che devi prendere in quanto le entità 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.


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

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 entità (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 prima di diverse ore e provo tutto, ma nel mio caso la soluzione era diversa da quella sopra elencata.

Se si utilizza l'entità già recuperata dal database e si tenta di modificarlo, l'errore si verificherà, ma se si ottiene una nuova copia dell'entità 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";
 }

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'oggetto genitore in DBContext e cancellandolo. Poiché le proprietà di navigazione dell'oggetto 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.


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

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

In composizione, l'oggetto 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'oggetto figlio può esistere indipendentemente dal suo genitore . Se il genitore è distrutto, l'oggetto 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'oggetto 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'oggetto 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'entità 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, è possibile 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'entità), 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();

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

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


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

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

AutoMapper.Map<A, A>(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<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId




entity-framework-4.1