web-dev-qa-db-fra.com

Comment Entity Framework fonctionne-t-il avec les hiérarchies récursives? Include () semble ne pas fonctionner avec

J'ai un Item. Item a une Category.

Category a ID, Name,ParentetChildren. Parent et Children sont également de Category.

Lorsque je fais une requête LINQ to Entities pour une Item spécifique, elle ne renvoie pas la Category associée, sauf si j'utilise la méthode Include("Category"). Mais il n'apporte pas la catégorie complète, avec ses parents et ses enfants. Je pourrais faire Include("Category.Parent"), mais cet objet ressemble à un arbre, j'ai une hiérarchie récursive et je ne sais pas où il se termine.

Comment puis-je faire en sorte que EF charge complètement la Category, avec le parent et les enfants, et le parent avec le parent et les enfants, etc.?

Ce n'est pas quelque chose pour toute l'application, pour des considérations de performances, il ne serait nécessaire que pour cette entité spécifique, la catégorie.

65
Victor Rodrigues

Au lieu d'utiliser la méthode Include, vous pouvez utiliser Load.

Vous pouvez ensuite en faire une pour chacun et parcourir tous les enfants en les chargeant. Ensuite, faites un pour chacun à travers leurs enfants, et ainsi de suite. 

Le nombre de niveaux que vous allez descendre sera codé en dur dans le nombre de boucles que vous avez. 

Voici un exemple d'utilisation de Load: http://msdn.Microsoft.com/en-us/library/bb896249.aspx

19
Shiraz Bhaiji

Si vous voulez absolument que toute la hiérarchie soit chargée, alors si c'était mon cas, j'essaierais d'écrire une procédure stockée. Son rôle est de renvoyer tous les éléments dans une hiérarchie, en renvoyant celui que vous demandez en premier (et ses enfants par la suite).

Et ensuite, laissez la correction des relations avec EF s’assurer qu’elles sont toutes connectées.

c'est-à-dire quelque chose comme:

// the GetCategoryAndHierarchyById method is an enum
Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();

Si vous avez écrit correctement votre procédure stockée, la matérialisation de tous les éléments de la hiérarchie (c'est-à-dire ToList()) devrait permettre la correction de la relation EF.

Et ensuite, l'élément que vous voulez (First ()) doit avoir tous ses enfants chargés, ainsi que leurs enfants, etc. Tous doivent être renseignés à partir de cet appel de procédure stockée, donc aucun problème MARS non plus.

J'espère que cela t'aides

Alex

14
Alex James

Il pourrait être dangereux de charger toutes les entités récursives, en particulier dans la catégorie. Vous pourriez vous retrouver avec bien plus que ce que vous aviez prévu:

Category > Item > OrderLine > Item
                  OrderHeader > OrderLine > Item
         > Item > ...

Tout à coup, vous avez chargé la majeure partie de votre base de données. Vous pourriez également avoir chargé des lignes de factures, puis des clients, puis toutes leurs autres factures.

Ce que vous devriez faire est quelque chose comme ceci:

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select q;

foreach (Category cat in qryCategories) {
    if (!cat.Items.IsLoaded)
        cat.Items.Load();
    // This will only load product groups "once" if need be.
    if (!cat.ProductGroupReference.IsLoaded)
        cat.ProductGroupReference.Load();
    foreach (Item item in cat.Items) {
        // product group and items are guaranteed
        // to be loaded if you use them here.
    }
}

Une meilleure solution consiste toutefois à construire votre requête pour créer une classe anonyme avec les résultats afin que vous n'ayez besoin de frapper votre magasin de données qu'une seule fois.

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select new {
                        Category = q,
                        ProductGroup = q.ProductGroup,
                        Items = q.Items
                    };

De cette façon, vous pouvez renvoyer un résultat de dictionnaire si nécessaire.

N'oubliez pas que vos contextes doivent être aussi brefs que possible.

5
Brett Ryan

Vous ne voulez pas effectuer de chargement récursif de la hiérarchie, sauf si vous autorisez un utilisateur à explorer de manière itérative l'arborescence: chaque niveau de récursivité est un autre voyage dans la base de données. De même, vous voudrez désactiver le chargement paresseux pour éviter d'autres déclenchements de base de données lorsque vous parcourez la hiérarchie lors du rendu sur une page ou lors de l'envoi sur un service Web.

Au lieu de cela, retournez votre requête: Obtenez Catalog, et Include les éléments qu’elle contient. Cela vous permettra d'obtenir tous les éléments à la fois hiérarchiquement (propriétés de navigation) et aplatis. Il ne vous reste donc plus qu'à exclure les éléments non-root présents à la racine, ce qui devrait être assez trivial. 

J'ai eu ce problème et fourni un exemple détaillé de cette solution à un autre, ici

4
JoeBrockhaus

Vous devriez plutôt introduire une table de correspondance qui associe chaque catégorie à un parent et à un enfant, au lieu d’ajouter les propriétés parent et enfant à la cargaison elle-même.

Selon la fréquence à laquelle vous avez besoin de ces informations, vous pouvez les consulter à la demande. Via des contraintes uniques dans la base de données, vous pouvez éviter une quantité infinie de relations possibles.

3
Johannes Rudolph

Utilisez cette méthode d'extension qui appelle la version codée en dur de Include, pour obtenir un niveau d'inclusion dynamique, cela fonctionne très bien.

namespace System.Data.Entity
{
  using Linq;
  using Linq.Expressions;
  using Text;

  public static class QueryableExtensions
  {
    public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
      int levelIndex, Expression<Func<TEntity, TEntity>> expression)
    {
      if (levelIndex < 0)
        throw new ArgumentOutOfRangeException(nameof(levelIndex));
      var member = (MemberExpression)expression.Body;
      var property = member.Member.Name;
      var sb = new StringBuilder();
      for (int i = 0; i < levelIndex; i++)
      {
        if (i > 0)
          sb.Append(Type.Delimiter);
        sb.Append(property);
      }
      return source.Include(sb.ToString());
    }
  }
}

