构建 LINQ 表达式以查找与树节点的所有后代相关的项目

Posted

技术标签:

【中文标题】构建 LINQ 表达式以查找与树节点的所有后代相关的项目【英文标题】:Building a LINQ Expression to find items related to all descendants of a tree node 【发布时间】:2011-03-07 21:37:02 【问题描述】:

情景

我已经构建了一个表示类别树的数据库结构,以帮助对我们存储的一些数据进行分类。实现是Category 表中的每条记录都有一个可以为空的外键回到Category 表中,以表示该类别的父Category(一对多),本质上允许在更广泛的父中包含子类别等级。有一个CategoryMembership 表将Item 表中的记录链接到其各自的Category(多对多)。我已经为这个数据库创建了 DBML,它有一个成员访问结构,包括以下内容:

Dim aCategory As New Category()
Dim aParentCategory As Category = aCategory.Parent
Dim aChildCategoryCollection As EntitySet(Of Category) = aCategory.Subcategories
Dim aMembershipCollection As EntitySet(Of CategoryMembership)  = aCategory.CategoryMemberships

aMembershipCollection 中的每个项目都具有以下成员访问结构:

Dim aMembership As CategoryMembership = aMembershipCollection.First()
Dim aLinkedCategory As Category = aMembership.Category
Dim aLinkedItem As Item = aMembership.Item

要求

我正在尝试构建一个 LINQ 表达式,它允许我确定哪些 Items 具有用于请求的 Category(即 aCategory.id = myID)的 CategoryMemberships 或请求的 Category 的后代的成员资格,我的想法是我想要父类别或其多个级别的子类别中的所有Items

基本上,查询将以类似于以下方式构建:

Dim results As IQueryable(Of Item) = _
    From cm In db.CategoryMemberships.Where(myInCategoryPredicate(myID)) _
    Select cm.Item

...myInCategoryPredicate 返回的 LINQ 表达式对象可以帮助我做出决定。这当然是假设CategoryMembership 表是开始检索IQueryable(Of Item) 的地方。我可能在这里做了一个错误的假设,这就是我来寻求建议的原因。

问题

我很难只见树木不见森林。我无法确定是否应该从 Category 或 CategoryMembership 开始构建谓词,也无法理解完成我想要的必要代码。我希望已经为数据库构建了类似树形结构的其他人能够帮助我浏览 DBML 类。

可用资源

我以前使用过PredicateBuilder,并且对它的工作原理比较熟悉,但我无法设计一种方法来向上遍历树并递归地构建一个谓词来指示一个项目是否属于所请求的类别或其子类别。到目前为止,我已经制作了以下内容,其中标记为 SomeRecursiveCall() 的差距非常明显:

Private Function InCategory(ByVal myID As Integer) As Expression(Of Func(Of CategoryMembership, Boolean))
    Dim predicate = PredicateBuilder.False(Of CategoryMembership)()

    predicate = predicate.Or(Function(cm) cm.fkCategoryID = myID OrElse SomeRecursiveCall())

    Return predicate
End Function

但是,我意识到谓词构建器在这里可能毫无用处,可能需要不同的方向。

我考虑过总是有可能为请求的 ID 选择 Category 记录,并从中递归地构建一个 ID 列表以及 Subcategories 的所有成员,然后使用该列表来评估 .Contains()该列表中的比较,但我想知道是否没有其他选项不那么难看。

【问题讨论】:

【参考方案1】:

您不能在 linq to sql 查询中进行数据限制递归(您希望在没有更多数据要获取之前进行递归)。这是因为查询翻译器需要知道何时停止生成查询,而它无法查看数据来知道这一点。

您可以在 TSql 中使用Common Table Expression 来进行数据限制递归...如果您只是在视图中添加该 CTE,您可以从 linq 到 sql 查询视图。

【讨论】:

我讨厌听到“你不能那样做”,但我明白你的意思。令人失望的是,我不能仅仅禁用延迟执行以允许数据限制。不幸的是,编程解决方案为 .Any() 重载引发了一个不受支持的异常,我最终确定会按照我想要的方式运行(如您所指示的)。感谢您向我指出 CTE。完成后我会发布存储过程的结果。【参考方案2】:

解决方案需要从 David B 描述的递归公用表表达式创建一个表值函数,并在我的测试类别的主键上使用 .Contains() 查询 LINQ-to-SQL 中的函数结果。以下是如何完成此操作的详细信息。

使用以下脚本声明了一个表值函数 GetAllCategories。当给定参数@ParentCategoryID 时,它会返回该父级以及所有子类别以及每个记录相对于父级的相应深度,作为一个名为 CategoryLevel 的新字段。

USE MyDatabase
GO

IF OBJECT_ID (N'dbo.GetAllCategories') IS NOT NULL

DROP FUNCTION dbo.GetAllCategories

GO

CREATE FUNCTION dbo.GetAllCategories(@ParentCategoryID int)

RETURNS TABLE

AS RETURN

(

WITH AllCategories (pkCategoryID, fkParentID, Name, Description, CategoryLevel)
AS
(
-- Anchor member definition
    SELECT c.pkCategoryID, c.fkParentID, c.Name, c.Description, 
        0 AS CategoryLevel
    FROM dbo.Category AS c
    WHERE c.pkCategoryID = @ParentCategoryID
    UNION ALL
-- Recursive member definition
    SELECT c.pkCategoryID, c.fkParentID, c.Name, c.Description,
        CategoryLevel + 1
    FROM dbo.Category AS c
    INNER JOIN AllCategories AS ac
        ON c.fkParentID = ac.pkCategoryID
)

SELECT *
FROM AllCategories

)

现在可以通过展开数据库连接的“Functions”子文件夹,从服务器资源管理器将这个表值函数包含在 DBML 中。仅供参考:也可以在 SQL Server Management Studio 2008 中的 MyDatabase > Programmability > Functions > Table-valued Functions 下看到。此函数现在成为您实例化的任何数据上下文对象的成员。

为了利用这个函数来解决上述要求,我构造了一个 LINQ-to-SQL 表达式,如下所示:

Using db As New MyDatabaseDataContext()
    Dim results As IQueryable(Of Item) =
        From cm In db.CategoryMemberships _
        Where (From i In db.GetAllCategories(searchValue) _
               Select i.pkCategoryID).Contains(cm.Category.pkCategoryID) _
        Select cm.Item
End Using

表达式从函数结果中投影出所有主键的列表,并使用.Contains() 扩展来测试每个CategoryMembership 记录的Category 内是否存在主键。如果成功,则选择会员对应的Item

这会返回所有Items 的成员,它们是主键等于searchValueCategory,或作为该父级子级的任何Category 的成员。

【讨论】:

我最终将表值函数转换为存储过程(实际上不需要对脚本进行太多重写),以便以后在 ADO.NET 实体模型中使用。在这种情况下,VB.NET 代码的用例完全没有改变。

以上是关于构建 LINQ 表达式以查找与树节点的所有后代相关的项目的主要内容,如果未能解决你的问题,请参考以下文章

使用 LINQ 选择具有不同名称的后代节点

ADT 树 - 是节点本身的祖先/后代吗?

Linq to XML 后代和元素有啥区别

深入学习jQuery节点关系

第五十三课 树中节点的查找操作

树与树算法