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

8

Recupero i dati del film da un'API esterna. In una prima fase analizzerò ogni film e lo inserirò nel mio database. In una seconda fase aggiornerò periodicamente il mio database utilizzando l'API "Changes" dell'API che posso interrogare per vedere quali filmati hanno cambiato le loro informazioni.

Il mio livello ORM è Entity-Framework. La classe Movie ha il seguente aspetto:

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 monitorare e quello nuovo che ricevo dall'API di aggiornamento chiamerà come oggetti diversi, ignorando .Equals() .

Questo causa un problema perché quando cerco di aggiornare il database con il film aggiornato, lo inserirà invece di aggiornare il film esistente.

Ho riscontrato questo problema prima con le lingue e la mia soluzione era quella di cercare gli oggetti lingua allegati, staccarli da il contesto, sposta il PK sull'oggetto aggiornato e lo collega al contesto. Quando SaveChanges() è ora eseguito, lo sostituirà essenzialmente.

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

C'è un modo per farlo in modo più elegante? Idealmente, voglio solo passare il film aggiornato al contesto e fare in modo che selezioni il film corretto da aggiornare in base al metodo Equals() , aggiorni tutti i suoi campi e per ogni oggetto complesso: usa di nuovo il record esistente in base alla sua% metodoEquals() e inserire se non esiste ancora.

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

    
posta Jeroen Vannevel 06.03.2015 - 17:25
fonte

2 risposte

6

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

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

Qualcosa di utile da notare: c'è un sovraccarico disponibile che ti permette di selezionare specificamente come deve essere determinata l'uguaglianza. Qui avrei potuto usare il mio TMDbId ma ho invece preferito semplicemente ignorare del tutto il mio ID e invece utilizzare un PK su TMDbId combinato con DatabaseGeneratedOption.None . Io uso questo approccio anche su ogni sottoraccolta, dove appropriato.

Parte interessante di l'origine :

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

che è il modo in cui i dati vengono effettivamente aggiornati sotto il cofano.

Tutto ciò che rimane chiama AddOrUpdate su ogni oggetto di cui 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 dal momento che devo specificare manualmente ogni parte del mio oggetto che deve essere aggiornato, ma è più vicino di quanto otterrà.

Lettura correlata: link

Aggiornamento:

Risulta che i miei test non sono stati abbastanza rigorosi. Dopo aver usato questa tecnica ho notato che mentre veniva aggiunta la nuova lingua, non era connessa al film. nella tabella molti a molti. Questo è un problema noto ma apparentemente a bassa priorità e non è stato corretto per quanto ne so.

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

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

È un sacco di lavoro manuale ed è brutto quindi passerà attraverso 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));

Si noti come riassegnare il valore recuperato all'oggetto root originale: ora è connesso a ciascun oggetto collegato. L'aggiornamento dell'oggetto root (il filmato) viene eseguito 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);
}
    
risposta data 11.03.2015 - 21:19
fonte
0

Poiché hai a che fare con diversi campi id e tmbid , 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 piuttosto che raccogliere le informazioni complete sull'oggetto specifico nella classe Movie.

    
risposta data 09.03.2015 - 14:05
fonte

Leggi altre domande sui tag