2008-10-14 7 views
11

J'ai quelques données hiérarchiques - chaque entrée a un ID et un ID d'entrée parent (nullable). Je veux récupérer toutes les entrées dans l'arborescence sous une entrée donnée. C'est dans une base de données SQL Server 2005. Je l'interroge avec LINQ to SQL en C# 3.5.Données hiérarchiques dans Linq - options et performances

LINQ to SQL ne prend pas en charge Common Table Expressions directement. Mes choix sont d'assembler les données dans le code avec plusieurs requêtes LINQ, ou de faire une vue sur la base de données qui fait surface d'un CTE.

Quelle option (ou une autre option) pensez-vous qui fonctionnera le mieux lorsque les volumes de données seront importants? Est-ce que HierarchyId type de SQL Server 2008 est pris en charge dans Linq to SQL?

Répondre

6

Je voudrais mettre en place une vue et une fonction basée sur la table associée basée sur le CTE. Mon raisonnement à ce sujet est que, alors que vous pourriez implémenter la logique du côté de l'application, cela impliquerait d'envoyer les données intermédiaires sur le fil pour le calcul dans l'application. En utilisant le concepteur DBML, la vue se traduit en une entité Table. Vous pouvez ensuite associer la fonction à l'entité Table et appeler la méthode créée sur DataContext pour dériver des objets du type défini par la vue. L'utilisation de la fonction table permet au moteur de recherche de prendre en compte vos paramètres lors de la construction du jeu de résultats plutôt que d'appliquer une condition sur l'ensemble de résultats défini par la vue après le fait.

CREATE TABLE [dbo].[hierarchical_table](
    [id] [int] IDENTITY(1,1) NOT NULL, 
    [parent_id] [int] NULL, 
    [data] [varchar](255) NOT NULL, 
CONSTRAINT [PK_hierarchical_table] PRIMARY KEY CLUSTERED 
(
    [id] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) ON [PRIMARY] 

CREATE VIEW [dbo].[vw_recursive_view] 
AS 
WITH hierarchy_cte(id, parent_id, data, lvl) AS 
(SELECT  id, parent_id, data, 0 AS lvl 
     FROM   dbo.hierarchical_table 
     WHERE  (parent_id IS NULL) 
     UNION ALL 
     SELECT  t1.id, t1.parent_id, t1.data, h.lvl + 1 AS lvl 
     FROM   dbo.hierarchical_table AS t1 INNER JOIN 
          hierarchy_cte AS h ON t1.parent_id = h.id) 
SELECT  id, parent_id, data, lvl 
FROM   hierarchy_cte AS result 


CREATE FUNCTION [dbo].[fn_tree_for_parent] 
(
    @parent int 
) 
RETURNS 
@result TABLE 
(
    id int not null, 
    parent_id int, 
    data varchar(255) not null, 
    lvl int not null 
) 
AS 
BEGIN 
    WITH hierarchy_cte(id, parent_id, data, lvl) AS 
    (SELECT  id, parent_id, data, 0 AS lvl 
     FROM   dbo.hierarchical_table 
     WHERE  (id = @parent OR (parent_id IS NULL AND @parent IS NULL)) 
     UNION ALL 
     SELECT  t1.id, t1.parent_id, t1.data, h.lvl + 1 AS lvl 
     FROM   dbo.hierarchical_table AS t1 INNER JOIN 
      hierarchy_cte AS h ON t1.parent_id = h.id) 
    INSERT INTO @result 
    SELECT  id, parent_id, data, lvl 
    FROM   hierarchy_cte AS result 
RETURN 
END 

ALTER TABLE [dbo].[hierarchical_table] WITH CHECK ADD CONSTRAINT [FK_hierarchical_table_hierarchical_table] FOREIGN KEY([parent_id]) 
REFERENCES [dbo].[hierarchical_table] ([id]) 

ALTER TABLE [dbo].[hierarchical_table] CHECK CONSTRAINT [FK_hierarchical_table_hierarchical_table] 

Pour l'utiliser, vous feriez quelque chose comme - en supposant un certain schéma de nommage raisonnable:

using (DataContext dc = new HierarchicalDataContext()) 
{ 
    HierarchicalTableEntity h = (from e in dc.HierarchicalTableEntities 
           select e).First(); 
    var query = dc.FnTreeForParent(h.ID); 
    foreach (HierarchicalTableViewEntity entity in query) { 
     ...process the tree node... 
    } 
} 
+1

J'ai essayé une fonction comme celle-ci, et il semble être le chemin à parcourir. Et il peut être appelé depuis LINQ, attaché au datacontext. Aussi, pourquoi à la fois la vue et la fonction? - ils semblent être la duplication – Anthony

+1

La fonction ne mappe pas le même schéma que la table. Il comprend le niveau. Si vous n'avez pas ajouté la colonne, vous pouvez la mapper directement sur la table. J'ai supposé que le niveau dans la hiérarchie était important. – tvanfosson

2

En MS SQL 2008, vous pouvez utiliser directement HierarchyID, en SQL2005 vous devrez peut-être les implémenter manuellement. ParentID n'est pas performant sur les grands ensembles de données. Vérifiez également this article pour plus de discussion sur le sujet.

+0

Il n'y a aucune mention là si HierarchyID est utilisable dans LINQ to SQL – Anthony

+1

wow, cela est certainement la réponse. belle publication! – Shawn

+0

ce n'est pas utilisable dans linq2sql out of the box –

3

Je l'ai fait deux façons:

  1. Drive la récupération de chaque couche de l'arbre basé sur l'entrée de l'utilisateur. Imaginez un contrôle de vue arborescente peuplé du nœud racine, des enfants de la racine et des petits-enfants de la racine. Seule la racine et les enfants sont élargis (les petits-enfants sont cachés avec l'effondrement). Au fur et à mesure que l'utilisateur développe un nœud enfant, les petits-enfants de la racine sont affichés (précédemment récupérés et cachés), et une récupération de tous les arrière-petits-enfants est lancée. Répétez le motif pour N-couches profondes. Ce modèle fonctionne très bien pour les grands arbres (profondeur ou largeur) car il ne récupère que la partie de l'arbre nécessaire.
  2. Utilisez une procédure stockée avec LINQ. Utilisez quelque chose comme une expression de table commune sur le serveur pour créer vos résultats dans une table plate ou créez une arborescence XML dans T-SQL. Scott Guthrie a un great article sur l'utilisation des procédures stockées dans LINQ. Construisez votre arbre à partir des résultats quand ils reviennent dans un format plat, ou utilisez l'arborescence XML si c'est ce que vous retournez.
