Come posso impedire a Entity Framework di provare a salvare / inserire oggetti figlio?


100

Quando salvo un'entità con Entity Framework, ho naturalmente pensato che avrebbe provato a salvare solo l'entità specificata. Tuttavia, sta anche tentando di salvare le entità figlio di quell'entità. Ciò sta causando tutti i tipi di problemi di integrità. Come faccio a forzare EF a salvare solo l'entità che voglio salvare e quindi a ignorare tutti gli oggetti figlio?

Se imposto manualmente le proprietà su null, viene visualizzato un messaggio di errore "Operazione non riuscita: la relazione non può essere modificata perché una o più proprietà della chiave esterna non sono annullabili". Questo è estremamente controproducente poiché ho impostato l'oggetto figlio su null in modo specifico in modo che EF lo lasci da solo.

Perché non voglio salvare / inserire gli oggetti figlio?

Poiché questo viene discusso avanti e indietro nei commenti, fornirò alcune giustificazioni del motivo per cui voglio che gli oggetti di mio figlio vengano lasciati in pace.

Nell'applicazione che sto creando, il modello a oggetti EF non viene caricato dal database ma utilizzato come oggetti dati che sto popolando durante l'analisi di un file flat. Nel caso degli oggetti figlio, molti di questi fanno riferimento a tabelle di ricerca che definiscono varie proprietà della tabella padre. Ad esempio, la posizione geografica dell'entità principale.

Poiché ho popolato questi oggetti da solo, EF presume che si tratti di nuovi oggetti e che debbano essere inseriti insieme all'oggetto padre. Tuttavia, queste definizioni esistono già e non voglio creare duplicati nel database. Uso l'oggetto EF solo per eseguire una ricerca e popolare la chiave esterna nella mia entità di tabella principale.

Anche con gli oggetti figlio che sono dati reali, ho bisogno di salvare prima il genitore e ottenere una chiave primaria o EF sembra solo fare un pasticcio di cose. Spero che questo dia qualche spiegazione.


Per quanto ne so, dovrai annullare gli oggetti figlio.
Johan

Ciao Johan. Non funziona Genera errori se annullo la raccolta. A seconda di come lo faccio, si lamenta che le chiavi siano nulle o che la raccolta sia stata modificata. Ovviamente, quelle cose sono vere, ma l'ho fatto apposta in modo da lasciare in pace gli oggetti che non dovrebbe toccare.
Mark Micallef

Euforico, questo è completamente inutile.
Mark Micallef

@Euphoric Anche quando non si modificano gli oggetti figlio, EF tenta comunque di inserirli per impostazione predefinita e non ignorarli o aggiornarli.
Johan

Quello che mi infastidisce davvero è che se faccio di tutto per annullare effettivamente quegli oggetti, allora si lamenta invece di rendersi conto che voglio che li lasci in pace. Poiché quegli oggetti figlio sono tutti facoltativi (nullable nel database), c'è un modo per forzare EF a dimenticare che avevo quegli oggetti? cioè eliminare il suo contesto o la cache in qualche modo?
Mark Micallef

Risposte:


55

Per quanto ne so, hai due opzioni.

Opzione 1)

Null tutti gli oggetti figlio, questo assicurerà che EF non aggiunga nulla. Inoltre non cancellerà nulla dal tuo database.

Opzione 2)

Impostare gli oggetti figlio come scollegati dal contesto utilizzando il codice seguente

 context.Entry(yourObject).State = EntityState.Detached

Nota che non puoi scollegare un file List/ Collection. Dovrai scorrere l'elenco e staccare ogni elemento nell'elenco in questo modo

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}

