C # Entity-Framework: Comment puis-je combiner un .Find et .Include sur un objet de modèle?

146

Je fais le didacticiel de pratique mvcmusicstore. J'ai remarqué quelque chose lors de la création de l'échafaudage pour le gestionnaire d'albums (ajouter supprimer modifier).

Je veux écrire du code avec élégance, donc je cherche la manière propre d'écrire ceci.

Pour info, je rends le magasin plus générique:

Albums = éléments

Genres = Catégories

Artiste = Marque

Voici comment l'index est récupéré (généré par MVC):

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

Voici comment l'élément à supprimer est récupéré:

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

Le premier ramène tous les articles et remplit les modèles de catégorie et de marque à l'intérieur du modèle d'article. Le second ne remplit pas la catégorie et la marque.

Comment puis-je écrire le second pour faire la recherche ET remplir ce qu'il y a à l'intérieur (de préférence en 1 ligne) ... théoriquement - quelque chose comme:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);
Ralph N
la source
Si quelqu'un a besoin de le faire de manière générique dans.net-core, voir ma réponse
johnny 5

Réponses:

162

Vous devez d' Include()abord utiliser , puis récupérer un seul objet de la requête résultante:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .SingleOrDefault(x => x.ItemId == id);
Dennis Traub
la source
24
Je recommanderais vraiment d'utiliser ce dernier (SingleOrDefault), ToList récupérera d' abord toutes les entrées, puis en sélectionnera une
Sander Rijken
5
Cela se décompose si nous avons une clé primaire composite et que nous utilisons la surcharge de recherche appropriée.
jhappoldt
78
Cela fonctionnerait, mais il y a une différence entre l'utilisation de "Find" et "SingleOrDefault". La méthode "Find" retourne l'objet à partir du magasin suivi local s'il existe, évitant un aller-retour vers la base de données, où l'utilisation de "SingleOrDefault" forcera quand même une requête à la base de données.
Iravanchi
3
@Iravanchi a raison. Cela a peut-être fonctionné pour l'utilisateur, mais l'opération et ses effets secondaires ne sont pas équivalents à Find, pour autant que je sache.
mwilson
3
Ne répond pas réellement à la question des ops car il n'utilise pas .Find
Paul Swetz
73

La réponse de Dennis utilise Includeet SingleOrDefault. Ce dernier fait un aller-retour vers la base de données.

Une alternative, est d'utiliser Find, en combinaison avec Load, pour le chargement explicite des entités liées ...

Ci-dessous un exemple 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(); 
}

Bien sûr, Findretourne immédiatement sans faire de requête au magasin, si cette entité est déjà chargée par le contexte.

Apprenant
la source
30
Cette méthode utilise Finddonc si l'entité est présente, il n'y a pas d'aller-retour vers la base de données pour l'entité elle-même. MAIS, vous aurez un aller-retour pour chaque relation que vous êtes Load, alors que la SingleOrDefaultcombinaison avec Includecharge tout en une seule fois.
Iravanchi
Quand j'ai comparé les 2 dans le profileur SQL, Find / Load était meilleur pour mon cas (j'avais une relation 1: 1). @Iravanchi: voulez-vous dire que si j'avais eu une relation 1: m, cela aurait appelé m fois le magasin? ... parce que cela n'aurait pas beaucoup de sens.
Apprenant
3
Pas de relation 1: m, mais de multiples relations. Chaque fois que vous appelez la Loadfonction, la relation doit être remplie au retour de l'appel. Donc, si vous appelez Loadplusieurs fois pour plusieurs relations, il y aura un aller-retour à chaque fois. Même pour une seule relation, si la Findméthode ne trouve pas l'entité en mémoire, elle effectue deux allers-retours: un pour Findet le second pour Load. Mais le Include. SingleOrDefaultapproche récupère l'entité et la relation en une seule fois pour autant que je sache (mais je ne suis pas sûr)
Iravanchi
1
Cela aurait été bien si le pouvait avoir suivi le design Inclure d'une manière ou d'une autre plutôt que d'avoir à traiter les collections et les références différemment. Cela rend plus difficile la création d'une façade GetById () qui ne prend qu'une collection facultative d'Expression <Func <T, object >> (par exemple _repo.GetById (id, x => x.MyCollection))
Derek Greer
4
N'oubliez pas de mentionner la référence de votre message: msdn.microsoft.com/en-us/data/jj574232.aspx#explicit
Hossein
1

Vous devez convertir IQueryable en DbSet

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

return dbSet.Find(id);

Rafael R. Souza
la source
Il n'y a pas de .Find ou .FindAsync dans le dbSet. Est-ce EF Core?
Thierry
il y a ef 6 aussi sur ef core
Rafael R. Souza
J'avais bon espoir, puis "InvalidCastException"
ZX9
0

Cela n'a pas fonctionné pour moi. Mais je l'ai résolu en faisant comme ça.

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

Je ne sais pas si c'est une bonne solution. Mais l'autre que Dennis m'a donné m'a donné une erreur booléenne .SingleOrDefault(x => x.ItemId = id);

Johan
la source
4
La solution de Dennis doit également fonctionner. Peut-être avez-vous cette erreur SingleOrDefault(x => x.ItemId = id)uniquement à cause du mauvais single =au lieu du double ==?
Slauma
6
ouais, on dirait que vous avez utilisé = not ==. Erreur de syntaxe;)
Ralph N
Je les ai essayés tous les deux == et = m'a toujours donné une erreur dans .SingleOrDefault (x => x.ItemId = id); = / Il doit y avoir quelque chose d'autre dans mon code qui ne va pas. Mais la façon dont je l'ai fait est une mauvaise façon? Peut-être que je ne comprends pas ce que tu veux dire, Dennis a un singel = dans son code aussi.
Johan le
0

Il n'y a pas de moyen vraiment simple de filtrer avec une trouvaille. Mais j'ai trouvé un moyen rapproché de répliquer la fonctionnalité, mais veuillez prendre note de quelques éléments pour ma solution.

Cette solution vous permet de filtrer de manière générique sans connaître la clé primaire dans .net-core

  1. Find est fondamentalement différent car il obtient l'entité si elle est présente dans le suivi avant d'interroger la base de données.

  2. En outre, il peut filtrer par un objet afin que l'utilisateur n'ait pas à connaître la clé primaire.

  3. Cette solution est pour EntityFramework Core.

  4. Cela nécessite un accès au contexte

Voici quelques méthodes d'extension à ajouter qui vous aideront à filtrer par clé primaire afin

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

Une fois que vous avez ces méthodes d'extension, vous pouvez filtrer comme suit:

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