Posso atualizar um objeto anexado usando um objeto desanexado, mas igual?

10

Recupero dados do filme de uma API externa. Numa primeira fase, raspar cada filme e inseri-lo no meu próprio banco de dados. Em uma segunda fase, atualizarei periodicamente meu banco de dados usando a API "Alterações" da API, que posso consultar para ver quais filmes tiveram suas informações alteradas.

Minha camada ORM é Entity-Framework. A classe Movie é assim:

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

O problema surge quando eu tenho um filme que precisa ser atualizado: meu banco de dados pensa no objeto que está sendo rastreado e o novo que eu recebo da API de atualização chama como objetos diferentes, desconsiderando .Equals().

Isso causa um problema porque, quando agora tento atualizar o banco de dados com o filme atualizado, ele será inserido em vez de atualizar o filme existente.

Eu já tive esse problema com os idiomas e minha solução foi procurar os objetos de idioma anexados, desanexá-los do contexto, mover seu PK para o objeto atualizado e anexá-lo ao contexto. Quando SaveChanges()agora é executado, o substituirá essencialmente.

Essa é uma abordagem bastante fedorenta, porque, se eu continuar com essa abordagem Movie, significa que terei que desanexar o filme, os idiomas, os gêneros e as palavras-chave, procurar cada um no banco de dados, transferir seus IDs e inserir o código. novos objetos.

Existe uma maneira de fazer isso com mais elegância? O ideal é apenas passar o filme atualizado para o contexto e selecionar o filme correto a ser atualizado com base no Equals()método, atualizar todos os seus campos e para cada objeto complexo: use o registro existente novamente com base em seu próprio Equals()método e insira se ainda não existe.

Posso pular a desanexação / anexação fornecendo .Update()métodos para cada objeto complexo que posso usar em combinação para recuperar todos os objetos anexados, mas isso ainda exigirá que eu recupere cada objeto existente para atualizá-lo.

Jeroen Vannevel
fonte
Por que você não pode simplesmente atualizar a entidade rastreada a partir dos dados da API e salvar as alterações sem substituir / desanexar / corresponder / anexar entidades?
Si-N
@ Si-N: você pode expandir como exatamente isso seria então?
Jeroen Vannevel
Ok, agora você adicionou o último parágrafo, faz mais sentido, você está tentando evitar recuperar as entidades antes de atualizá-las? Não há nada que possa ser o seu PK na sua aula de cinema? Como você está associando os filmes da API externa às suas entidades? Você pode recuperar todas as entidades que precisa atualizar em uma chamada de banco de dados de qualquer maneira; essa não seria a solução mais simples que não causaria um grande impacto no desempenho (a menos que você esteja falando de um grande número de filmes para atualizar)?
Si-N
Minha classe filme tem um PK ide os filmes da API externa são compatíveis com os locais usando o campo tmdbid. Não consigo recuperar todas as entidades que precisam ser atualizadas em uma chamada, porque trata-se de filmes, gêneros, idiomas, palavras-chave etc. Cada uma delas possui uma PK e pode já existir no banco de dados.
Jeroen Vannevel

Respostas:

8

Não encontrei o que esperava, mas encontrei uma melhoria na sequência existente de seleção-desanexação-atualização-conexão.

O método de extensão AddOrUpdate(this DbSet)permite que você faça exatamente o que eu quero: insira se não estiver lá e atualize se encontrar um valor existente. Eu não percebi o uso mais cedo, pois realmente só vi ele ser usado no seed()método em combinação com Migrações. Se houver algum motivo para eu não usar isso, me avise.

Algo útil a ser observado: Há uma sobrecarga disponível que permite selecionar especificamente como a igualdade deve ser determinada. Aqui eu poderia ter usado o meu, TMDbIdmas optei por simplesmente desconsiderar completamente meu próprio ID e, em vez disso, usar um PK no TMDbId combinado com DatabaseGeneratedOption.None. Também uso essa abordagem em cada subcoleção, quando apropriado.

Parte interessante da fonte :

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

é assim que os dados são realmente atualizados.

Tudo o que resta é chamar AddOrUpdatecada objeto que eu quero ser afetado por isso:

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

Não é tão limpo quanto eu esperava, pois tenho que especificar manualmente cada parte do meu objeto que precisa ser atualizada, mas é o mais próximo possível.

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


Atualizar:

Acontece que meus testes não foram rigorosos o suficiente. Depois de usar essa técnica, notei que, embora o novo idioma tenha sido adicionado, ele não estava conectado ao filme. na tabela muitos para muitos. Esse é um problema conhecido, mas aparentemente de baixa prioridade e não foi corrigido até onde eu sei.

No final, decidi seguir a abordagem em que tenho Update(T)métodos para cada tipo e seguir esta sequência de eventos:

  • Fazer loop sobre coleções no novo objeto
  • Para cada entrada em cada coleção, procure-a no banco de dados
  • Se existir, use o Update()método para atualizá-lo com os novos valores
  • Se não existir, adicione-o ao DbSet apropriado
  • Retorne os objetos anexados e substitua as coleções no objeto raiz pelas coleções dos objetos anexados
  • Encontre e atualize o objeto raiz

É muito trabalho manual e é feio, portanto passará por mais algumas refatorações, mas agora meus testes indicam que ele deve funcionar para cenários mais rigorosos.


Depois de limpá-lo ainda mais, agora 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();
}

Isso me permite chamá-lo assim e inserir / atualizar as coleções subjacentes:

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

Observe como eu reatribui o valor recuperado ao objeto raiz original: agora ele está conectado a cada objeto anexado. A atualização do objeto raiz (o filme) é feita da mesma maneira:

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
fonte
Como você está lidando com exclusões no relacionamento 1-M? por exemplo, 1-Movie pode ter vários idiomas; se um dos idiomas for removido, seu código está removendo? Parece sua solução apenas inserções e ou atualizações (mas não remove?)
joedotnot
0

Como você está lidando com diferentes campos ide tmbid, sugiro que atualizar a API para fazer um índice único e separada de todas as informações, como gêneros, línguas, palavras-chave etc ... E, em seguida, fazer uma chamada para indexar e buscar por informações em vez de encontro toda a informação sobre um objeto específico na sua classe de filme.

Snazzy Sanoj
fonte
11
Eu não sigo a linha de pensamento aqui. Você pode expandir? Observe que a API externa está totalmente fora do meu controle.
Jeroen Vannevel