2011-05-17 33 views
10

He examinado algunos tutoriales de jerarquía de SQL, pero ninguno de ellos tenía mucho sentido para mi aplicación. Tal vez simplemente no los entiendo correctamente. Estoy escribiendo una aplicación C# ASP.NET y me gustaría crear una jerarquía de vista de árbol a partir de datos SQL.Jerarquía de datos SQL

Esta es la forma en la jerarquía funcionaría:

 
SQL TABLE 

ID  | Location ID | Name 
_______| __________ |_____________ 
1331 | 1331  | House 
1321 | 1331  | Room 
2141 | 1321  | Bed 
1251 | 2231  | Gym 

Si el ID y la ID de ubicación son los mismos, esto sería determinar el Padre superior. Todos los hijos de ese padre tendrían la misma identificación de ubicación que el padre. Todos los nietos de ese niño tendrían una identificación de ubicación igual a la identificación del niño, y así sucesivamente.

Para el ejemplo anterior:

 
- House 
    -- Room 
     --- Bed 

Cualquier ayuda o dirección a fácil de seguir tutoriales sería muy apreciada.

EDIT:

código que tengo hasta ahora, pero que sólo se pone la de los padres y los niños, no hay nietos. Parece que no puedo encontrar la manera de obtener recursivamente todos los nodos.

using System; 
using System.Data; 
using System.Collections.Generic; 
using System.Web; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
using System.Configuration; 
using System.Data.SqlClient; 

namespace TreeViewProject 
{ 
public partial class _Default : System.Web.UI.Page 
{ 
    protected void Page_Load(object sender, EventArgs e) 
    { 
     PopulateTree(SampleTreeView); 

    } 



    public void PopulateTree(Control ctl) 
    { 

     // Data Connection 
     SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["AssetWhereConnectionString1"].ConnectionString); 
     connection.Open(); 

     // SQL Commands 
     string getLocations = "SELECT ID, LocationID, Name FROM dbo.Locations"; 
     SqlDataAdapter adapter = new SqlDataAdapter(getLocations, connection); 
     DataTable locations = new DataTable(); 
     // Fill Data Table with SQL Locations Table 
     adapter.Fill(locations); 
     // Setup a row index 
     DataRow[] myRows; 
     myRows = locations.Select(); 

     // Create an instance of the tree 
     TreeView t1 = new TreeView(); 
     // Assign the tree to the control 
     t1 = (TreeView)ctl; 
     // Clear any exisiting nodes 
     t1.Nodes.Clear(); 

     // BUILD THE TREE! 
     for (int p = 0; p < myRows.Length; p++) 
     { 
      // Get Parent Node 
      if ((Guid)myRows[p]["ID"] == (Guid)myRows[p]["LocationID"]) 
      { 
       // Create Parent Node 
       TreeNode parentNode = new TreeNode(); 
       parentNode.Text = (string)myRows[p]["Name"]; 
       t1.Nodes.Add(parentNode); 

       // Get Child Node 
       for (int c = 0; c < myRows.Length; c++) 
       { 
        if ((Guid)myRows[p]["LocationID"] == (Guid)myRows[c]["LocationID"] 
         && (Guid)myRows[p]["LocationID"] != (Guid)myRows[c]["ID"] /* Exclude Parent */) 
        { 
         // Create Child Node 
         TreeNode childNode = new TreeNode(); 
         childNode.Text = (string)myRows[c]["Name"]; 
         parentNode.ChildNodes.Add(childNode); 
        } 
       } 
      } 
     } 
     // ALL DONE BUILDING! 

     // Close the Data Connection 
     connection.Close(); 
    } 

} 
} 

