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

时间:2011-03-07 21:37:02

标签: vb.net tsql linq-to-sql common-table-expression user-defined-functions

情景

我已经构建了一个表示类别树的数据库结构,以帮助对我们存储的一些数据进行分类。实现是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具有CategoryMemberships Category(即aCategory.id = myID)或成员资格的成员资格请求Category的后代,我的想法是我想要父类别中的所有Items或其子类别的多个级别。

本质上,查询将以类似于:

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

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

问题

我很难看到森林里的树木。我无法确定是否应该从类别或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()比较该列表,但我想知道是否没有其他选项不那么难看。

2 个答案:

答案 0 :(得分:1)

你不能在linq to sql查询中进行数据限制递归(你想要递归,直到没有更多的数据要提取)。这是因为查询翻译需要知道何时停止生成查询,并且无法查看数据以了解该信息。

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

答案 1 :(得分:1)

该解决方案需要从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中。仅供参考:在MyDatabase>下的SQL Server Management Studio 2008中也可以看到它。可编程性>功能>表值函数。此函数现在成为您实例化的任何数据上下文对象的成员。

为了在解决上述要求时使用此函数,我构造了一个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成员的所有Category,其中主键等于searchValue,或者是该父母子女的任何Category成员。< / p>

相关问题