场景
我构建了一个表示类别树的数据库结构,以帮助对我们存储的一些数据进行分类。具体实现是,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
的 CategoryMemberships
(即 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.
发布评论
评论(2)
您无法在 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.
该解决方案需要从 David B 描述的递归公用表表达式创建一个表值函数,并在 LINQ-to-SQL 中使用我的测试类别主键上的 .Contains() 查询该函数结果。下面详细介绍了如何完成此操作。
使用以下脚本声明表值函数 GetAllCategories。当给定参数 @ParentCategoryID 时,它会返回该父类别以及所有子类别以及每个记录相对于父类别的相应深度,作为名为 CategoryLevel 的新字段。
现在,通过展开数据库连接的“Functions”子文件夹,可以将该表值函数包含在服务器资源管理器中的 DBML 中。仅供参考:它也可以在 SQL Server Management Studio 2008 中的 MyDatabase > 下看到。可编程性>功能>>表值函数。该函数现在成为您实例化的任何数据上下文对象的成员。
为了利用此函数来解决上述要求,我构造了一个 LINQ-to-SQL 表达式,如下所示:
该表达式从函数结果中投影所有主键的列表,并使用
.Contains()
扩展测试其中每个CategoryMembership
记录的Category
是否存在主键。如果成功,则会选择该成员资格的相应Item
。这将返回主键等于
searchValue
的Category
成员的所有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.
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:
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 eachCategoryMembership
record'sCategory
within. If successful, the correspondingItem
for the membership is selected.This returned all
Items
that were members of theCategory
with a primary key equal tosearchValue
, or members of anyCategory
that were children of that parent.