Вы ищете рекурсивный запрос с использованием общего табличного выражения или 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:
Вы спрашивали о включении этого в структуру данных 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
, вы можете быть математически уверены, что никогда не встретите дочерний узел, пока не встретите его родительский узел. Следовательно, вы можете сделать это за один проход.
Вам все еще понадобится узел, который будет служить «корнем» вашего дерева. Вы сами решаете, будет ли это «Дом» (из вашего примера), или это фиктивный узел-заполнитель, который вы создадите именно для этой цели. Я предлагаю позже.
Итак, к коду! Опять же, это оптимизировано для простоты и удобочитаемости. Есть некоторые проблемы с производительностью, которые вы, возможно, захотите устранить в производственном коде (например, нет необходимости постоянно искать «родительский» узел). Я избегал этих оптимизаций здесь, потому что они увеличивают сложность.
// 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
. Созданное вами древовидное представление теряется при выполнении следующей строки.
Исправьте оператор 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);
}
}
}
}
}
}
Пожалуйста, дайте мне знать, если найдете какие-либо дополнительные ошибки!