Иерархия данных SQL

Я просмотрел несколько учебников по иерархии SQL, но ни один из них не имел большого смысла для моего приложения. Возможно, я просто не понимаю их правильно. Я пишу приложение С# ASP.NET, и я хотел бы создать иерархию древовидных представлений из данных SQL.

Так будет работать иерархия:

SQL TABLE

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

Если идентификатор и идентификатор местоположения совпадают, это будет определять главный родитель. У всех детей этого родителя будет такой же идентификатор местоположения, что и у родителя. Любые внуки этого ребенка имеют идентификатор местоположения, равный ID ребенка, и т.д.

В приведенном выше примере:

- House
   -- Room
       --- Bed

Любая помощь или руководство, чтобы легко следовать учебникам было бы очень признателен.

EDIT:

Код, который у меня есть до сих пор, но он получает только родителя и детей, а не GrandChildren. Я не могу понять, как заставить его рекурсивно получить все узлы.

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

}
}

Вот фрагмент из таблицы SQL: Locations

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

Спасибо.

Ответ 1

Вы ищете рекурсивный запрос, используя общее табличное выражение, или CTE для краткости. Подробное описание для этого в SQL Server 2008 может быть найдено в MSDN.

В общем, они имеют структуру, похожую на следующее:

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

Когда это выполняется, SQL Server сделает что-то похожее на следующее (перефразированное на более простой язык из MSDN):

  • Разделите выражение CTE на элементы привязки и рекурсии.
  • Запустите якорь, создав первый набор результатов.
  • Запустите рекурсивную часть с предыдущим шагом в качестве ввода.
  • Повторите шаг 3 до тех пор, пока не будет возвращен пустой набор.
  • Возвращает набор результатов. Это СОЕДИНЕНИЕ ВСЕХ якорей и всех рекурсивных шагов.

В этом конкретном примере попробуйте что-то вроде этого:

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

Учитывая ваши данные примера, вы должны получить что-то вроде этого:

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

Обратите внимание, что исключается "Тренажер". Основываясь на ваших образцовых данных, идентификатор не соответствует его [Идентификатору местоположения], поэтому он не будет элементом с корневым уровнем. Идентификатор местоположения, 2231, не отображается в списке действительных идентификаторов родителя.


Изменить 1:

Вы спросили о том, как это сделать в структуру данных С#. Существует множество различных способов представления иерархии на С#. Вот один пример, выбранный по своей простоте. Без сомнения, реальный образец кода будет более обширным.

Первый шаг - определить, как выглядит каждый node в иерархии. Помимо свойств свойств для каждой базы данных в node, я включил свойства Parent и Children, плюс методы для Add дочернего элемента и Get дочернего элемента. Метод Get будет выполнять поиск по всей оси потомков node, а не только для собственных детей node.

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

Теперь вы хотите заполнить свое дерево. У вас есть проблема: трудно заполнить дерево в неправильном порядке. Прежде чем добавить дочерний элемент node, вам действительно нужна ссылка на родительский node. Если вы имеете, чтобы сделать это не по порядку, вы можете уменьшить проблему, выполнив два прохода (один для создания всех узлов, а затем другой для создания дерева). Однако в этом случае это необязательно.

Если вы берете SQL-запрос, указанный выше, и заказываете столбец depth, вы можете быть математически уверены, что никогда не столкнетесь с дочерним node, прежде чем вы столкнетесь с его родительским node. Поэтому вы можете сделать это за один проход.

Вам все равно понадобится node, чтобы служить "корнем" вашего дерева. Вы решаете, будет ли это "Дом" (из вашего примера), или это вымышленный заполнитель node, который вы создаете именно для этой цели. Я предлагаю позже.

Итак, к коду! Опять же, это оптимизировано для простоты и удобочитаемости. Есть некоторые проблемы с производительностью, которые вы можете захотеть решить в производственном коде (например, нет необходимости постоянно искать "родительский" node). Я избегал этих оптимизаций здесь, потому что они увеличивают сложность.

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

Та-да! root LocationNode теперь содержит всю вашу иерархию. Кстати, я на самом деле не выполнил этот код, поэтому, пожалуйста, дайте мне знать, если вы заметите какие-либо вопиющие проблемы.


Изменить 2

Чтобы исправить код примера, внесите следующие изменения:

Удалить эту строку:

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

Эта строка не является проблемой, но ее нужно удалить. Ваши комментарии здесь неточны; вы на самом деле не назначаете дерево элементу управления. Вместо этого вы создаете новый TreeView, назначая его t1, а затем сразу назначая другой объект t1. Созданный TreeView теряется, как только выполняется следующая строка.

Исправить ваш оператор SQL

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

Замените эту инструкцию SQL выражением SQL, которое я предложил ранее, с предложением ORDER BY. Прочтите мои предыдущие изменения, которые объясняют, почему важна "глубина": вы действительно хотите добавить узлы в определенном порядке. Вы не можете добавить дочерний элемент node, пока не будет родительский node.

По желанию, я думаю, что здесь вам не нужны накладные расходы SqlDataAdapter и DataTable. Первоначально предлагаемое решение DataReader было проще, проще в работе и более эффективно с точки зрения ресурсов.

Кроме того, большинство объектов С# SQL реализуют IDisposable, поэтому вы захотите убедиться, что используете их правильно. Если что-то реализует IDisposable, обязательно заверните его в инструкции using (см. Пример предыдущего кода С#).

Исправьте цикл построения дерева

Вы получаете только родительский и дочерний узлы, потому что у вас есть петля для родителей и внутренний цикл для детей. Как вы уже должны знать, вы не получаете внуков, потому что у вас нет кода, который их добавляет.

Вы могли бы добавить внутренний внутренний цикл, чтобы получить внуков, но ясно, что вы просите о помощи, потому что вы поняли, что это приведет только к безумию. Что произойдет, если вы тогда захотите правнуков? Внутренняя внутренняя внутренняя петля? Этот метод не является жизнеспособным.

Вероятно, вы подумали о рекурсии. Это идеальное место для него, и если вы имеете дело с древовидными структурами, это в конечном итоге появится. Теперь, когда вы отредактировали свой вопрос, ясно, что у вашей проблемы мало что может быть с SQL. Ваша настоящая проблема заключается в рекурсии. Кто-то может в конце концов прийти и разработать для этого рекурсивное решение. Это был бы совершенно правильный и, возможно, предпочтительный подход.

Однако мой ответ уже рассмотрел рекурсивную часть - он просто переместил ее в слой SQL. Поэтому я сохраню свой предыдущий код, поскольку я считаю, что это подходящий общий ответ на вопрос. Для вашей конкретной ситуации вам нужно сделать еще несколько изменений.

Сначала вам не понадобится класс LocationNode, который я предложил. Вместо этого вы используете TreeNode, и это будет работать нормально.

Во-вторых, TreeView.FindNode похож на метод LocationNode.Get, который я предложил, за исключением того, что FindNode требует полного пути к node. Чтобы использовать FindNode, вы должны изменить SQL, чтобы предоставить вам эту информацию.

Поэтому вся ваша функция PopulateTree должна выглядеть так:

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

Пожалуйста, дайте мне знать, если вы найдете дополнительные ошибки!

Ответ 4

В SQl 2008 появилась новая функция. Это иерархия. Эта функция облегчает мою жизнь.

Существует полезный метод для типа данных иерархии, GetAncestor(), GetRoot()... Это уменьшит сложность запроса, когда я буду работать над иерархией.