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

8

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 de "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 cine 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 actualizarse: mi base de datos pensará que el objeto que se está rastreando y el nuevo que recibo de la llamada a la actualización de la API son objetos diferentes, sin tener en cuenta .Equals() .

Esto causa un problema porque cuando intento 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 idioma adjuntos, separarlos de el contexto, mueva su PK al objeto actualizado y adjúntelo al contexto. Cuando SaveChanges() ahora se ejecuta, esencialmente lo reemplazará.

Este es un enfoque bastante maloliente porque si continúo este enfoque hacia mi objeto Movie , 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, transfiere sus ID e inserta los nuevos objetos.

¿Hay una manera de hacerlo de manera más elegante? Idealmente, solo quiero pasar la película actualizada al contexto y hacer que seleccione la película correcta para actualizar en base al método Equals() , actualizar todos sus campos y para cada objeto complejo: usar el registro existente nuevamente en base a su propio% Método Equals() e inserte si aún no existe.

Puedo omitir el separar / adjuntar proporcionando los métodos .Update() 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.

    
pregunta Jeroen Vannevel 06.03.2015 - 17:25

2 respuestas

6

No encontré lo que esperaba, pero sí encontré una mejora con respecto a la secuencia existente de seleccionar-desconectar-actualizar-adjuntar.

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

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

Parte interesante de la fuente :

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

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

Todo lo que queda es llamar a AddOrUpdate en cada objeto al que quiero afectarme:

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

No está tan limpio como esperaba, ya que tengo que especificar manualmente cada parte de mi objeto que se debe actualizar, pero es lo más cercano posible.

Lectura relacionada: enlace

Actualización:

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

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

  • Bucle sobre colecciones en objeto nuevo
  • Para cada entrada en cada colección, búsquelo en la base de datos
  • Si existe, use el método Update() 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
  • Encuentre y actualice 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 en 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);
}
    
respondido por el Jeroen Vannevel 11.03.2015 - 21:19
0

Ya que estás tratando con diferentes campos id y tmbid , sugiero que actualices la API para crear un índice único e independiente de toda la información, como géneros, idiomas, palabras clave, etc ... Y luego hacer una llamada para indexar y verificar la información en lugar de recopilar toda la información sobre un objeto específico en su clase de película.

    
respondido por el Snazzy Sanoj 09.03.2015 - 14:05

Lea otras preguntas en las etiquetas