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

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

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

SQL TABLE

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

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

Для приведенного выше примера:

- House
   -- Room
       --- Bed

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

ИЗМЕНИТЬ:

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

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

Спасибо.


person Will    schedule 17.05.2011    source источник
comment
Эти названия сбивают с толку. Я бы изменил Location ID на Parent_ID.   -  person FrustratedWithFormsDesigner    schedule 18.05.2011
comment
Для ответа на чистый SQL может потребоваться знать, какой вариант SQL вы используете. Можете ли вы указать, какая база данных у вас есть? SQL Server 2008, возможно?   -  person Chris Nielsen    schedule 18.05.2011
comment
К сожалению, я не могу изменить имена, от них зависит многое другое. Я использую SQL Server 2008.   -  person Will    schedule 18.05.2011
comment
Вы используете типы данных HierarchyID?   -  person goalie7960    schedule 18.05.2011


Ответы (4)


Вы ищете рекурсивный запрос с использованием общего табличного выражения или 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):

  1. Разделите выражение CTE на элементы привязки и рекурсии.
  2. Запустите якорь, создав первый результирующий набор.
  3. Запустите рекурсивную часть с предыдущим шагом в качестве входных данных.
  4. Повторяйте шаг 3, пока не будет возвращен пустой набор.
  5. Вернуть результирующий набор. Это UNION ALL якоря и всех рекурсивных шагов.

Для этого конкретного примера попробуйте что-то вроде этого:

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:

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

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

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

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

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

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

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

// 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. Прочтите мое предыдущее редактирование, в котором объясняется, почему важна глубина: вы действительно хотите добавлять узлы в определенном порядке. Вы не можете добавить дочерний узел, пока у вас нет родительского узла.

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

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

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

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

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

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

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

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

Во-вторых, TreeView.FindNode похож на метод LocationNode.Get, который я предложил, за исключением того, что FindNode требует полного пути к узлу. Чтобы использовать 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);
                    }
                }
            }
        }
    }
}

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

person Chris Nielsen    schedule 17.05.2011
comment
Отлично, это то, что я искал. Спасибо! Однако еще один вопрос. Как узнать, какой ребенок принадлежит какому родителю? Чтобы разработать древовидное представление этих данных, мне нужно знать отношение. - person Will; 18.05.2011
comment
Есть ли способ выполнить ту же задачу с помощью ASP.NET C#, чтобы я мог построить дерево одновременно с запросом данных? - person Will; 20.05.2011
comment
Я включил пример кода C#, который использует запрос CTE для построения древовидной структуры C#. Это то, что вы ищите? - person Chris Nielsen; 20.05.2011
comment
Это то, что я искал. У меня есть некоторые проблемы с компиляцией. Будет ли этот код работать с проектом ASP.NET C#? - person Will; 20.05.2011
comment
Да, я думаю, это должно сработать. Я не выполнял его, но я проверил его синтаксическую правильность. Если хотите, попробуйте отредактировать свой вопрос, указав более подробную информацию о том, что у вас есть, и я могу попытаться вам помочь. В качестве альтернативы, если вы чувствуете, что понимаете сторону SQL в иерархии и застряли на стороне C#, вы можете рассмотреть это как отдельный вопрос и создать новую публикацию SO. - person Chris Nielsen; 20.05.2011
comment
Хорошо, я выясню, что компилятор спотыкается с вашим кодом. Я добавлю код, который написал до того, как получил ваш ответ на свой первоначальный пост. Я также добавлю выдержку из фактической таблицы, которую я использую. - person Will; 21.05.2011
comment
Я обновил свой ответ, чтобы обратиться к вашему конкретному образцу кода. Удачи! - person Chris Nielsen; 22.05.2011
comment
Это был невероятно полезный пост, большое спасибо за вашу помощь. Единственная ошибка, которую я получаю во время компиляции, заключается в том, что ссылка на объект не указывает на экземпляр объекта по отношению к следующей строке кода: t1.FindNode(rs.GetString(4)).ChildNodes.Add(node); - person Will; 23.05.2011
comment
@ChrisNielsen - отличная запись. Я считаю, что в вашем первом примере установки значений узлов из sql: 'var parent = root.Get(id) ?? root; следует использовать идентификатор местоположения, а не текущий идентификатор: var parent = root.Get(rs.GetInt32(1)) ?? root; - person Metro Smurf; 21.08.2012

Я рекомендую вам также взглянуть на тип данных HierarchyId, представленный в SQL. Server 2008, который дает вам множество возможностей для обхода и управления древовидной структурой. Вот учебник:

Работа с типом данных SQL Server HierarchyId In Приложение .NET

person Alireza Maddah    schedule 22.05.2011

Извините, я просто просматриваю StackOverflow. Я видел ваш вопрос, и мне кажется, что я написал статью, отвечающую на ваш вопрос, три года назад. Пожалуйста, дайте мне знать, если это поможет.
http://www.simple-talk.com/dotnet/asp.net/rendering-hierarchical-data-with-the-treeview/

person Ion Freeman    schedule 14.02.2012

В SQl 2008 появилась новая функция. Это hierarchyid. Эта функция делает мою жизнь проще.

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

person Pongsathon.keng    schedule 14.02.2012