Aquí es una snippit de la tabla de SQL real: Ubicaciones

 
ID          LocationID        Name 
____________________________________ ____________________________________ ______________ 
DEAF3FFF-FD33-4ECF-910B-1B07DF192074 48700BC6-D422-4B26-B123-31A7CB704B97 Drop F 
48700BC6-D422-4B26-B123-31A7CB704B97 7EBDF61C-3425-46DB-A4D5-686E91FD0832 Olway 
06B49351-6D18-4595-8228-356253CF45FF 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 Drop E 5 
E98BC1F6-4BAE-4022-86A5-43BBEE2BA6CD DEAF3FFF-FD33-4ECF-910B-1B07DF192074 Drop F 6 
F6A2CF99-F708-4C61-8154-4C04A38ADDC6 7EBDF61C-3425-46DB-A4D5-686E91FD0832 Pree 
0EC89A67-D74A-4A3B-8E03-4E7AAAFEBE51 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 Drop E 4 
35540B7A-62F9-487F-B65B-4EA5F42AD88A 48700BC6-D422-4B26-B123-31A7CB704B97 Olway Breakdown 
5000AB9D-EB95-48E3-B5C0-547F5DA06FC6 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 Out 1 
53CDD540-19BC-4BC2-8612-5C0663B7FDA5 6E8C65AC-CB22-42DA-89EB-D81C5ED0BBD0 Drop E 3 
7EBDF61C-3425-46DB-A4D5-686E91FD0821 B46C7305-18B1-4499-9E1C-7B6FDE786CD6 TEST 1 
7EBDF61C-3425-46DB-A4D5-686E91FD0832 7EBDF61C-3425-46DB-A4D5-686E91FD0832 HMN 

Gracias.

+0

Esos nombres son confusos. Cambiaría 'Location ID' por' Parent_ID'. – FrustratedWithFormsDesigner

+2

Una respuesta de SQL puro puede necesitar saber qué sabor de SQL está utilizando. ¿Puedes especificar qué base de datos tienes? SQL Server 2008, tal vez? –

+0

Desafortunadamente no puedo cambiar los nombres, demasiadas cosas dependen de ellos. Estoy usando SQL Server 2008. – Will

Respuesta

13

Está buscando una consulta recursiva utilizando una expresión de tabla común, o CTE para abreviar. Un informe detallado para esto en SQL Server 2008 puede ser found on MSDN.

En general, tienen una estructura similar a la siguiente:

WITH cte_name (column_name [,...n]) 
AS (
    –- Anchor 
    CTE_query_definition 

    UNION ALL 

    –- Recursive portion 
    CTE_query_definition 
) 
-- Statement using the CTE 
SELECT * FROM cte_name 

Cuando esto se ejecuta, SQL Server va a hacer algo similar a lo siguiente (parafraseado en un lenguaje más sencillo desde el MSDN):

  1. Divida la expresión CTE en miembros de anclaje y recursivos.
  2. Ejecute el anclaje, creando el primer conjunto de resultados.
  3. Ejecute la parte recursiva, con el paso anterior como entrada.
  4. Repita el paso 3 hasta que se devuelva un conjunto vacío.
  5. Devuelve el conjunto de resultados. Este es UNION TODO el ancla y todos los pasos recursivos.

Para este ejemplo específico, intentar algo como esto:

With hierarchy (id, [location id], name, depth) 
As (
    -- selects the "root" level items. 
    Select ID, [LocationID], Name, 1 As depth 
    From dbo.Locations 
    Where ID = [LocationID] 

    Union All 

    -- selects the descendant items. 
    Select child.id, child.[LocationID], child.name, 
     parent.depth + 1 As depth 
    From dbo.Locations As child 
    Inner Join hierarchy As parent 
     On child.[LocationID] = parent.ID 
    Where child.ID != parent.[Location ID]) 
-- invokes the above expression. 
Select * 
From hierarchy 

Teniendo en cuenta los datos de ejemplo, usted debe obtener algo como esto:

ID  | Location ID | Name | Depth 
_______| __________ |______ | _____ 
1331 | 1331  | House |  1 
1321 | 1331  | Room |  2 
2141 | 1321  | Bed |  3 

Tenga en cuenta que "gimnasio" se excluye. En función de los datos de muestra, su ID no coincide con su [ID de ubicación], por lo que no sería un elemento de nivel raíz. Su ID de ubicación, 2231, no aparece en la lista de identificadores principales válidos.


Edición 1:

Ha pedido de conseguir esto en una estructura de datos de C#. Hay muchas, muchas formas diferentes de representar una jerarquía en C#. Aquí hay un ejemplo, elegido por su simplicidad. Una muestra de código real sin duda sería más extensa.

El primer paso es definir cómo se ve cada nodo en la jerarquía. Además de contener propiedades para cada dato en el nodo, he incluido Parent y Children propiedades, más métodos para Add un niño y Get un niño. El método Get buscará en todo el eje descendente del nodo, no solo en los propios hijos del nodo.

