¿Puedo actualizar un objeto adjunto usando un objeto separado pero igual?

10

Recupero datos de películas de una API externa. En una primera fase, rasparé cada película y la insertaré en mi propia base de datos. En una segunda fase, actualizaré periódicamente mi base de datos utilizando la API "Cambios" de la API, que puedo consultar para ver qué películas han cambiado su información.

Mi capa ORM es Entity-Framework. La clase de película se ve así:

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

El problema surge cuando tengo una película que necesita ser actualizada: mi base de datos pensará en el objeto que se está rastreando y en el nuevo que recibo de la llamada API de actualización como objetos diferentes, sin tener en cuenta .Equals().

Esto causa un problema porque cuando ahora trato de actualizar la base de datos con la película actualizada, la insertará en lugar de actualizar la película existente.

Tuve este problema antes con los idiomas y mi solución fue buscar los objetos de lenguaje adjuntos, separarlos del contexto, mover su PK al objeto actualizado y adjuntarlo al contexto. Cuando SaveChanges()ahora se ejecuta, esencialmente lo reemplazará.

Este es un enfoque bastante maloliente porque si continúo este enfoque hacia mi Movieobjeto, significa que tendré que separar la película, los idiomas, los géneros y las palabras clave, buscar cada uno en la base de datos, transferir sus ID e insertar el nuevos objetos

¿Hay alguna manera de hacer esto con más elegancia? Idealmente, solo quiero pasar la película actualizada al contexto y hacer que seleccione la película correcta para actualizar según el Equals()método, actualice todos sus campos y para cada objeto complejo: use el registro existente nuevamente según su propio Equals()método e inserte si aún no existe

Puedo omitir la separación / fijación proporcionando .Update()métodos en cada objeto complejo que puedo usar en combinación para recuperar todos los objetos adjuntos, pero esto aún requerirá que recupere cada objeto existente para luego actualizarlo.

Jeroen Vannevel
fuente
¿Por qué no puede simplemente actualizar la entidad rastreada a partir de los datos de su API y guardar los cambios sin reemplazar / separar / emparejar / adjuntar entidades?
Si-N
@ Si-N: ¿puedes ampliar cómo sería exactamente eso entonces?
Jeroen Vannevel
Ok, ahora que ha agregado el último párrafo tiene más sentido, ¿está tratando de evitar recuperar las entidades antes de actualizarlas? ¿No hay nada que pueda ser tu PK en tu clase de película? ¿Cómo se relacionan las películas de la API externa con sus entidades? De todos modos, puede recuperar todas las entidades que necesita actualizar en una llamada db, ¿no sería esta la solución más simple que no debería causar un gran impacto en el rendimiento (a menos que esté hablando de una gran cantidad de películas para actualizar)?
Si-N
Mi clase de película tiene un PK idy las películas de la API externa coinciden con las locales que usan el campo tmdbid. No puedo recuperar todas las entidades que deben actualizarse en una sola llamada porque se trata de películas, géneros, idiomas, palabras clave, etc. Cada una de estas tiene un PK y puede que ya exista en la base de datos.
Jeroen Vannevel

Respuestas:

8

No encontré lo que esperaba, pero sí encontré una mejora con respecto a la secuencia existente select-detach-update-attach.

El método de extensión le AddOrUpdate(this DbSet)permite hacer exactamente lo que quiero hacer: insertar si no está allí y actualizar si encuentra un valor existente. No me di cuenta de usar esto antes, ya que realmente solo he visto que se use en el seed()método en combinación con Migraciones. Si hay alguna razón por la que no debería usar esto, avíseme.

Algo útil a tener en cuenta: hay una sobrecarga disponible que le permite seleccionar específicamente cómo se debe determinar la igualdad. Aquí podría haber usado mi TMDbIdpero en lugar de eso opté por simplemente ignorar mi propia ID por completo y usar una PK en TMDbId combinada con DatabaseGeneratedOption.None. También uso este enfoque en cada subcolección, cuando corresponde.

Parte interesante de la fuente :

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

que es cómo se actualizan los datos realmente bajo el capó.

Todo lo que queda es llamar AddOrUpdatea cada objeto que quiero que se vea afectado por esto:

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

No es tan limpio como esperaba, ya que tengo que especificar manualmente cada pieza de mi objeto que necesita actualizarse, pero está lo más cerca posible.

Lectura relacionada: /programming/15336248/entity-framework-5-updating-a-record


Actualizar:

Resulta que mis pruebas no fueron lo suficientemente rigurosas. Después de usar esta técnica, noté que si bien se agregó el nuevo idioma, no estaba conectado a la película. en la tabla de muchos a muchos. Este es un problema conocido pero aparentemente de baja prioridad y, hasta donde sé, no se ha solucionado.

Al final decidí ir al enfoque donde tengo Update(T)métodos en cada tipo y seguir esta secuencia de eventos:

  • Recorrer colecciones en un nuevo objeto
  • Para cada entrada en cada colección, búsquela en la base de datos
  • Si existe, utilice el Update()método para actualizarlo con los nuevos valores.
  • Si no existe, agréguelo al DbSet apropiado
  • Devuelva los objetos adjuntos y reemplace las colecciones en el objeto raíz con las colecciones de los objetos adjuntos.
  • Encuentra y actualiza el objeto raíz

Es mucho trabajo manual y es feo, por lo que pasará por algunas refactorizaciones más, pero ahora mis pruebas indican que debería funcionar para escenarios más rigurosos.


Después de limpiarlo más, ahora uso este método:

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

Esto me permite llamarlo así e insertar / actualizar las colecciones subyacentes:

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

Observe cómo reasigno el valor recuperado al objeto raíz original: ahora está conectado a cada objeto adjunto. La actualización del objeto raíz (la película) se realiza de la misma manera:

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}
Jeroen Vannevel
fuente
¿Cómo maneja las eliminaciones en la relación 1-M? por ejemplo, 1-Movie puede tener varios idiomas; si se elimina uno de los idiomas, ¿lo está eliminando su código? Al parecer, su solución sólo inserciones y actualizaciones o (pero no elimina?)
joedotnot
0

Dado que se trata de diferentes campos idy tmbid, sugiero que actualice la API para hacer un índice único e independiente de toda la información, como géneros, idiomas, palabras clave, etc. Y luego haga una llamada para indexar y verificar la información en lugar de recopilar toda la información sobre un objeto específico en tu clase de película.

Snazzy Sanoj
fuente
1
No sigo el tren del pensamiento aquí. Se puede ampliar? Tenga en cuenta que la API externa está completamente fuera de mi control.
Jeroen Vannevel