Иерархические данные в Linq - параметры и производительность
У меня есть некоторые иерархические данные - каждая запись имеет идентификатор родительской записи id и (nullable).
Я хочу получить все записи в дереве под данной записью. Это находится в базе данных SQL Server 2005. Я запрашиваю его с LINQ to SQL в С# 3.5.
LINQ to SQL не поддерживает Общие выражения таблицы напрямую. Мой выбор состоит в том, чтобы собрать данные в коде с несколькими запросами LINQ или сделать представление о базе данных, которая покрывает CTE.
Какой вариант (или другой вариант), по вашему мнению, будет лучше работать при больших объемах данных?
Является ли SQL Server 2008 HierarchyId type поддерживается в Linq to SQL?
Ответы
Ответ 1
Я бы создал представление и связанную с таблицей функцию на основе CTE. Мое рассуждение заключается в том, что, хотя вы можете реализовать логику на стороне приложения, это будет связано с отправкой промежуточных данных по проводу для вычисления в приложении. Используя конструктор DBML, представление преобразуется в объект таблицы. Затем вы можете связать функцию с объектом таблицы и вызвать метод, созданный в 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...
}
}
Ответ 2
Этот параметр также может оказаться полезным:
Метод расширения LINQ AsHierarchy()
http://www.scip.be/index.php?Page=ArticlesNET18
Ответ 3
Я удивлен, что никто не упомянул альтернативный дизайн базы данных - когда иерархию нужно сгладить с нескольких уровней и получить с высокой производительностью (не учитывая пространство для хранения), лучше использовать другую таблицу сущность-2-сущность для отслеживания иерархии вместо подхода 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)
);
Ответ 4
Я сделал два способа:
- Воспроизводить каждый слой дерева на основе пользовательского ввода. Представьте, что элемент управления древовидной структурой заполнен корнем node, дочерними элементами корня и внуками корня. Расширяются только корень и дети (внуки скрыты с крахом). По мере того, как пользователь расширяет дочерний элемент node, внуки корня отображаются (которые ранее были извлечены и скрыты), и начинается поиск всех правнуков. Повторите шаблон для глубоких слоев N. Этот шаблон работает очень хорошо для больших деревьев (глубина или ширина), потому что он только извлекает часть необходимого дерева.
- Используйте хранимую процедуру с LINQ. Используйте что-то вроде обычного табличного выражения на сервере для построения результатов в плоской таблице или построения дерева XML в T-SQL. У Скотта Гатри отличная статья об использовании хранимых procs в LINQ. Создайте свое дерево из результатов, когда они вернутся, если в плоском формате, или используйте дерево XML, если это то, что вы возвращаете.
Ответ 5
Этот метод расширения потенциально может быть изменен для использования 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)
Этот код является улучшенной, исправленной ошибкой версией кода, найденного здесь.
Ответ 6
В MS SQL 2008 вы можете напрямую использовать HierarchyID, в sql2005 вам, возможно, придется реализовать их вручную. ParentID не работает на больших наборах данных. Также проверьте эту статью для более подробного обсуждения темы.
Ответ 7
Я получил этот подход от Блог Роба Конира (отметьте около 6 для этого кода, также на codeplex), и я люблю его использовать, Это может быть изменено для поддержки нескольких уровней "sub".
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
}
)
};
Ответ 8
Проблема с извлечением данных с клиентской стороны заключается в том, что вы никогда не можете быть уверены в том, насколько глубоко вам нужно идти. Этот метод будет делать одно округление на глубину, и его можно было бы объединить с 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;
}
Однако он не может выполнять произвольную глубину. Если вам действительно нужна произвольная глубина, вам нужно сделать это в базе данных, чтобы вы могли принять правильное решение для остановки.
Ответ 9
Прочтите следующую ссылку.
http://support.microsoft.com/default.aspx?scid=kb;en-us;q248915