public class LocationNode { 
    public LocationNode Parent { get; set; } 
    public List<LocationNode> Children = new List<LocationNode>(); 

    public int ID { get; set; } 
    public int LocationID { get; set; } 
    public string Name { get; set; } 

    public void Add(LocationNode child) { 
     child.Parent = this; 
     this.Children.Add(child); 
    } 

    public LocationNode Get(int id) { 
     LocationNode result; 
     foreach (LocationNode child in this.Children) { 
      if (child.ID == id) { 
       return child; 
      } 
      result = child.Get(id); 
      if (result != null) { 
       return result; 
      } 
     } 
     return null; 
    } 
} 

Ahora querrá poblar su árbol. Aquí tienes un problema: es difícil completar un árbol en el orden incorrecto. Antes de agregar un nodo secundario, realmente necesita una referencia al nodo primario. Si tiene para hacerlo fuera de servicio, puede mitigar el problema haciendo dos pasadas (una para crear todos los nodos, y luego otra para crear el árbol). Sin embargo, en este caso, eso es innecesario.

Si toma la consulta SQL que proporcioné arriba y ordena por la columna depth, puede estar matemáticamente seguro de que nunca encontrará un nodo secundario antes de encontrar su nodo principal. Por lo tanto, puedes hacer esto en una sola pasada.

Aún necesitará un nodo para que sirva como la "raíz" de su árbol. Tienes que decidir si esto será "House" (de tu ejemplo), o si es un nodo ficticio de marcador de posición que creas solo para este propósito. Sugiero el más tarde.

¡Así que, al código! De nuevo, esto está optimizado para simplificar y facilitar la lectura. Hay algunos problemas de rendimiento que quizás desee abordar en el código de producción (por ejemplo, no es realmente necesario buscar constantemente el nodo "primario"). Evité estas optimizaciones porque aumentan la complejidad.

// Create the root of the tree. 
LocationNode root = new LocationNode(); 

using (SqlCommand cmd = new SqlCommand()) { 
    cmd.Connection = conn; // your connection object, not shown here. 
    cmd.CommandText = "The above query, ordered by [Depth] ascending"; 
    cmd.CommandType = CommandType.Text; 
    using (SqlDataReader rs = cmd.ExecuteReader()) { 
     while (rs.Read()) { 
      int id = rs.GetInt32(0); // ID column 
      var parent = root.Get(id) ?? root; 
      parent.Add(new LocationNode { 
       ID = id, 
       LocationID = rs.GetInt32(1), 
       Name = rs.GetString(2) 
      }); 
     } 
    } 
} 

Ta-da! El LocationNode root ahora contiene toda su jerarquía. Por cierto, no he ejecutado este código, así que avíseme si detecta algún problema evidente.


Editar 2

para fijar su código de ejemplo, hacer estos cambios:

elimine esta línea:

// Create an instance of the tree 
TreeView t1 = new TreeView(); 

Esta línea no es realmente un problema, pero debería ser removido. Sus comentarios aquí son inexactos; en realidad no estás asignando un árbol al control. En su lugar, está creando un nuevo TreeView, asignándolo al t1, y luego asignando un objeto diferente al t1. El TreeView que crea se pierde tan pronto como se ejecuta la siguiente línea.

arreglar su instrucción SQL

// SQL Commands 
string getLocations = "SELECT ID, LocationID, Name FROM dbo.Locations"; 

Reemplazar esta sentencia SQL con la instrucción SQL que he sugerido anteriormente, con una cláusula ORDER BY. Lea mi edición anterior que explica por qué la "profundidad" es importante: realmente desea agregar los nodos en un orden particular. No puede agregar un nodo secundario hasta que tenga el nodo principal.

Opcionalmente, creo que no es necesario la sobrecarga de un SqlDataAdapter y DataTable aquí. La solución DataReader que originalmente sugerí es más simple, más fácil de trabajar y más eficiente en términos de recursos.

Además, la mayoría de los objetos de C# SQL implementan IDisposable, por lo que tendrá que asegurarse de que está utilizando correctamente. Si algo implementa IDisposable, estar seguro de que lo envuelve en using declaraciones (véase mi C# código de ejemplo anterior).

arreglar su árbol de fomento de bucle

Usted está consiguiendo solamente los nodos padre e hijo porque tiene un lazo para los padres y un bucle interno para los niños. Como ya debe saber, no recibirá los nietos porque no tiene un código que los agregue.

