Иерархические данные в Linq - параметры и производительность
-
03-07-2019 - |
Вопрос
У меня есть некоторые иерархические данные - каждая запись имеет идентификатор и (обнуляемый) идентификатор родительской записи.Я хочу получить все записи в дереве под данной записью.Это находится в базе данных SQL Server 2005.Я запрашиваю его с помощью LINQ to SQL на C # 3.5.
LINQ для SQL не поддерживает Общие Табличные выражения напрямую.Мой выбор - собрать данные в коде с помощью нескольких запросов LINQ или создать представление в базе данных, которое отображает CTE.
Какой вариант (или другой вариант), по вашему мнению, будет работать лучше, когда объемы данных станут большими?Является ли SQL Server 2008 Тип HierarchyId поддерживается в Linq to SQL?
Решение
Я бы настроил представление и связанную с ним табличную функцию на основе CTE.Мои доводы в пользу этого заключаются в том, что, хотя вы могли бы реализовать логику на стороне приложения, это потребовало бы отправки промежуточных данных по проводам для вычислений в приложении.Используя DBML designer, представление преобразуется в объект таблицы.Затем вы можете связать функцию с объектом 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 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)
);
Я сделал это двумя способами:
- Управляйте извлечением каждого слоя дерева на основе введенных пользователем данных.Представьте элемент управления древовидным представлением, заполненный корневым узлом, дочерними элементами корня и внуками корня.Расширены только корень и дети (внуки скрыты при сворачивании).Когда пользователь расширяет дочерний узел, отображаются внуки корневого узла (которые были ранее извлечены и скрыты), и запускается поиск всех правнуков.Повторите процедуру для N слоев в глубину.Этот шаблон очень хорошо работает для больших деревьев (глубины или ширины), потому что он извлекает только необходимую часть дерева.
- Используйте хранимую процедуру с LINQ.Используйте что-то вроде общего табличного выражения на сервере для построения результатов в виде плоской таблицы или создайте XML-дерево в T-SQL.У Скотта Гатри есть отличная статья об использовании сохраненных процедур в LINQ.Постройте свое дерево на основе результатов, когда они вернутся, если они в плоском формате, или используйте XML-дерево, если это то, что вы возвращаете.
Этот метод расширения потенциально может быть изменен для использования 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)
Этот код является улучшенной версией найденного кода с исправленными ошибками здесь.
В MS SQL 2008 вы могли бы использовать Иерархический идентификатор непосредственно в sql2005 вам, возможно, придется реализовать их вручную.ParentID не настолько эффективен для больших наборов данных.Также проверьте эта статья для более подробного обсуждения этой темы.
Я перенял этот подход у Блог Роба Конери (проверьте по всему Пт.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;
}
Однако он не может создавать произвольную глубину.Если вам действительно требуется произвольная глубина, вам нужно сделать это в базе данных - так вы сможете принять правильное решение остановиться.
Пожалуйста, прочтите следующую ссылку.
http://support.microsoft.com/default.aspx?scid=kb ;ru-США;q248915