+1

J'étais plutôt embourbé dans la recherche d'une solution à ce problème lorsque votre réponse m'a ouvert le fait que je n'ai pas besoin de tirer un arbre entier, il suffit de tirer sur les enfants en cas de besoin. – ProfK

3

Cette méthode d'extension pourrait être potentiellement modifiée pour utiliser IQueryable. Je l'ai utilisé avec succès dans le passé sur une collection d'objets. Cela peut fonctionner pour votre scénario.

public static IEnumerable<T> ByHierarchy<T>(
this IEnumerable<T> source, Func<T, bool> startWith, Func<T, T, bool> connectBy) 
{ 
    if (source == null) 
    throw new ArgumentNullException("source"); 

    if (startWith == null) 
    throw new ArgumentNullException("startWith"); 

    if (connectBy == null) 
    throw new ArgumentNullException("connectBy"); 

    foreach (T root in source.Where(startWith)) 
    { 
    yield return root; 
    foreach (T child in source.ByHierarchy(c => connectBy(root, c), connectBy)) 
    { 
    yield return child; 
    } 
} 
} 

Voici comment je l'ai appelé:

comments.ByHierarchy(comment => comment.ParentNum == parentNum, 
(parent, child) => child.ParentNum == parent.CommentNum && includeChildren) 

Ce code est une version améliorée de bogue fixe du code trouvé here.

+0

Ou vous pouvez vérifier où il a arraché cela de: http://weblogs.asp.net/okloeten/archive/2006/07/09/Hierarchical-Linq-Queries.aspx – TheSoftwareJedi

+1

J'ai ajouté l'attribution au Jedi. Ma version est simplifiée et améliorée. – JarrettV

1

J'ai obtenu cette approche de Rob Conery's blog (vérifiez autour de Pt.6 pour ce code, également sur codeplex) et j'adore l'utiliser. Cela pourrait être remodelé pour supporter plusieurs niveaux "sub".

var categories = from c in db.Categories 
       select new Category 
       { 
        CategoryID = c.CategoryID, 
        ParentCategoryID = c.ParentCategoryID, 
        SubCategories = new List<Category>(
             from sc in db.Categories 
             where sc.ParentCategoryID == c.CategoryID 
             select new Category { 
             CategoryID = sc.CategoryID, 
             ParentProductID = sc.ParentProductID 
             } 
            ) 
          }; 
+1

Mais peut-il être remodelé pour supporter un nombre illimité de sous-niveaux? – Anthony

+0

Vous n'allez pas ajouter une douzaine de sous-catégories à cette requête - elle n'est pas particulièrement flexible. –

0

Le problème avec l'extraction des données du côté client est que vous ne pouvez jamais être sûr de la profondeur de vos opérations. Cette méthode effectuera un aller-retour par profondeur et il sera possible de faire une union de 0 à une profondeur spécifiée dans un aller-retour.

public IQueryable<Node> GetChildrenAtDepth(int NodeID, int depth) 
{ 
    IQueryable<Node> query = db.Nodes.Where(n => n.NodeID == NodeID); 
    for(int i = 0; i < depth; i++) 
    query = query.SelectMany(n => n.Children); 
     //use this if the Children association has not been defined 
    //query = query.SelectMany(n => db.Nodes.Where(c => c.ParentID == n.NodeID)); 
    return query; 
} 

Cependant, il ne peut pas faire de profondeur arbitraire. Si vous avez réellement besoin d'une profondeur arbitraire, vous devez le faire dans la base de données pour pouvoir prendre la bonne décision.

8

Je suis surpris que personne n'a mentionné une conception de base de données alternative - lorsque la hiérarchie doit être aplatie de multiples niveaux et récupéré avec de hautes performances (pas si compte tenu de l'espace de stockage) il est préférable d'utiliser une autre table entité-2-entité pour suivre la hiérarchie au lieu de l'approche parent_id.

Il permettra non seulement les relations monoparentales, mais aussi les relations parents multi, indications de niveau et les différents types de relations:

CREATE TABLE Person (
    Id INTEGER, 
    Name TEXT 
); 

CREATE TABLE PersonInPerson (
    PersonId INTEGER NOT NULL, 
    InPersonId INTEGER NOT NULL, 
    Level INTEGER, 
    RelationKind VARCHAR(1) 
); 
0
+0

Je n'aime pas cette méthode - "while" les boucles ne sont pas très bonnes en pratique SQL, et s'il y a une façon plus déclarative de le faire, cela devrait être préféré à la place. Et il existe maintenant: utilisez une fonction basée sur une vue ou une table en utilisant l'expression de table commune, en utilisant la construction WITH .. UNION ALL comme indiqué dans les autres réponses ici. – Anthony

+0

Veuillez envisager d'insérer un extrait de la solution sur la page que vous avez liée. Les liens peuvent être morts un jour. – rcdmk

Questions connexes