2008-10-14 29 views
11

Tengo algunos datos jerárquicos: cada entrada tiene una identificación y una identificación de entrada principal (anulable). Quiero recuperar todas las entradas en el árbol debajo de una entrada determinada. Esto está en una base de datos SQL Server 2005. Lo estoy consultando con LINQ to SQL en C# 3.5.Datos jerárquicos en Linq - opciones y rendimiento

LINQ to SQL no admite Common Table Expressions directamente. Mis opciones son ensamblar los datos en código con varias consultas LINQ, o hacer una vista en la base de datos que surja un CTE.

¿Qué opción (u otra opción) crees que funcionará mejor cuando los volúmenes de datos sean grandes? ¿Se admite SQL Server 2008 HierarchyId type en Linq a SQL?

Respuesta

6

Configuraría una vista y una función asociada basada en tablas basada en el CTE. Mi razonamiento para esto es que, si bien podría implementar la lógica en el lado de la aplicación, esto implicaría enviar los datos intermedios a través del cable para el cálculo en la aplicación. Usando el diseñador de DBML, la vista se traduce en una entidad de tabla. A continuación, puede asociar la función con la entidad Tabla e invocar el método creado en el DataContext para derivar objetos del tipo definido por la vista. El uso de la función basada en tablas permite que el motor de consultas tenga en cuenta los parámetros al construir el conjunto de resultados en lugar de aplicar una condición en el conjunto de resultados definido por la vista después del hecho.

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] 

Para usarlo que haría algo así como - si se asume algún esquema de nomenclatura razonable:

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

He probado una función como esta, y parece ser el camino a seguir. Y se puede llamar desde LINQ, adjunto al contexto de datos. Además, ¿por qué tanto la vista como la función? - parecen ser una duplicación – Anthony

+1

La función no asigna el mismo esquema que la tabla. Incluye el nivel. Si no tiene la columna agregada, puede asignarla directamente a la tabla. Supuse que el nivel en la jerarquía era importante. – tvanfosson

2

En MS SQL 2008 puede usar HierarchyID directamente, en sql2005 puede que tenga que implementarlos manualmente. ParentID no es tan eficiente en grandes conjuntos de datos. También verifique this article para más discusión sobre el tema.

+0

No se menciona allí si HierarchyID es utilizable en LINQ to SQL – Anthony

+1

wow, esto es definitivamente la respuesta. ¡buen post! – Shawn

+0

no es utilizable en linq2sql fuera de la caja –

3

he hecho esto de dos maneras:

  1. impulsan la recuperación de cada capa de el árbol basado en la entrada del usuario. Imagine un control de vista de árbol poblado con el nodo raíz, los elementos secundarios de la raíz y los nietos de la raíz. Solo la raíz y los hijos se expanden (los nietos se ocultan con el colapso). A medida que el usuario expande un nodo secundario, se muestran los nietos de la raíz (que se recuperaron y ocultaron previamente), y se lanza una recuperación de todos los bisnietos. Repita el patrón para N capas profundas. Este patrón funciona muy bien para árboles grandes (profundidad o ancho) porque solo recupera la parte del árbol necesaria.
  2. Utilice un procedimiento almacenado con LINQ. Utilice algo así como una expresión de tabla común en el servidor para generar sus resultados en una tabla plana, o cree un árbol XML en T-SQL. Scott Guthrie tiene un great article sobre el uso de procs almacenados en LINQ. Cree su árbol a partir de los resultados cuando regresen si está en un formato plano, o use el árbol XML si eso es lo que devuelve.
+1

Estaba completamente atascado en encontrar una solución a esto cuando su respuesta me abrió el hecho de que no es necesario tirar de un árbol entero, simplemente sacar a los niños cuando sea necesario. – ProfK

3

Este método de extensión podría ser modificado para usar IQueryable. Lo he usado con éxito en el pasado en una colección de objetos. Puede funcionar para su escenario.

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

Así es como lo he llamado:

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

Este código es una versión mejorada, insecto-fija del código que se encuentra here.

+0

O puede ver dónde se enganchó en: http://weblogs.asp.net/okloeten/archive/2006/07/09/Hierarchical-Linq-Queries.aspx – TheSoftwareJedi

+1

Agregué la atribución al Jedi. Mi versión está simplificada y mejorada. – JarrettV

1

Obtuve este enfoque desde Rob Conery's blog (compruebe alrededor de Pt. 6 para este código, también en codeplex) y me encanta usarlo. Esto podría remodelarse para admitir múltiples niveles "secundarios".

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

¿Pero puede ser remodelado para admitir un número ilimitado de subniveles? – Anthony

+0

No va a agregar una docena de subcategorías a esta consulta; no es particularmente flexible. –

0

El problema para obtener los datos del lado del cliente es que nunca se puede estar seguro de qué tan profundo debe ir. Este método hará un viaje de ida y vuelta por profundidad y se puede unir para hacer desde 0 hasta una profundidad específica en una ida y vuelta.

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

No puede, sin embargo, hacer profundidad arbitraria. Si realmente necesita una profundidad arbitraria, debe hacer eso en la base de datos para que pueda tomar la decisión correcta de detenerse.

15

Este option también podrían resultar útiles:

LINQ AsHierarchy método de extensión()
http://www.scip.be/index.php?Page=ArticlesNET18

+1

Estoy usando esto y funciona extremadamente bien, especialmente la versión actualizada a la que se hace referencia en la parte inferior. –

8

Soy sorprende que nadie ha mencionado un diseño de base de datos alternativo - cuando jerarquía tiene que ser aplanado a partir de múltiples niveles y se recupera con alto rendimiento (no considerando el espacio de almacenamiento), es mejor usar otra tabla entity-2-entity para rastrear la jerarquía en lugar del método parent_id.

Permitirá no sólo las relaciones individuales de los padres, sino también a las relaciones de múltiples matrices, las indicaciones de nivel y diferentes tipos de relaciones:

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

No me gusta ese método: los bucles "while" no son muy buenos para SQL, y si hay una forma más declarativa de hacerlo, en su lugar debería preferirse. Y ahora existe: use una vista o una función basada en tablas usando Common Table Expression, usando el constructo WITH .. UNION ALL como se muestra en otras respuestas aquí. – Anthony

+0

Por favor, considere insertar un extracto de la solución en la página que ha vinculado. Los enlaces pueden estar muertos algún día. – rcdmk

Cuestiones relacionadas