Se podría añadir un bucle interior-interior para obtener los nietos, pero está claro que está pidiendo ayuda porque se ha dado cuenta de que al hacerlo sólo conducirá a la locura. ¿Qué pasaría si luego quisieras los bisnietos? ¿Un bucle interno-interno-interno? Esta técnica no es viable.

es probable que haya pensado en la recursividad aquí. Este es un lugar perfecto para ello, y si se trata de estructuras similares a árboles, eventualmente surgirá. Ahora que ha editado su pregunta, está claro que su problema tiene poco o nada que ver con SQL. Tu verdadero problema es con la recursividad. Alguien eventualmente puede venir e idear una solución recursiva para esto. Ese sería un enfoque perfectamente válido, y posiblemente preferible.

Sin embargo, mi respuesta ya ha cubierto la parte recursiva - simplemente se ha movido en la capa de SQL. Por lo tanto, mantendré mi código anterior, ya que creo que es una respuesta genérica adecuada a la pregunta. Para su situación específica, necesitará hacer algunas modificaciones más.

En primer lugar, usted no necesitará la clase LocationNode que he sugerido. Está utilizando TreeNode en su lugar, y eso funcionará bien.

En segundo lugar, el TreeView.FindNode es similar al método LocationNode.Get que sugerí, excepto que FindNode requiere la ruta completa al nodo. Para usar FindNode, debe modificar el SQL para darle esta información.

Por lo tanto, toda la función PopulateTree debería tener este aspecto:

public void PopulateTree(TreeView t1) { 

    // Clear any exisiting nodes 
    t1.Nodes.Clear(); 

    using (SqlConnection connection = new SqlConnection()) { 
     connection.ConnectionString = "((replace this string))"; 
     connection.Open(); 

     string getLocations = @" 
      With hierarchy (id, [location id], name, depth, [path]) 
      As (

       Select ID, [LocationID], Name, 1 As depth, 
        Cast(Null as varChar(max)) As [path] 
       From dbo.Locations 
       Where ID = [LocationID] 

       Union All 

       Select child.id, child.[LocationID], child.name, 
        parent.depth + 1 As depth, 
        IsNull(
         parent.[path] + '/' + Cast(parent.id As varChar(max)), 
         Cast(parent.id As varChar(max)) 
        ) As [path] 
       From dbo.Locations As child 
       Inner Join hierarchy As parent 
        On child.[LocationID] = parent.ID 
       Where child.ID != parent.[Location ID]) 

      Select * 
      From hierarchy 
      Order By [depth] Asc"; 

     using (SqlCommand cmd = new SqlCommand(getLocations, connection)) { 
      cmd.CommandType = CommandType.Text; 
      using (SqlDataReader rs = cmd.ExecuteReader()) { 
       while (rs.Read()) { 
        // I guess you actually have GUIDs here, huh? 
        int id = rs.GetInt32(0); 
        int locationID = rs.GetInt32(1); 
        TreeNode node = new TreeNode(); 
        node.Text = rs.GetString(2); 
        node.Value = id.ToString(); 

        if (id == locationID) { 
         t1.Nodes.Add(node); 
        } else { 
         t1.FindNode(rs.GetString(4)).ChildNodes.Add(node); 
        } 
       } 
      } 
     } 
    } 
} 

Por favor, hágamelo saber si usted encuentra algún error adicionales!

+0

Excelente, esto es lo que estaba buscando. ¡Gracias! Sin embargo, una pregunta más. ¿Cómo sé qué niño pertenece a qué padre? Para desarrollar la vista en árbol de estos datos, necesitaré conocer la relación. – Will

+0

¿Hay alguna manera de lograr esta misma tarea usando ASP.NET C#, para poder construir el árbol al mismo tiempo que consulto los datos? – Will

+0

He incluido algún ejemplo de código C# que hace uso de la consulta CTE para construir una estructura tipo árbol C#. ¿Es esto lo que estás buscando? –

0

Hay una nueva característica en SQl 2008. Es hierarchyid. Esta característica me hace la vida más fácil.

Son útiles method for hierarchyid datatype, GetAncestor(), GetRoot() ... Disminuirá la complejidad de la consulta una vez que trabajo en la jerarquía.

Cuestiones relacionadas