C # Entity-Framework: como combinar um .Find e .Include em um objeto de modelo?

145

Estou fazendo o tutorial de prática do mvcmusicstore. Notei algo ao criar o andaime para o gerenciador de álbuns (adicionar excluir editar).

Eu quero escrever código de forma elegante, então estou procurando a maneira limpa de escrever isso.

Para sua informação, estou tornando a loja mais genérica:

Álbuns = Itens

Gêneros = Categorias

Artista = Marca

Aqui está como o índice é recuperado (gerado pelo MVC):

var items = db.Items.Include(i => i.Category).Include(i => i.Brand);

Aqui está como o item para exclusão é recuperado:

Item item = db.Items.Find(id);

O primeiro traz de volta todos os itens e preenche os modelos de categoria e marca dentro do modelo de item. O segundo, não preenche a categoria e a marca.

Como posso escrever o segundo para encontrar e preencher o que está dentro (de preferência em 1 linha) ... teoricamente - algo como:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);
Ralph N
fonte
Se alguém necessidade de fazer isso genericamente in.net-core ver a minha resposta
johnny 5

Respostas:

162

Você precisa usar Include()primeiro e recuperar um único objeto da consulta resultante:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .SingleOrDefault(x => x.ItemId == id);
Dennis Traub
fonte
24
Eu realmente recomendo usar o último (SingleOrDefault), ToList irá recuperar todas as entradas em primeiro lugar e, em seguida, selecione uma
Sander Rijken
5
Isso ocorre se tivermos uma chave primária composta e estiver usando a sobrecarga de localização relevante.
precisa saber é o seguinte
78
Isso funcionaria, mas há uma diferença entre usar "Localizar" e "SingleOrDefault". O método "Find" retorna o objeto do armazenamento rastreado local, se houver, evitando uma ida e volta ao banco de dados, onde o uso de "SingleOrDefault" forçará uma consulta ao banco de dados de qualquer maneira.
Iravanchi
3
@Iravanchi está correto. Isso pode ter funcionado para o usuário, mas a operação e seus efeitos colaterais não são equivalentes a Find, tanto quanto eu sei.
Mwilson 15/07/2013
3
Na verdade não responder à pergunta ops, uma vez que não está usando .Find
Paul Swetz
73

A resposta de Dennis está usando Includee SingleOrDefault. O último vai de volta ao banco de dados.

Uma alternativa é usar Find, em combinação com Load, para carregamento explícito de entidades relacionadas ...

Abaixo um exemplo do MSDN :

using (var context = new BloggingContext()) 
{ 
  var post = context.Posts.Find(2); 

  // Load the blog related to a given post 
  context.Entry(post).Reference(p => p.Blog).Load(); 

  // Load the blog related to a given post using a string  
  context.Entry(post).Reference("Blog").Load(); 

  var blog = context.Blogs.Find(1); 

  // Load the posts related to a given blog 
  context.Entry(blog).Collection(p => p.Posts).Load(); 

  // Load the posts related to a given blog  
  // using a string to specify the relationship 
  context.Entry(blog).Collection("Posts").Load(); 
}

Obviamente, Findretorna imediatamente sem fazer uma solicitação para a loja, se essa entidade já estiver carregada pelo contexto.

Aprendiz
fonte
30
Esse método usa, Findportanto, se a entidade estiver presente, não haverá viagem de ida e volta ao DB para a própria entidade. MAS, você terá uma viagem de ida e volta para cada relacionamento que estiver estabelecendo Load, enquanto a SingleOrDefaultcombinação Includecarrega tudo de uma só vez.
Iravanchi 31/03
Quando comparei os 2 no criador de perfil SQL, o Find / Load foi melhor para o meu caso (eu tinha uma relação de 1: 1). @ Iravanchi: você quer dizer que se eu tivesse uma relação de 1: m teria chamado m vezes a loja? ... porque não faria tanto sentido.
Learner
3
Relação não 1: m, mas múltiplas relações. Cada vez que você chama a Loadfunção, a relação deve ser preenchida quando a chamada retornar. Portanto, se você ligar Loadvárias vezes para várias relações, haverá uma viagem de ida e volta a cada vez. Mesmo para uma única relação, se o Findmétodo não encontrar a entidade na memória, ele fará duas viagens de ida e volta: uma para Finde a segunda para Load. Mas o Include. SingleOrDefaultabordagem busca a entidade e relação de uma só vez, tanto quanto eu sei (mas não tenho certeza)
Iravanchi
1
Seria bom se pudéssemos seguir o design de inclusão de alguma forma, em vez de ter que tratar coleções e referências de maneira diferente. Isso dificulta a criação de uma fachada GetById () que aceita apenas uma coleção opcional de Expressão <Func <T, objeto >> (por exemplo, _repo.GetById (id, x => x.MyCollection))
Derek Greer
4
Lembre-se de mencionar a referência da sua postagem: msdn.microsoft.com/en-us/data/jj574232.aspx#explicit
Hossein
1

