Aggiorna le relazioni durante il salvataggio delle modifiche degli oggetti EF4 POCO


107

Entity Framework 4, oggetti POCO e ASP.Net MVC2. Ho una relazione molti a molti, diciamo tra BlogPost e le entità Tag. Ciò significa che nella mia classe POCO BlogPost generata da T4 ho:

public virtual ICollection<Tag> Tags {
    // getter and setter with the magic FixupCollection
}
private ICollection<Tag> _tags;

Chiedo un BlogPost e i relativi tag da un'istanza di ObjectContext e lo invio a un altro livello (Visualizza nell'applicazione MVC). Successivamente ricevo il BlogPost aggiornato con proprietà modificate e relazioni modificate. Ad esempio, aveva i tag "A" "B" e "C" e i nuovi tag sono "C" e "D". Nel mio esempio particolare non ci sono nuovi tag e le proprietà dei tag non cambiano mai, quindi l'unica cosa che dovrebbe essere salvata sono le relazioni modificate. Ora ho bisogno di salvarlo in un altro ObjectContext. (Aggiornamento: ora ho provato a fare nella stessa istanza di contesto e anche non è riuscito.)

Il problema: non riesco a salvare le relazioni correttamente. Ho provato tutto quello che ho trovato:

  • Controller.UpdateModel e Controller.TryUpdateModel non funzionano.
  • Ottenere il vecchio BlogPost dal contesto e quindi modificare la raccolta non funziona. (con metodi diversi dal punto successivo)
  • Questo probabilmente avrebbe funzionato, ma spero che questo è solo una soluzione, non la soluzione :(.
  • Ho provato le funzioni Allega / Aggiungi / Modifica Stato oggetto per BlogPost e / o Tag in tutte le possibili combinazioni. Impossibile.
  • Questo assomiglia a quello che mi serve, ma non funziona (ho cercato di risolvere il problema, ma non può per il mio problema).
  • Ho provato ChangeState / Add / Attach / ... gli oggetti di relazione del contesto. Impossibile.

"Non funziona" significa nella maggior parte dei casi che ho lavorato sulla "soluzione" data fino a quando non produce errori e salva almeno le proprietà di BlogPost. Quello che succede con le relazioni varia: normalmente i Tag vengono aggiunti di nuovo alla tabella Tag con nuovi PK e il BlogPost salvato fa riferimento a quelli e non a quelli originali. Ovviamente i tag restituiti hanno PK, e prima dei metodi di salvataggio / aggiornamento controllo i PK e sono uguali a quelli nel database, quindi probabilmente EF pensa che siano nuovi oggetti e quei PK sono quelli temporanei.

Un problema che conosco e che potrebbe rendere impossibile trovare una soluzione semplice automatizzata: quando la raccolta di un oggetto POCO viene modificata, ciò dovrebbe accadere per la proprietà della raccolta virtuale sopra menzionata, perché quindi il trucco FixupCollection aggiornerà i riferimenti inversi dall'altra parte della relazione molti-a-molti. Tuttavia, quando una vista "restituisce" un oggetto BlogPost aggiornato, ciò non è accaduto. Ciò significa che forse non esiste una soluzione semplice al mio problema, ma ciò mi renderebbe molto triste e odierei il trionfo di EF4-POCO-MVC :(. Inoltre ciò significherebbe che EF non può farlo nell'ambiente MVC a seconda di quale Vengono utilizzati i tipi di oggetto EF4 :(. Penso che il rilevamento delle modifiche basato su snapshot dovrebbe scoprire che il BlogPost modificato ha relazioni con i tag con PK esistenti.

A proposito: penso che lo stesso problema si verifichi con le relazioni uno-a-molti (Google e il mio collega lo dicono). Lo proverò a casa, ma anche se funziona, non mi aiuta nelle mie sei relazioni molti-a-molti nella mia app :(.


Inserisci il tuo codice. Questo è uno scenario comune.
John Farrell

1
Ho una soluzione automatica a questo problema, è nascosta nelle risposte di seguito, così tanti la mancherebbero, ma per favore dai un'occhiata perché ti farà risparmiare un lavoro infernale vedi il post qui
brentmckendrick

@brentmckendrick Penso che un altro approccio sia migliore. Invece di inviare l'intero oggetto grafico modificato in rete, perché non inviare semplicemente il delta? Non avresti nemmeno bisogno di classi DTO generate in quel caso. Se hai un'opinione su questo in entrambi i casi, discutiamone su stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Risposte:


145

Proviamolo in questo modo:

  • Allega BlogPost al contesto. Dopo aver collegato l'oggetto al contesto, lo stato dell'oggetto, tutti gli oggetti correlati e tutte le relazioni vengono impostati su Invariato.
  • Utilizza context.ObjectStateManager.ChangeObjectState per impostare il tuo BlogPost su Modified
  • Scorri la raccolta di tag
  • Utilizza context.ObjectStateManager.ChangeRelationshipState per impostare lo stato per la relazione tra Tag corrente e BlogPost.
  • Salvare le modifiche

Modificare:

Immagino che uno dei miei commenti ti abbia dato la falsa speranza che EF farà l'unione per te. Ho giocato molto con questo problema e la mia conclusione dice che EF non lo farà per te. Penso che tu abbia trovato la mia domanda anche su MSDN . In realtà ci sono molte domande del genere su Internet. Il problema è che non è indicato chiaramente come affrontare questo scenario. Quindi diamo uno sguardo al problema:

Sfondo del problema

EF deve tenere traccia delle modifiche sulle entità in modo che la persistenza sappia quali record devono essere aggiornati, inseriti o eliminati. Il problema è che è responsabilità di ObjectContext tenere traccia delle modifiche. ObjectContext è in grado di tenere traccia delle modifiche solo per le entità associate. Le entità che vengono create all'esterno di ObjectContext non vengono tracciate affatto.

Descrizione del problema

In base alla descrizione precedente, possiamo chiaramente affermare che EF è più adatto per scenari connessi in cui l'entità è sempre collegata al contesto, tipico dell'applicazione WinForm. Le applicazioni Web richiedono uno scenario disconnesso in cui il contesto viene chiuso dopo l'elaborazione della richiesta e il contenuto dell'entità viene passato come risposta HTTP al client. La successiva richiesta HTTP fornisce il contenuto modificato dell'entità che deve essere ricreata, allegata al nuovo contesto e mantenuta. La ricreazione di solito avviene al di fuori dell'ambito del contesto (architettura a strati con ignoranza persistente).

Soluzione

Allora come affrontare uno scenario così disconnesso? Quando si utilizzano le classi POCO, abbiamo 3 modi per gestire il rilevamento delle modifiche:

  • Snapshot: richiede lo stesso contesto = inutile per lo scenario disconnesso
  • Proxy di tracciamento dinamico: richiede lo stesso contesto = inutile per lo scenario disconnesso
  • Sincronizzazione manuale.

La sincronizzazione manuale su una singola entità è un compito facile. È sufficiente collegare l'entità e chiamare AddObject per l'inserimento, DeleteObject per l'eliminazione o impostare lo stato in ObjectStateManager su Modified per l'aggiornamento. Il vero dolore arriva quando hai a che fare con il grafico a oggetti invece che con una singola entità. Questo dolore è ancora peggiore quando si ha a che fare con associazioni indipendenti (quelle che non utilizzano proprietà di chiave estera) e relazioni molti a molti. In tal caso è necessario sincronizzare manualmente ogni entità nell'oggetto grafico ma anche ogni relazione nell'oggetto grafico.

La sincronizzazione manuale viene proposta come soluzione dalla documentazione MSDN: Collegamento e scollegamento di oggetti dice:

Gli oggetti vengono associati al contesto dell'oggetto in uno stato invariato. Se è necessario modificare lo stato di un oggetto o la relazione perché si sa che l'oggetto è stato modificato nello stato scollegato, utilizzare uno dei seguenti metodi.

I metodi menzionati sono ChangeObjectState e ChangeRelationshipState di ObjectStateManager = rilevamento manuale delle modifiche. Una proposta simile si trova in un altro articolo della documentazione MSDN: Definizione e gestione delle relazioni dice:

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

Inoltre c'è un post sul blog relativo a EF v1 che critica esattamente questo comportamento di EF.

Motivo della soluzione

EF ha molte operazioni e impostazioni "utili" come Aggiorna , Carica , ApplyCurrentValues , ApplyOriginalValues , MergeOption ecc. Ma secondo la mia indagine, tutte queste funzionalità funzionano solo per una singola entità e interessano solo le proprietà scalari (= non proprietà e relazioni di navigazione). Preferisco non testare questi metodi con tipi complessi annidati in entità.

Altra soluzione proposta

Invece della vera funzionalità di unione, il team EF fornisce qualcosa chiamato Self Tracking Entities (STE) che non risolve il problema. Prima di tutto STE funziona solo se la stessa istanza viene utilizzata per l'intera elaborazione. Nell'applicazione web non è il caso a meno che non si memorizzi l'istanza nello stato di visualizzazione o nella sessione. Per questo motivo non sono molto contento di utilizzare EF e controllerò le funzionalità di NHibernate. La prima osservazione dice che NHibernate forse ha tale funzionalità .

Conclusione

Terminerò questa ipotesi con un collegamento singolo a un'altra domanda correlata sul forum MSDN. Controlla la risposta di Zeeshan Hirani. È autore di Entity Framework 4.0 Recipes . Se dice che l'unione automatica degli oggetti grafici non è supportata, gli credo.

Ma c'è ancora la possibilità che mi sbagli completamente e alcune funzionalità di unione automatica esistono in EF.

Modifica 2:

Come puoi vedere, questo è stato già aggiunto a MS Connect come suggerimento nel 2007. MS lo ha chiuso come qualcosa da fare nella prossima versione, ma in realtà non è stato fatto nulla per migliorare questo divario tranne STE.


7
Questa è una delle migliori risposte che ho letto su SO. Hai dichiarato chiaramente cosa tanti articoli, documentazione e post di blog MSDN sull'argomento non sono riusciti a trasmettere. EF4 non supporta intrinsecamente l'aggiornamento delle relazioni da entità "scollegate". Fornisce solo strumenti per implementarlo da solo. Grazie!
tyriker

1
Quindi, dopo pochi mesi, che ne dici di NHibernate relativo a questo problema rispetto a EF4?
CallMeLaNN

1
Questo è molto ben supportato in NHibernate :-) non c'è bisogno di unire manualmente, nel mio esempio è un grafo di oggetti profondo a 3 livelli, la domanda ha risposte, ogni risposta ha commenti e anche la domanda ha commenti. NHibernate può persistere / unire il tuo oggetto grafico, non importa quanto sia complesso ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Un altro utente NHibernate soddisfatto: codinginstinct.com/2009/11/…
Michael Buen

2
Una delle migliori spiegazioni che io abbia mai letto !! Grazie mille
marvelTracker

2
Il team EF prevede di affrontare questo problema post-EF6. Potresti votare per entityframework.codeplex.com/workitem/864
Eric J.

19

Ho una soluzione al problema descritto sopra da Ladislav. Ho creato un metodo di estensione per DbContext che eseguirà automaticamente l'aggiunta / aggiornamento / eliminazione in base a una differenza del grafico fornito e del grafico persistente.

Al momento utilizzando Entity Framework sarà necessario eseguire manualmente gli aggiornamenti dei contatti, controllare se ogni contatto è nuovo e aggiungerlo, controllare se aggiornato e modificare, controllare se rimosso quindi cancellarlo dal database. Una volta che devi fare questo per alcuni aggregati diversi in un grande sistema, inizi a capire che deve esserci un modo migliore e più generico.

Dai un'occhiata e vedi se può aiutare http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- grafico-di--entità indipendenti /

Puoi andare direttamente al codice qui https://github.com/refactorthis/GraphDiff


Sono sicuro che puoi risolvere facilmente questa domanda, sto passando un brutto momento.
Shimmy Weitzhandler

1
Ciao Shimmy, scusa finalmente ho avuto un po 'di tempo per dare un'occhiata. Lo esaminerò stasera.
brentmckendrick

Questa libreria è fantastica e mi ha fatto risparmiare così tanto tempo! Grazie!
lordjeb

9

So che è tardi per l'OP, ma poiché questo è un problema molto comune, l'ho pubblicato nel caso in cui serva qualcun altro. Sto giocando con questo problema e penso di aver ottenuto una soluzione abbastanza semplice, quello che faccio è:

  1. Salva l'oggetto principale (ad esempio Blog) impostando il suo stato su Modificato.
  2. Interroga il database per l'oggetto aggiornato, comprese le raccolte che devo aggiornare.
  3. Interroga e converti .ToList () le entità che voglio che la mia raccolta includa.
  4. Aggiorna le raccolte dell'oggetto principale all'elenco ottenuto dal passaggio 3.
  5. Salvare le modifiche();

Nel seguente esempio "dataobj" e "_categories" sono i parametri ricevuti dal mio controller "dataobj" è il mio oggetto principale e "_categories" è un IEnumerable contenente gli ID delle categorie selezionate dall'utente nella vista.

    db.Entry(dataobj).State = EntityState.Modified;
    db.SaveChanges();
    dataobj = db.ServiceTypes.Include(x => x.Categories).Single(x => x.Id == dataobj.Id);
    var it = _categories != null ? db.Categories.Where(x => _categories.Contains(x.Id)).ToList() : null;
    dataobj.Categories = it;
    db.SaveChanges();

Funziona anche per più relazioni


7

Il team di Entity Framework è consapevole del fatto che si tratta di un problema di usabilità e prevede di risolverlo dopo EF6.

Dal team di Entity Framework:

Questo è un problema di usabilità di cui siamo consapevoli ed è qualcosa a cui abbiamo pensato e abbiamo in programma di lavorare di più sul post-EF6. Ho creato questo elemento di lavoro per tenere traccia del problema: http://entityframework.codeplex.com/workitem/864 L'elemento di lavoro contiene anche un collegamento all'elemento vocale dell'utente per questo - ti incoraggio a votarlo se lo hai non già fatto.

Se questo ti colpisce, vota per la funzione su

http://entityframework.codeplex.com/workitem/864


post-EF6? che anno sarà allora nel caso ottimistico?
quetzalcoatl

@quetzalcoatl: Almeno è sul loro radar :-) EF ha fatto molta strada da EF 1 ma ha ancora molta strada da fare.
Eric J.

1

Tutte le risposte sono state ottime per spiegare il problema, ma nessuna ha risolto il problema per me.

Ho scoperto che se non ho utilizzato la relazione nell'entità padre ma ho appena aggiunto e rimosso le entità figlio, tutto funzionava perfettamente.

Ci scusiamo per il VB ma questo è ciò in cui è scritto il progetto a cui sto lavorando.

L'entità padre "Report" ha una relazione uno a molti con "ReportRole" e ha la proprietà "ReportRoles". I nuovi ruoli vengono passati da una stringa separata da virgole da una chiamata Ajax.

La prima riga rimuoverà tutte le entità figlio e se usassi "report.ReportRoles.Remove (f)" invece di "db.ReportRoles.Remove (f)" otterrei l'errore.

report.ReportRoles.ToList.ForEach(Function(f) db.ReportRoles.Remove(f))
Dim newRoles = If(String.IsNullOrEmpty(model.RolesString), New String() {}, model.RolesString.Split(","))
newRoles.ToList.ForEach(Function(f) db.ReportRoles.Add(New ReportRole With {.ReportId = report.Id, .AspNetRoleId = f}))
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.