Pregunta

Tengo algunos datos jerárquicos: cada entrada tiene un ID y un ID de entrada principal (que puede contener nulos). Quiero recuperar todas las entradas en el árbol bajo una entrada determinada. Esto está en una base de datos de SQL Server 2005. Lo estoy consultando con LINQ to SQL en C # 3.5.

LINQ to SQL no admite Expresiones de tabla comunes directamente. Mis opciones son reunir los datos en código con varias consultas LINQ, o hacer una vista en la base de datos que aparece en un CTE.

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

¿Fue útil?

Solución

Configuraré una vista y una función asociada basada en la tabla 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 de la aplicación. Usando el diseñador DBML, la vista se traduce en una entidad de tabla. Luego puede asociar la función con la entidad de 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 le permite al motor de consultas tener en cuenta sus 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, haría algo como: asumir un esquema de denominación 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...
    }
}

Otros consejos

Esta opción también puede resultar útil:

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

Me sorprende que nadie haya mencionado un diseño de base de datos alternativo: cuando la jerarquía necesita ser aplanada desde múltiples niveles y recuperada con un alto rendimiento (sin considerar el espacio de almacenamiento) es mejor usar otra tabla de entidad-2-entidad para rastrear la jerarquía En lugar del enfoque parent_id.

Permitirá no solo relaciones con un solo padre sino también relaciones con varios padres, 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)
);

He hecho esto de dos maneras:

  1. Conduzca la recuperación de cada capa del árbol en función de la entrada del usuario. Imagine un control de vista de árbol poblado con el nodo raíz, los hijos 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 visualizan los nietos de la raíz (que se recuperaron y ocultaron previamente), y se inicia una recuperación de todos los bisnietos. Repita el patrón para las capas N profundas. Este patrón funciona muy bien para árboles grandes (profundidad o ancho) porque solo recupera la porción del árbol necesaria.
  2. Utilice un procedimiento almacenado con LINQ. Use algo como una expresión de tabla común en el servidor para generar sus resultados en una tabla plana, o genere un árbol XML en T-SQL. Scott Guthrie tiene un gran artículo sobre el uso de procedimientos almacenados en LINQ. Construya su árbol a partir de los resultados cuando regresen si están en un formato plano, o use el árbol XML si eso es lo que devuelve.

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 llamé:

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

Este código es una versión mejorada y corregida de errores del código encontrado here .

En MS SQL 2008 puede usar HierarchyID directamente, en sql2005 puede que tenga que implementarlos manualmente. ParentID no es tan eficaz en grandes conjuntos de datos. También consulte este artículo para obtener más información sobre el tema.

Obtuve este enfoque del blog de Rob Conery (consulte el Pt. 6 para ver esto código, también en codeplex) y me encanta usarlo. Esto podría modificarse para que sea compatible con múltiples " sub " niveles.

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

El problema de obtener los datos del lado del cliente es que nunca puede estar seguro de la profundidad a la que debe ir. Este método hará un viaje de ida y vuelta por profundidad y podría unirse para hacer desde 0 hasta una profundidad específica en un viaje de 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 hacerlo en la base de datos, para que pueda tomar la decisión correcta de parar.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top