Иерархия данных SQL
Я просмотрел несколько руководств по иерархии SQL, но ни один из них не имел большого смысла для моего приложения. Возможно, я просто неправильно их понимаю. Я пишу C# ASP.NET приложение и я хотели бы создать иерархию древовидного представления из данных SQL.
вот как будет работать иерархия:
SQL TABLE ID | Location ID | Name _______| __________ |_____________ 1331 | 1331 | House 1321 | 1331 | Room 2141 | 1321 | Bed 1251 | 2231 | Gym
если идентификатор и идентификатор местоположения одинаковы, это определит верхний родитель. Любые дети этого родителя будут иметь тот же идентификатор местоположения, что и Родитель. Любые внуки этого ребенка будут иметь идентификатор местоположения, равный идентификатору ребенка, и так далее.
для приведенного выше примера:
- House -- Room --- Bed
любая помощь или направление, чтобы легко следовать учебники будет принята с благодарностью.
EDIT:
код у меня пока есть, но он получает только родителей и детей, внуков нет. Я не могу понять, как заставить его рекурсивно получить все узлы.
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
спасибо.
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):
- разделить выражение 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
обратите внимание, что" тренажерный зал " исключен. Основываясь на ваших образцах данных, его ID не соответствует его [Location ID], поэтому он не будет элементом корневого уровня. Это идентификатор местоположения, 2231, не отображается в списке допустимых родительских идентификаторов.
Edit 1:
вы спросили о том, чтобы получить это в структуру данных c#. Существует множество различных способов представления иерархии в C#. Вот один пример, выбранный за свою простоту. Реальный пример кода без сомнения, будет более обширным.
первый шаг-определить, как выглядит каждый узел в иерархии. Помимо свойств для каждого datum в узле, я включил Parent
и Children
свойства, а также методы для Add
ребенка и Get
ребенок. The 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;
}
}
теперь вы захотите заселить свое дерево. У вас есть проблема здесь: это трудно заселить дерево в неправильном порядке. Перед добавлением дочернего узла вам действительно нужна ссылка на родительский узел. Если ты ... --61-->есть чтобы сделать это не по порядку, вы можете смягчить проблему, сделав два прохода (один для создания всех узлов, а другой для создания дерева). Однако, в данном случае это unnecessay.
если вы берете SQL-запрос, который я предоставил выше, и заказываете , вы можете быть математически уверены, что вы никогда не будете встречайте дочерний узел до встречи с родительским узлом. Поэтому вы можете сделать это за один проход.
вам все равно понадобится узел, который будет служить "корнем" вашего дерева. Вы можете решить, будет ли это "дом" (из вашего примера), или это вымышленный узел-заполнитель, который вы создаете только для этой цели. Я предлагаю позже.
Итак, к коду! Опять же, это оптимизировано для простоты и удобочитаемости. Есть некоторые проблемы с производительностью, которые вы может потребоваться адрес в производственном коде (например, на самом деле нет необходимости постоянно искать "родительский" узел). Я избегал этих оптимизаций здесь, потому что они увеличивают сложность.
// 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)
});
}
}
}
та-да! The 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, которое я первоначально предложил проще, легче работать и эффективнее с точки зрения ресурсов.
кроме того, большинство объектов SQL C# реализуют IDisposable
, поэтому вы захотите убедиться, что используете их правильно. Если что-то реализует IDisposable
, обязательно заверните его в using
операторы (см. мой предыдущий пример кода C#).
исправьте петлю дерева
вы получаете только родительские и дочерние узлы, потому что у вас есть цикл для родителей и внутренний цикл для детей. Как вы уже должны знать, вы не получаете внуков, потому что у вас нет кода, который добавляет их.
вы можете добавить внутренний-внутренний цикл, чтобы получить внуков, но, очевидно, вы просите о помощи, потому что поняли, что это приведет только к безумию. Что было бы, если бы вы захотели правнуков? Внутренняя-внутренняя-внутренняя петля? Эта техника не жизнеспособна.
вы, вероятно, подумали о рекурсии здесь. Это идеальное место для этого, и если вы имеете дело с древовидными структурами, в конце концов это всплывет. Теперь, когда вы отредактировали свой вопрос, ясно, что ваша проблема имеет мало общего, если что-либо, с SQL. Ваша настоящая проблема-рекурсия. Кто-то может в конце концов прийти и разработать рекурсивное решение для этого. Это был бы вполне обоснованный и, возможно, предпочтительный подход.
однако мой ответ уже охватил рекурсивную часть--it просто переместил его в слой 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);
}
}
}
}
}
}
пожалуйста, дайте мне знать, если вы обнаружите какие-либо дополнительные ошибки!
Я рекомендую вам также взглянуть на HierarchyId тип данных в SQL Server 2008, который дает вам много возможностей для перемещения и манипулирования структурой дерева. Вот учебник:
работа с типом данных SQL Server HierarchyId в приложении .NET
Извините, я просто просматриваю StackOverflow. Я видел ваш вопрос, и я чувствую, что написал статью, отвечающую на ваш вопрос три года назад. Пожалуйста, дайте мне знать, если это helps.
http://www.simple-talk.com/dotnet/asp.net/rendering-hierarchical-data-with-the-treeview/
в SQl 2008 появилась новая функция. Это hierarchyid. Эта функция делает мою жизнь проще.
есть метод для типа данных hierarchyid, GetAncestor (), GetRoot ()... Это уменьшит сложность запроса, как только я буду работать над иерархией.