Posso aggiornare un oggetto collegato usando un oggetto distaccato ma uguale?


10

Recupero i dati del film da un'API esterna. In una prima fase, rascherò ogni film e lo inserirò nel mio database. In una seconda fase, aggiornerò periodicamente il mio database utilizzando l'API "Modifiche" dell'API alla quale posso interrogare per vedere quali film sono stati modificati.

Il mio livello ORM è Entity-Framework. La classe Movie si presenta così:

class Movie
{
    public virtual ICollection<Language> SpokenLanguages { get; set; }
    public virtual ICollection<Genre> Genres { get; set; }
    public virtual ICollection<Keyword> Keywords { get; set; }
}

Il problema sorge quando ho un film che deve essere aggiornato: il mio database penserà all'oggetto da tracciare e a quello nuovo che ricevo dalla chiamata dell'API di aggiornamento come oggetti diversi, ignorando .Equals().

Ciò causa un problema perché quando provo ora ad aggiornare il database con il filmato aggiornato, lo inserirò invece di aggiornare il film esistente.

Avevo già avuto questo problema con le lingue e la mia soluzione era quella di cercare gli oggetti linguaggio collegati, staccarli dal contesto, spostare il loro PK sull'oggetto aggiornato e collegarlo al contesto. Quando SaveChanges()viene eseguito, lo sostituirà essenzialmente.

Questo è un approccio piuttosto puzzolente perché se continuo questo approccio al mio Movieoggetto, significa che dovrò staccare il film, le lingue, i generi e le parole chiave, cercare ognuno nel database, trasferire i loro ID e inserire il nuovi oggetti.

C'è un modo per farlo in modo più elegante? Idealmente, voglio solo passare il filmato aggiornato al contesto e fare in modo che selezioni il filmato corretto da aggiornare in base al Equals()metodo, aggiornare tutti i suoi campi e per ogni oggetto complesso: utilizzare nuovamente il record esistente basato sul proprio Equals()metodo e inserire se non esiste ancora.

Posso saltare il distacco / collegamento fornendo .Update()metodi su ogni oggetto complesso che posso usare in combinazione con il recupero di tutti gli oggetti collegati, ma ciò richiederà comunque che io recuperi ogni singolo oggetto esistente per poi aggiornarlo.


Perché non puoi semplicemente aggiornare l'entità tracciata dai dati API e salvare le modifiche senza sostituire / staccare / abbinare / allegare entità?
Si-N

@ Si-N: puoi espandere come andrebbe esattamente allora?
Jeroen Vannevel,

Ok ora hai aggiunto che l'ultimo paragrafo ha più senso, stai cercando di evitare di recuperare le entità prima di aggiornarle? Non c'è niente che possa essere il tuo PK nella tua classe di film? Come stai abbinando i film dall'API esterna alle tue entità? Puoi comunque recuperare tutte le entità che devi aggiornare in una chiamata db, non sarebbe questa la soluzione più semplice che non dovrebbe causare un grande calo delle prestazioni (a meno che non parli di un numero enorme di film da aggiornare)?
Si-N

La mia classe Movie ha un PK ide i film dell'API esterna sono abbinati a quelli locali usando il campo tmdbid. Non riesco a recuperare tutte le entità che devono essere aggiornate in una chiamata perché si tratta di film, generi, lingue, parole chiave, ecc. Ognuno di questi ha un PK e potrebbe già esistere nel database.
Jeroen Vannevel

Risposte:


8

Non ho trovato quello che speravo, ma ho trovato un miglioramento rispetto all'esistente sequenza select-detach-update-attach.

Il metodo di estensione AddOrUpdate(this DbSet)ti consente di fare esattamente quello che voglio fare: inserisci se non è presente e aggiorna se trova un valore esistente. Non mi ero reso conto di usarlo prima poiché l'ho visto solo usato nel seed()metodo in combinazione con Migrations. Se c'è qualche motivo per cui non dovrei usarlo, fammelo sapere.