Ciao Johan, ho provato a scollegare una delle raccolte e ha generato il seguente errore: Il tipo di entità HashSet`1 non fa parte del modello per il contesto corrente.
Mark Micallef

@MarkyMark Non scollegare la raccolta. Dovrai eseguire il ciclo della raccolta e scollegarla oggetto per oggetto (aggiornerò ora la mia risposta).
Johan

11
Sfortunatamente, anche con un elenco di loop per staccare tutto, sembra che EF stia ancora cercando di inserire in alcune delle tabelle correlate. A questo punto, sono pronto per estrarre EF e tornare a SQL che almeno si comporta in modo ragionevole. Che dolore.
Mark Micallef

2
Posso usare context.Entry (yourObject) .State prima ancora di aggiungerlo?
Thomas Klammer

1
@ mirind4 Non ho usato lo stato. Se inserisco un oggetto, mi assicuro che tutti i figli siano nulli. All'aggiornamento ottengo prima l'oggetto senza i bambini.
Thomas Klammer

41

Per farla breve: usa la chiave esterna e ti salverà la giornata.

Supponi di avere un'entità Scuola e una Città , e questa è una relazione molti-a-uno in cui una città ha molte scuole e una scuola appartiene a una città. E supponi che le città siano già esistenti nella tabella di ricerca, quindi NON vuoi che vengano inserite di nuovo quando si inserisce una nuova scuola.

Inizialmente potresti definirti entità come questa:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

E potresti eseguire l' inserimento della scuola in questo modo (supponiamo che tu abbia già la proprietà City assegnata al newItem ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

L'approccio di cui sopra può funzionare perfettamente in questo caso, tuttavia, preferisco l' approccio della chiave esterna che per me è più chiaro e flessibile. Vedi la soluzione aggiornata di seguito:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

In questo modo si definisce esplicitamente che la Scuola ha una chiave esterna City_Id e si riferisce all'entità City . Quindi, quando si tratta dell'inserimento di Scuola , puoi fare:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

In questo caso, specifichi esplicitamente City_Id del nuovo record e rimuovi la City dal grafico in modo che EF non si preoccupi di aggiungerlo al contesto insieme a School .

Anche se alla prima impressione l'approccio della chiave straniera sembra più complicato, ma credimi questa mentalità ti farà risparmiare molto tempo quando si tratta di inserire una relazione molti-a-molti (immaginando di avere una relazione Scuola e Studente, e lo Studente ha una proprietà City) e così via.

Spero che questo ti sia utile.


Ottima risposta, mi ha aiutato molto! Ma non sarebbe meglio indicare il valore minimo di 1 per City_Id utilizzando l' [Range(1, int.MaxValue)]attributo?
Dan Rayson

Come un sogno!! Grazie mille!
CJH

Ciò rimuoverebbe il valore dall'oggetto School nel chiamante. SaveChanges ricarica il valore delle proprietà di navigazione null? In caso contrario, il chiamante dovrebbe ricaricare l'oggetto City dopo aver chiamato il metodo Insert () se ha bisogno di queste informazioni. Questo è lo schema che ho usato spesso, ma sono ancora aperto a schemi migliori se qualcuno ne ha uno buono.
sbilenco

1
Questo è stato davvero utile per me. EntityState.Unchaged è esattamente ciò di cui avevo bisogno per assegnare un oggetto che rappresenta una chiave esterna della tabella di ricerca in un grande oggetto grafico che stavo salvando come una singola transazione. EF Core genera un messaggio di errore meno intuitivo IMO. Ho memorizzato nella cache le mie tabelle di ricerca che cambiano raramente per motivi di prestazioni. Il tuo presupposto è che, poiché il PK dell'oggetto della tabella di ricerca è lo stesso di quello che è già nel database, sa che è invariato e per assegnare semplicemente il FK all'elemento esistente. Invece di provare a inserire l'oggetto della tabella di ricerca come nuovo.
tnk479

Nessuna combinazione di annullamento o impostazione dello stato su invariato funziona per me. EF insiste sul fatto che è necessario inserire un nuovo record figlio quando voglio solo aggiornare un campo scalare nel genitore.
BobRz

21

Se vuoi solo memorizzare le modifiche a un oggetto padre ed evitare di memorizzare le modifiche a uno qualsiasi dei suoi oggetti figlio , perché non fare semplicemente quanto segue:

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

La prima riga collega l'oggetto genitore e l'intero grafico dei suoi oggetti figli dipendenti al contesto in Unchangedstato.

La seconda riga cambia lo stato solo per l'oggetto padre, lasciando i suoi figli nello Unchangedstato.

Si noti che utilizzo un contesto appena creato, quindi questo evita di salvare altre modifiche al database.


2
Questa risposta ha il vantaggio che se qualcuno arriva più tardi e aggiunge un oggetto figlio, non interromperà il codice esistente. Si tratta di una soluzione di "partecipazione" in cui gli altri richiedono di escludere esplicitamente gli oggetti figlio.
Jim,

Questo non funziona più nel core. "Allega: allega ogni entità raggiungibile, tranne quando un'entità raggiungibile ha una chiave generata dal negozio e non è assegnato alcun valore della chiave; queste saranno contrassegnate come aggiunte." Se anche i bambini sono nuovi, verranno aggiunti.
mmix

14

Una delle soluzioni suggerite è assegnare la proprietà di navigazione dallo stesso contesto di database. In questa soluzione, la proprietà di navigazione assegnata dall'esterno del contesto del database verrebbe sostituita. Si prega di vedere il seguente esempio per l'illustrazione.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Salvataggio nel database:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft lo inserisce come funzionalità, tuttavia lo trovo fastidioso. Se l'oggetto reparto associato all'oggetto società ha un ID già esistente nel database, perché EF non associa semplicemente l'oggetto società all'oggetto database? Perché dovremmo prenderci cura dell'associazione da soli? Prendersi cura della proprietà di navigazione durante l'aggiunta di un nuovo oggetto è qualcosa come spostare le operazioni del database da SQL a C #, scomode per gli sviluppatori.


Sono d'accordo al 100% che è ridicolo che EF tenti di creare un nuovo record quando viene fornito l'ID. Se ci fosse un modo per riempire le opzioni dell'elenco di selezione con l'oggetto reale, saremmo in affari.
T3.0

10

Innanzitutto è necessario sapere che esistono due modi per aggiornare l'entità in EF.

  • Oggetti allegati

Quando si modifica la relazione degli oggetti collegati al contesto dell'oggetto utilizzando uno dei metodi descritti in precedenza, Entity Framework deve mantenere sincronizzati chiavi esterne, riferimenti e raccolte.

  • Oggetti scollegati

Se stai lavorando con oggetti disconnessi, devi gestire manualmente la sincronizzazione.

Nell'applicazione che sto creando, il modello a oggetti EF non viene caricato dal database ma utilizzato come oggetti dati che sto popolando durante l'analisi di un file flat.

Ciò significa che stai lavorando con un oggetto disconnesso, ma non è chiaro se stai utilizzando un'associazione indipendente o un'associazione di chiave esterna .

  • Inserisci

    Quando si aggiunge una nuova entità con l'oggetto figlio esistente (oggetto che esiste nel database), se l'oggetto figlio non viene tracciato da EF, l'oggetto figlio verrà reinserito. A meno che non si colleghi manualmente prima l'oggetto figlio.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Aggiornare

    Puoi semplicemente contrassegnare l'entità come modificata, quindi tutte le proprietà scalari verranno aggiornate e le proprietà di navigazione verranno semplicemente ignorate.

      db.Entity(entity).State = EntityState.Modified;

Grafico diff

Se vuoi semplificare il codice quando lavori con oggetti scollegati, puoi provare a rappresentare graficamente la libreria diff .

Ecco l'introduzione, Introduzione a GraphDiff per Entity Framework Code First - Consentire aggiornamenti automatici di un grafico di entità separate .

Codice di esempio

  • Inserisci entità se non esiste, altrimenti aggiorna.

      db.UpdateGraph(entity);
  • Inserisci entità se non esiste, altrimenti aggiorna E inserisci oggetto figlio se non esiste, altrimenti aggiorna.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));

3

Il modo migliore per farlo è sovrascrivere la funzione SaveChanges nel tuo contesto dati.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }

2

Ho lo stesso problema quando provo a salvare il profilo, ho già il saluto della tabella e nuovo per creare il profilo. Quando inserisco il profilo lo inserisco anche nel saluto. Quindi ho provato in questo modo prima di savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;


1

Questo ha funzionato per me:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;

0

Quello che abbiamo fatto è prima di aggiungere il genitore al dbset, disconnettere le raccolte figlie dal genitore, assicurandoci di spingere le raccolte esistenti su altre variabili per consentire di lavorarci successivamente, e quindi sostituire le raccolte figlie correnti con nuove raccolte vuote. L'impostazione delle raccolte figlio su null / nothing sembrava fallire per noi. Dopo averlo fatto, aggiungi il genitore al dbset. In questo modo i bambini non vengono aggiunti finché non lo desideri.


0

So che è un vecchio post, tuttavia se stai usando l'approccio code-first puoi ottenere il risultato desiderato usando il seguente codice nel tuo file di mappatura.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Questo fondamentalmente dirà a EF di escludere la proprietà "ChildObjectOrCollection" dal modello in modo che non venga mappata al database.


In quale contesto viene utilizzato "Ignora"? Non sembra esistere nel contesto presunto.
T3.0

0

Ho avuto una sfida simile utilizzando Entity Framework Core 3.1.0, la mia logica del repository è piuttosto generica.

Questo ha funzionato per me:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Tieni presente che "ParentEntityId" è il nome della colonna della chiave esterna nell'entità figlio. Ho aggiunto la riga di codice sopra menzionata su questo metodo:

protected override void OnModelCreating(ModelBuilder builder)...
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.