3

场景构建LINQ表达找到相关的树节点

我已经建立了一个代表类别树,以帮助进行分类一些我们所储存的数据的数据库结构的所有后代项目。实现方式是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表达的对象,这将有助于我作出这样的决定。这当然是基于假设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 

不过,我认识到,一个谓词建设者可能是没有用的,在这里和任何可能需要不同的方向。

我认为总会有选择Category纪录被请求的ID,并从中建立ID的列表和Subcategories所有成员递归然后使用该名单,以评估对一。载有()比较的可能性名单,但我想知道是否没有其他选项不那么难受。

回答

1

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

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

+0

我讨厌听到“你不能这样做”,但我理解你的意思。这是一个失望,我不能只是禁用延迟执行,以便允许数据限制。不幸的是,编程解决方案为我最终确定的.Any()重载引发了一个不支持的异常,这会导致我想要的行为(如您所示)。感谢您指点我CTE。一旦完成,我将发布存储过程的结果。 – lsuarez 2011-03-08 05:56:44

1

该解决方案需要从David B描述的递归公用表表达式中创建表值函数,并使用我的测试类别的主键上的.Contains()来查询LINQ到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 

) 

此表值函数现在可以包含在您的DBML从通过扩大你的数据库连接的“功能”子文件夹中的服务器资源管理器。仅供参考:在MyDatabase>可编程性>函数>表值函数下,SQL Server Management Studio 2008中也可以看到它。该函数现在成为您实例化的任何数据上下文对象的成员。

为了解决上述规定应用这个功能,我构造的LINQ到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的成员。

+0

我最终将表值函数转换为存储过程(在脚本中实际上不需要重写),以便稍后在ADO.NET实体模型中使用。在这种情况下,VB.NET代码的用例根本不会改变。 – lsuarez 2011-03-09 22:31:42