C # Entity-Framework: ¿Cómo puedo combinar .Find e .Include en un objeto modelo?

145

Estoy haciendo el tutorial de práctica mvcmusicstore. Noté algo al crear el andamio para el administrador de álbumes (agregar, eliminar, editar).

Quiero escribir código con elegancia, así que estoy buscando la forma limpia de escribir esto.

FYI estoy haciendo la tienda más genérica:

Álbumes = Artículos

Géneros = Categorías

Artista = Marca

Así es como se recupera el índice (generado por MVC):

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

Así es como se recupera el elemento para eliminar:

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

El primero recupera todos los artículos y completa la categoría y los modelos de marca dentro del modelo de artículo. El segundo, no llena la categoría y la marca.

¿Cómo puedo escribir el segundo para hacer la búsqueda Y llenar lo que hay dentro (preferiblemente en 1 línea) ... en teoría, algo como:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);
Ralph N
fuente
Si alguien necesita hacer esto genéricamente en.net-core, vea mi respuesta
johnny 5

Respuestas:

162

Include()Primero debe usar , luego recuperar un solo objeto de la consulta resultante:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .SingleOrDefault(x => x.ItemId == id);
Dennis Traub
fuente
24
Realmente recomendaría usar este último (SingleOrDefault), ToList recuperará todas las entradas primero y luego seleccionará una
Sander Rijken
55
Esto se descompone si tenemos una clave primaria compuesta y estamos usando la sobrecarga de búsqueda relevante.
jhappoldt
78
Esto funcionaría, pero hay una diferencia entre usar "Buscar" y "SingleOrDefault". El método "Buscar" devuelve el objeto de la tienda local rastreada si existe, evitando un viaje de ida y vuelta a la base de datos, donde el uso de "SingleOrDefault" forzará una consulta a la base de datos de todos modos.
Iravanchi
3
@Iravanchi es correcto. Esto puede haber funcionado para el usuario, pero la operación y sus efectos secundarios no son equivalentes a Buscar, que yo sepa.
mwilson
3
En realidad no responde a la pregunta de operaciones, ya que no está utilizando. Encontrar
Paul Swetz
73

La respuesta de Dennis es usar Includey SingleOrDefault. Este último va de ida y vuelta a la base de datos.

Una alternativa es usar Find, en combinación con Load, para la carga explícita de entidades relacionadas ...

Debajo de un ejemplo de 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(); 
}

Por supuesto, Findregresa inmediatamente sin hacer una solicitud a la tienda, si esa entidad ya está cargada por el contexto.

Aprendiz
fuente
30
Este método utiliza, Findpor lo tanto, si la entidad está presente, no hay ida y vuelta a la base de datos para la entidad misma. PERO, tendrás un viaje de ida y vuelta para cada relación que estés teniendo Load, mientras que la SingleOrDefaultcombinación con Includecarga todo de una vez.
Iravanchi
Cuando comparé el 2 en el generador de perfiles SQL, Find / Load fue mejor para mi caso (tenía una relación 1: 1). @Iravanchi: ¿quieres decir que si tuviera una relación 1: m hubiera llamado m veces la tienda? ... porque no tendría mucho sentido.
Estudiante
3
No relación 1: m, sino relaciones múltiples. Cada vez que llama a la Loadfunción, la relación debe rellenarse cuando vuelve la llamada. Entonces, si llama Loadvarias veces para relaciones múltiples, habrá un viaje de ida y vuelta cada vez. Incluso para una sola relación, si el Findmétodo no encuentra la entidad en la memoria, realiza dos viajes de ida y vuelta: uno para Findy el segundo para Load. Pero el Include. SingleOrDefaultenfoque alcanza la entidad y la relación de una vez hasta donde yo sé (pero no estoy seguro)
Iravanchi
1
Hubiera sido agradable si hubiera podido seguir el diseño Incluir de alguna manera en lugar de tener que tratar las colecciones y referencias de manera diferente. Eso hace que sea más difícil crear una fachada GetById () que solo tome una colección opcional de Expression <Func <T, object >> (por ejemplo, _repo.GetById (id, x => x.MyCollection))
Derek Greer
44
Recuerde mencionar la referencia de su publicación: msdn.microsoft.com/en-us/data/jj574232.aspx#explicit
Hossein
1

Tienes que lanzar IQueryable a DbSet

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

return dbSet.Find(id);

Rafael R. Souza
fuente
No hay .Find o .FindAsync en dbSet. ¿Es este EF Core?
Thierry
hay ef 6 también en ef core
Rafael R. Souza
Tenía esperanzas y luego "InvalidCastException"
ZX9
0

No funcionó para mí. Pero lo resolví haciendo así.

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

No sé si esa es una buena solución. Pero el otro que Dennis me dio me dio un error de bool en .SingleOrDefault(x => x.ItemId = id);

Johan
fuente
44
La solución de Dennis también debe funcionar. ¿Quizás tenga este error SingleOrDefault(x => x.ItemId = id)solo por el single incorrecto en =lugar del doble ==?
Slauma
66
sí, parece que usaste = no ==. Error de sintaxis;)
Ralph N
Los probé ambos == y = todavía me dio un error en .SingleOrDefault (x => x.ItemId = id); = / Debe haber algo más en mi código que esté mal. ¿Pero la forma en que lo hice es mala? Tal vez no entiendo a qué te refieres Dennis tiene un singel = en su código también.
Johan
0

No hay una manera fácil de filtrar con un hallazgo. Pero he encontrado una forma cercana de replicar la funcionalidad, pero tenga en cuenta algunas cosas para mi solución.

Esta solución le permite filtrar genéricamente sin conocer la clave principal en .net-core

  1. Buscar es fundamentalmente diferente porque obtiene la entidad si está presente en el seguimiento antes de consultar la base de datos.

  2. Además, puede filtrar por un Objeto para que el usuario no tenga que conocer la clave principal.

  3. Esta solución es para EntityFramework Core.

  4. Esto requiere acceso al contexto

Aquí hay algunos métodos de extensión para agregar que lo ayudarán a filtrar por clave primaria

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

Una vez que tenga estos métodos de extensión, puede filtrar así:

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