У меня есть некоторые иерархические данные - каждая запись имеет идентификатор и (обнуляемый) идентификатор родительской записи. Я хочу получить все записи в дереве под данной записью. Это в базе данных SQL Server 2005. Я запрашиваю его с помощью LINQ to SQL в C# 3.5.
LINQ to SQL не поддерживает Общие табличные выражения напрямую. Мой выбор - собрать данные в коде с помощью нескольких запросов LINQ или создать представление базы данных, отображающее CTE.
Как вы думаете, какой вариант (или другой вариант) будет работать лучше при увеличении объемов данных? Поддерживается ли Тип HierarchyId SQL Server 2008 в Linq to SQL?


В MS SQL 2008 вы можете использовать HierarchyID напрямую, в sql2005 вам, возможно, придется реализовать их вручную. ParentID не так эффективен для больших наборов данных. Также проверьте эта статья для более подробного обсуждения темы.
его нельзя использовать в linq2sql из коробки
Я бы настроил представление и связанную с ним табличную функцию на основе CTE. Я считаю, что, хотя вы можете реализовать логику на стороне приложения, это потребует отправки промежуточных данных по сети для вычислений в приложении. Используя конструктор DBML, представление преобразуется в сущность таблицы. Затем вы можете связать функцию с сущностью Table и вызвать метод, созданный в DataContext, для получения объектов типа, определенного представлением. Использование табличной функции позволяет механизму запросов учитывать ваши параметры при построении набора результатов, а не применять условие к набору результатов, определенному представлением постфактум.
CREATE TABLE [dbo].[hierarchical_table](
[id] [int] IDENTITY(1,1) NOT NULL,
[parent_id] [int] NULL,
[data] [varchar](255) NOT NULL,
CONSTRAINT [PK_hierarchical_table] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
CREATE VIEW [dbo].[vw_recursive_view]
AS
WITH hierarchy_cte(id, parent_id, data, lvl) AS
(SELECT id, parent_id, data, 0 AS lvl
FROM dbo.hierarchical_table
WHERE (parent_id IS NULL)
UNION ALL
SELECT t1.id, t1.parent_id, t1.data, h.lvl + 1 AS lvl
FROM dbo.hierarchical_table AS t1 INNER JOIN
hierarchy_cte AS h ON t1.parent_id = h.id)
SELECT id, parent_id, data, lvl
FROM hierarchy_cte AS result
CREATE FUNCTION [dbo].[fn_tree_for_parent]
(
@parent int
)
RETURNS
@result TABLE
(
id int not null,
parent_id int,
data varchar(255) not null,
lvl int not null
)
AS
BEGIN
WITH hierarchy_cte(id, parent_id, data, lvl) AS
(SELECT id, parent_id, data, 0 AS lvl
FROM dbo.hierarchical_table
WHERE (id = @parent OR (parent_id IS NULL AND @parent IS NULL))
UNION ALL
SELECT t1.id, t1.parent_id, t1.data, h.lvl + 1 AS lvl
FROM dbo.hierarchical_table AS t1 INNER JOIN
hierarchy_cte AS h ON t1.parent_id = h.id)
INSERT INTO @result
SELECT id, parent_id, data, lvl
FROM hierarchy_cte AS result
RETURN
END
ALTER TABLE [dbo].[hierarchical_table] WITH CHECK ADD CONSTRAINT [FK_hierarchical_table_hierarchical_table] FOREIGN KEY([parent_id])
REFERENCES [dbo].[hierarchical_table] ([id])
ALTER TABLE [dbo].[hierarchical_table] CHECK CONSTRAINT [FK_hierarchical_table_hierarchical_table]
Чтобы использовать его, вы должны сделать что-то вроде - при условии некоторой разумной схемы именования:
using (DataContext dc = new HierarchicalDataContext())
{
HierarchicalTableEntity h = (from e in dc.HierarchicalTableEntities
select e).First();
var query = dc.FnTreeForParent( h.ID );
foreach (HierarchicalTableViewEntity entity in query) {
...process the tree node...
}
}
Я пробовал такую функцию, и, похоже, это правильный путь. И его можно вызвать из LINQ, прикрепленного к тексту данных. Кроме того, почему и представление, и функция? - они кажутся дублированием
Функция не отображает ту же схему, что и таблица. Он включает в себя уровень. Если у вас не было добавленного столбца, вы могли бы сопоставить его прямо с таблицей. Я считал, что уровень в иерархии важен.
Я сделал это двумя способами:
Я был очень увяз в поиске решения этой проблемы, когда ваш ответ заставил меня подумать о том, что мне не нужно тянуть целое дерево, просто тянуть детей, когда это необходимо.
Этот метод расширения потенциально может быть изменен для использования IQueryable. Я успешно использовал его в прошлом для коллекции объектов. Это может сработать для вашего сценария.
public static IEnumerable<T> ByHierarchy<T>(
this IEnumerable<T> source, Func<T, bool> startWith, Func<T, T, bool> connectBy)
{
if (source == null)
throw new ArgumentNullException("source");
if (startWith == null)
throw new ArgumentNullException("startWith");
if (connectBy == null)
throw new ArgumentNullException("connectBy");
foreach (T root in source.Where(startWith))
{
yield return root;
foreach (T child in source.ByHierarchy(c => connectBy(root, c), connectBy))
{
yield return child;
}
}
}
Вот как я это назвал:
comments.ByHierarchy(comment => comment.ParentNum == parentNum,
(parent, child) => child.ParentNum == parent.CommentNum && includeChildren)
Этот код является улучшенной версией кода здесь с исправленными ошибками.
Или вы можете проверить, откуда он это взял: weblogs.asp.net/okloeten/archive/2006/07/09/…
Я добавил указание на джедаев. Моя версия упрощена и улучшена.
Я получил этот подход от Блог Роба Конери (проверьте Pt.6 для этого кода, также на codeplex), и мне нравится его использовать. Это может быть изменено для поддержки нескольких «подуровней».
var categories = from c in db.Categories
select new Category
{
CategoryID = c.CategoryID,
ParentCategoryID = c.ParentCategoryID,
SubCategories = new List<Category>(
from sc in db.Categories
where sc.ParentCategoryID == c.CategoryID
select new Category {
CategoryID = sc.CategoryID,
ParentProductID = sc.ParentProductID
}
)
};
Но можно ли его переделать для поддержки неограниченного количества подуровней?
Вы не собираетесь добавлять в этот запрос дюжину подкатегорий - он не особенно гибкий.
Проблема с получением данных со стороны клиента заключается в том, что вы никогда не можете быть уверены, насколько глубоко вам нужно зайти. Этот метод будет выполнять один обход на глубину, и его можно объединить для выполнения от 0 до указанной глубины за один обход.
public IQueryable<Node> GetChildrenAtDepth(int NodeID, int depth)
{
IQueryable<Node> query = db.Nodes.Where(n => n.NodeID == NodeID);
for(int i = 0; i < depth; i++)
query = query.SelectMany(n => n.Children);
//use this if the Children association has not been defined
//query = query.SelectMany(n => db.Nodes.Where(c => c.ParentID == n.NodeID));
return query;
}
Однако он не может делать произвольную глубину. Если вам действительно нужна произвольная глубина, вам нужно сделать это в базе данных, чтобы вы могли принять правильное решение остановиться.
Этот вариант также может оказаться полезным:
Метод расширения LINQ AsHierarchy ()
http://www.scip.be/index.php?Page=ArticlesNET18
Я использую это, и он работает очень хорошо, особенно обновленная версия, указанная внизу.
Я удивлен, что никто не упомянул альтернативный дизайн базы данных - когда иерархию необходимо выровнять с нескольких уровней и извлекать с высокой производительностью (не учитывая пространство для хранения), лучше использовать другую таблицу entity-2-entity для отслеживания иерархии вместо parent_id подход.
Это позволит не только отношения с одним родителем, но и отношения с несколькими родителями, указание уровня и различные типы отношений:
CREATE TABLE Person (
Id INTEGER,
Name TEXT
);
CREATE TABLE PersonInPerson (
PersonId INTEGER NOT NULL,
InPersonId INTEGER NOT NULL,
Level INTEGER,
RelationKind VARCHAR(1)
);
Пожалуйста, прочтите следующую ссылку.
http://support.microsoft.com/default.aspx?scid=kb;en-us;q248915
Мне не нравится этот метод - циклы «while» - не очень хорошая практика SQL, и если есть более декларативный способ сделать это, то ему следует отдать предпочтение. И теперь: используйте функцию на основе представления или таблицы, используя Common Table Expression, используя конструкцию WITH .. UNION ALL, как показано в других ответах здесь.
Пожалуйста, подумайте о том, чтобы вставить отрывок из решения на страницу, на которую вы ссылаетесь. Ссылки могут когда-нибудь умереть.
Там не упоминается, можно ли использовать HierarchyID в LINQ to SQL.