Usage:

var affiliate = await DbContext.Affiliates
  .Include(3, a => a.Referrer)
  .SingleOrDefaultAsync(a => a.Id == affiliateId);

Quoi qu’il en soit, rejoignez la discussion à ce sujet sur le repo EF.

3
Shimmy

Voici une fonction récursive intelligente que j'ai trouvée ici qui fonctionnerait pour cela: 

public partial class Category
{
    public IEnumerable<Category> AllSubcategories()
    {
        yield return this;
        foreach (var directSubcategory in Subcategories)
            foreach (var subcategory in directSubcategory.AllSubcategories())
            {
                yield return subcategory;
            }
    }
}
1
parliament

Vous pouvez également créer une fonction tablevalued dans la base de données et l’ajouter à votre DBContext. Ensuite, vous pouvez appeler cela depuis votre code. 

Cet exemple nécessite l'importation de EntityFramework.Functions à partir de pépite.

public class FunctionReturnType
{
    public Guid Id { get; set; } 

    public Guid AnchorId { get; set; } //the zeroPoint for the recursion

    // Add other fields as you want (add them to your tablevalued function also). 
    // I noticed that nextParentId and depth are useful
}

public class _YourDatabaseContextName_ : DbContext
{
    [TableValuedFunction("RecursiveQueryFunction", "_YourDatabaseContextName_")]
    public IQueryable<FunctionReturnType> RecursiveQueryFunction(
        [Parameter(DbType = "boolean")] bool param1 = true
    )
    {
        //Example how to add parameters to your function
        //TODO: Ask how to make recursive queries with SQL 
        var param1 = new ObjectParameter("param1", param1);
        return this.ObjectContext().CreateQuery<FunctionReturnType>(
            $"RecursiveQueryFunction(@{nameof(param1)})", param1);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //add both (Function returntype and the actual function) to your modelbuilder. 
        modelBuilder.ComplexType<FunctionReturnType>();
        modelBuilder.AddFunctions(typeof(_YourDatabaseContextName_), false);

        base.OnModelCreating(modelBuilder);
    }

    public IEnumerable<Category> GetParents(Guid id)
    {
        //this = dbContext
        return from hierarchyRow in this.RecursiveQueryFunction(true)
            join yourClass from this.Set<YourClassThatHasHierarchy>()
            on hierarchyRow.Id equals yourClass.Id
            where hierarchyRow.AnchorId == id
            select yourClass;
    }
}
1
Ozzian

Ma suggestion serait

var query = CreateQuery()
    .Where(entity => entity.Id == Id)
    .Include(entity => entity.Parent);
var result = await FindAsync(query);

return result.FirstOrDefault();

et cela signifie qu'il va charger une seule variable entity et toutes ces entity.Parent entités recursive.

entity is same as entity.Parent
0
aursad

@parlement m'a donné une idée pour EF6. Exemple de catégorie avec méthodes pour charger tous les parents jusqu'au nœud racine et tous les enfants.

NOTE: Utilisez ceci uniquement pour les opérations non critiques en termes de performances. Exemple avec performances de 1000 nœuds depuis http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html .

Loading 1000 cat. with navigation properties took 15259 ms 
Loading 1000 cat. with stored procedure took 169 ms

Code:

public class Category 
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Name { get; set; }

    public int? ParentId { get; set; }

    public virtual Category Parent { get; set; }

    public virtual ICollection<Category> Children { get; set; }

    private IList<Category> allParentsList = new List<Category>();

    public IEnumerable<Category> AllParents()
    {
        var parent = Parent;
        while (!(parent is null))
        {
            allParentsList.Add(parent);
            parent = parent.Parent;
        }
        return allParentsList;
    }

    public IEnumerable<Category> AllChildren()
    {
        yield return this;
        foreach (var child in Children)
        foreach (var granChild in child.AllChildren())
        {
            yield return granChild;
        }
    }   
}
0
Ogglas

essaye ça

List<SiteActionMap> list = this.GetQuery<SiteActionMap>()
                .Where(m => m.Parent == null && m.Active == true)
                .Include(m => m.Action)
                .Include(m => m.Parent).ToList();    

if (list == null)
    return null;

this.GetQuery<SiteActionMap>()
    .OrderBy(m => m.SortOrder)
    .Where(m => m.Active == true)
    .Include(m => m.Action)
    .Include(m => m.Parent)
    .ToList();

return list;
0
tobias

Et maintenant, pour une approche complètement différente des données hiérarchiques, par exemple le peuplement d’un arbre.

Commencez par effectuer une requête simple pour toutes les données, puis construisez le graphe d'objets en mémoire:

  var items = this.DbContext.Items.Where(i=> i.EntityStatusId == entityStatusId).Select(a=> new ItemInfo() { 
            Id = a.Id,
            ParentId = a.ParentId,
            Name = a.Name,
            ItemTypeId = a.ItemTypeId
            }).ToList();

Obtenez l'élément racine:

 parent = items.FirstOrDefault(a => a.ItemTypeId == (int)Enums.ItemTypes.Root);

Maintenant, construis ton graphique:

 this.GetDecendantsFromList(parent, items);


 private void GetDecendantsFromList(ItemInfo parent, List<ItemInfo> items)
    {
        parent.Children = items.Where(a => a.ParentId == parent.Id).ToList();
        foreach (var child in parent.Children)
        {
            this.GetDecendantsFromList(child,items);
        }
    }
0
Greg Gum