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

发布于 2024-10-20 21:05:35 字数 2471 浏览 6 评论 0 原文

场景

我构建了一个表示类别树的数据库结构,以帮助对我们存储的一些数据进行分类。具体实现是,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 具有适用于所请求的 CategoryCategoryMemberships(即 aCategory. id = myID) 或所请求的 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() 比较,但我想知道是否没有其他选项不太让人感觉那么难看。

The Scenario

I have built a database structure that represents a category tree to help classify some of the data we have stored. The implementation is that each record in the Category table has a nullable foreign key back into the Category table to represent the parent Category of this category (one-to-many), essentially allowing for subcategories within a broader parent level. There is a CategoryMembership table that links a record in the Item table to its respective Category (many-to-many). I have created the DBML for this database, and it has a member access structure that includes the following:

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

Each item in aMembershipCollection has the following member access structure:

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

The Requirement

I am attempting to construct a LINQ Expression that would allow me to determine which Items have CategoryMemberships that are for the requested Category (i.e. aCategory.id = myID) or memberships for descendants of the requested Category, the idea being that I want all Items that are in the parent category or its multiple levels of subcategories.

Essentially the query would be built in a fashion similar to:

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

...where myInCategoryPredicate returns the LINQ Expression object that would help me make that determination. This is of course working from the assumption that the CategoryMembership table is the place to start to retrieve the IQueryable(Of Item). I may have made an erroneous assumption here and that is why I have come seeking advice.

The Problem

I'm having a hard time seeing the forest for the trees. I can't determine if I should start building the predicate from the Category or from the CategoryMembership, nor can I fathom the requisite code that would accomplish what I would like. I'm hoping that someone else who has already built a similar tree structure for a database might be able to help me navigate my way around the DBML classes.

Available Resources

I have previously made use of PredicateBuilder in the past and am relatively familiar with its workings but I haven't been able to devise a way to traverse upward through the tree and build a predicate recursively that would indicate whether an Item is in a category that is either the requested category or a child thereof. So far I've produced the following, with the very noticeable gap labeled 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

However, I realize that a predicate builder may be of no use here whatsoever and a different direction may be required.

I've considered there's always the possibility of selecting the Category record for the requested ID and building a list of IDs from it and all the members of Subcategories recursively then using that list to evaluate a .Contains() comparison on that list, but I was wondering if there weren't other options that didn't quite feel so ugly.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

彩扇题诗 2024-10-27 21:05:36

您无法在 linq to sql 查询中执行数据限制递归(您希望递归直到没有更多数据可供获取)。这是因为查询翻译器需要知道何时停止生成查询,并且它无法查看数据来知道这一点。

您可以在 TSql 中使用公用表表达式来执行数据限制递归...如果您只是将该 CTE 放入视图中,您就可以从 linq to sql 查询该视图。

You can't do data-limitted recursion in a linq to sql query (where you want to recurse until there is no more data to fetch). This is because the query translator needs to know when to stop generating the query and it can't look at the data to know that.

You can use a Common Table Expression in TSql to do data-limitted recursion... If you just slap that CTE in a view, you can query the view from linq to sql.

鼻尖触碰 2024-10-27 21:05:36

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

使用以下脚本声明表值函数 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 > 下看到。可编程性>功能>>表值函数。该函数现在成为您实例化的任何数据上下文对象的成员。

为了利用此函数来解决上述要求,我构造了一个 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

这将返回主键等于 searchValueCategory 成员的所有 Items 或任何 Category 的成员> 那是该父母的孩子。

The solution required creating a table-valued function from a recursive common table expression described by David B and querying against the function result in LINQ-to-SQL with a .Contains() on my test category's primary key. The details of how this is done are below.

A table-valued function GetAllCategories was declared using the following script. When given a parameter @ParentCategoryID, it returns that parent along with all subcategories and the respective depth of each record relative to the parent as a new field called 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

)

This table-valued function can now be included in your DBML from the server explorer by expanding the "Functions" subfolder of your database connection. FYI: It can also be seen in SQL Server Management Studio 2008 under MyDatabase > Programmability > Functions > Table-valued Functions. This function now becomes a member of any data context object you instantiate.

To utilize this function in solving the requirements above, I constructed a LINQ-to-SQL expression like so:

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

The expression projects a list of all primary keys from the function result and uses the .Contains() extension to test for the presence of the primary key for each CategoryMembership record's Category within. If successful, the corresponding Item for the membership is selected.

This returned all Items that were members of the Category with a primary key equal to searchValue, or members of any Category that were children of that parent.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文