Qualcosa di utile da notare: è disponibile un sovraccarico che consente di selezionare in modo specifico come deve essere determinata l'uguaglianza. Qui avrei potuto usare il mio TMDbIdma invece ho optato per ignorare del tutto il mio ID e usare invece un PK su TMDbId combinato con DatabaseGeneratedOption.None. Uso questo approccio anche su ogni sottocollection, se del caso.

Parte interessante della fonte :

internalSet.InternalContext.Owner.Entry(existing).CurrentValues.SetValues(entity);

che è come i dati vengono effettivamente aggiornati sotto il cofano.

Tutto ciò che rimane è chiamare AddOrUpdateogni oggetto che voglio essere interessato da questo:

public void InsertOrUpdate(Movie movie)
{
    _context.Movies.AddOrUpdate(movie);
    _context.Languages.AddOrUpdate(movie.SpokenLanguages.ToArray());
    // Other objects/collections
    _context.SaveChanges();
}

Non è pulito come speravo poiché devo specificare manualmente ogni pezzo del mio oggetto che deve essere aggiornato, ma è il più vicino possibile.

Lettura correlata: /programming/15336248/entity-framework-5-updating-a-record


Aggiornare:

Si scopre che i miei test non erano abbastanza rigorosi. Dopo aver usato questa tecnica ho notato che mentre la nuova lingua veniva aggiunta, non era collegata al film. nella tabella molti-a-molti. Questo è un problema noto ma apparentemente di bassa priorità e non è stato risolto per quanto ne so.

Alla fine ho deciso di optare per l'approccio in cui ho Update(T)metodi su ogni tipo e seguire questa sequenza di eventi:

  • Scorri le raccolte in un nuovo oggetto
  • Per ogni voce in ogni raccolta, cercala nel database
  • Se esiste, utilizzare il Update()metodo per aggiornarlo con i nuovi valori
  • Se non esiste, aggiungilo al DbSet appropriato
  • Restituisce gli oggetti collegati e sostituisce le raccolte nell'oggetto radice con le raccolte degli oggetti collegati
  • Trova e aggiorna l'oggetto root

È un sacco di lavoro manuale ed è brutto, quindi passerà attraverso alcuni altri refactoring, ma ora i miei test indicano che dovrebbe funzionare per scenari più rigorosi.


Dopo averlo pulito ulteriormente, ora uso questo metodo:

private IEnumerable<T> InsertOrUpdate<T, TKey>(IEnumerable<T> entities, Func<T, TKey> idExpression) where T : class
{
    foreach (var entity in entities)
    {
        var existingEntity = _context.Set<T>().Find(idExpression(entity));
        if (existingEntity != null)
        {
            _context.Entry(existingEntity).CurrentValues.SetValues(entity);
            yield return existingEntity;
        }
        else
        {
            _context.Set<T>().Add(entity);
            yield return entity;
        }
    }
    _context.SaveChanges();
}

Questo mi permette di chiamarlo così e inserire / aggiornare le raccolte sottostanti:

movie.Genres = new List<Genre>(InsertOrUpdate(movie.Genres, x => x.TmdbId));

Nota come riassegno il valore recuperato all'oggetto radice originale: ora è collegato a ciascun oggetto collegato. L'aggiornamento dell'oggetto root (il filmato) avviene allo stesso modo:

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}

Come gestisci le eliminazioni nella relazione 1-M? ad esempio, 1-Movie può avere più lingue; se una delle lingue viene rimossa, il tuo codice la rimuove? Sembra che la tua soluzione inserisca e / o aggiorni solo (ma non rimuova?)
joedotnot

0

Dato che hai a che fare con diversi campi ide tmbid, ti suggerisco di aggiornare l'API per creare un indice singolo e separato di tutte le informazioni come generi, lingue, parole chiave ecc ... E quindi effettuare una chiamata per indicizzare e controllare le informazioni anziché raccogliere tutte le informazioni su un oggetto specifico nella tua classe Movie.


1
Non seguo il treno del pensiero qui. Puoi espandermi? Nota che l'API esterna è completamente fuori dal mio controllo.
Jeroen Vannevel,
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.