Você precisa converter IQueryable para DbSet

var dbSet = (DbSet<Item>) db.Set<Item>().Include("");

return dbSet.Find(id);

Rafael R. Souza
fonte
Não há .Find ou .FindAsync no dbSet. Esse é o EF Core?
Thierry
há ef 6 também no núcleo ef
Rafael R. Souza
Fiquei esperançoso e depois "InvalidCastException"
ZX9 7/11/19
0

Não funcionou para mim. Mas eu resolvi fazendo assim.

var item = db.Items
             .Include(i => i.Category)
             .Include(i => i.Brand)
             .Where(x => x.ItemId == id)
             .First();

Não sei se isso é uma solução ok. Mas o outro que Dennis me deu me deu um erro bool .SingleOrDefault(x => x.ItemId = id);

Johan
fonte
4
A solução de Dennis também deve funcionar. Talvez você tenha esse erro SingleOrDefault(x => x.ItemId = id)apenas por causa do single errado, em =vez de duplo ==?
Slauma
6
sim, parece que você usou = não ==. Erro de sintaxe;)
Ralph N
Eu tentei os dois == e = ainda me deu um erro no .SingleOrDefault (x => x.ItemId = id); = / Deve estar algo errado no meu código. Mas o jeito que eu fiz é ruim? Talvez eu não entenda o que você quer dizer com Dennis também.
Johan
0

Não existe uma maneira fácil de filtrar com uma descoberta. Mas eu vim com uma maneira próxima de replicar a funcionalidade, mas observe algumas coisas para a minha solução.

Esta solução permite que você filtre genericamente sem conhecer a chave primária no .net-core

  1. A localização é fundamentalmente diferente porque ela obtém a entidade se ela estiver presente no rastreamento antes de consultar o banco de dados.

  2. Além disso, ele pode filtrar por um objeto para que o usuário não precise conhecer a chave primária.

  3. Esta solução é para o EntityFramework Core.

  4. Isso requer acesso ao contexto

Aqui estão alguns métodos de extensão a serem adicionados que ajudarão você a filtrar por chave primária, para

    public static IReadOnlyList<IProperty> GetPrimaryKeyProperties<T>(this DbContext dbContext)
    {
        return dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties;
    }

    //TODO Precompile expression so this doesn't happen everytime
    public static Expression<Func<T, bool>> FilterByPrimaryKeyPredicate<T>(this DbContext dbContext, object[] id)
    {
        var keyProperties = dbContext.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var body = keyProperties
            // e => e.PK[i] == id[i]
            .Select((p, i) => Expression.Equal(
                Expression.Property(parameter, p.Name),
                Expression.Convert(
                    Expression.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"),
                    p.ClrType)))
            .Aggregate(Expression.AndAlso);
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }

    public static Expression<Func<T, object[]>> GetPrimaryKeyExpression<T>(this DbContext context)
    {
        var keyProperties = context.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var keyPropertyAccessExpression = keyProperties.Select((p, i) => Expression.Convert(Expression.Property(parameter, p.Name), typeof(object))).ToArray();
        var selectPrimaryKeyExpressionBody = Expression.NewArrayInit(typeof(object), keyPropertyAccessExpression);

        return Expression.Lambda<Func<T, object[]>>(selectPrimaryKeyExpressionBody, parameter);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this DbSet<TEntity> dbSet, DbContext context, object[] id)
        where TEntity : class
    {
        return FilterByPrimaryKey(dbSet.AsQueryable(), context, id);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this IQueryable<TEntity> queryable, DbContext context, object[] id)
        where TEntity : class
    {
        return queryable.Where(context.FilterByPrimaryKeyPredicate<TEntity>(id));
    }

Depois de ter esses métodos de extensão, você pode filtrar da seguinte maneira:

query.FilterByPrimaryKey(this._context, id);
johnny 5
fonte