在LINQ中是否可以进行递归查询

fir*_*tap 5 linq asp.net-mvc entity-framework

这是我的第一个问题,抱歉我的语言不好.

我有一个像这个模特的桌子;

 public class Menu
 {
      [Key]
      public int ID {get;set;}
      public int ParentID {get;set;}
      public string MenuName {get;set;}
      public int OrderNo {get;set;}
      public bool isDisplayInMenu {get;set;} // Menu or just for Access Authority
 }
Run Code Online (Sandbox Code Playgroud)

这个菜单上有很多行;

ID     ParentID      MenuName          Order
---    ---------     -------------     ------
1      0             Main.1               1     >> if ParentID==0 is Root
2      1             Sub.1.1              1
3      2             Sub.1.2              2
4      0             Main.2               2
5      4             Sub.2.1              1
6      4             Sub.2.2              2
Run Code Online (Sandbox Code Playgroud)

我有第二堂课准备菜单树;

public class MyMenu:Menu
{
    public List<MyMenu> Childs { get;set;}
}
Run Code Online (Sandbox Code Playgroud)

我需要一个linq查询来得到这样的结果;

var result = (...linq..).ToList<MyMenu>();
Run Code Online (Sandbox Code Playgroud)

我正在使用递归函数来获取孩子,但这需要花费太多时间才能获得结果.

如何在一个查询中编写一个句子来获取所有菜单树?

更新:

我想将主菜单存储在表格中.此表将用于用户的访问权限控制.某些行将显示在菜单内,有些行仅用于获取访问权限.

在这种情况下,我需要多次才能获得表树.表树将创建为筛选的用户权限.获取树时,存储在会话中.但很多会话意味着很多RAM.如果有什么快速的方法从我需要的时候从sql获取菜单树,那么我将不会存储在会话中.

Chr*_*att 5

如果需要遍历整个树,则应使用存储过程.实体框架特别不适合递归关系.您需要为每个级别发出N + 1个查询,或者急切地加载一组已定义的级别.例如,.Include("Childs.Childs.Childs")将加载三个级别.但是,这将创建一个可怕的查询,您仍然需要为您在开始时未包含的任何其他级别发出N + 1个查询.

在SQL中,你可以使用WITH递归走表,这将是多大的速度比任何实体框架可以做.但是,您的结果将被展平,而不是您将从Entity Framework返回的对象图.例如:

DECLARE @Pad INT = (
    SELECT MAX([Length])
    FROM (
        SELECT LEN([Order]) AS [Length] FROM [dbo].[Menus]
    ) x
);

WITH Tree ([Id], [ParentId], [Name], [Hierarchy]) AS
(
    SELECT
        [ID],
        [ParentID],
        [MenuName],
        REPLICATE('0', @Pad - LEN([Order])) + CAST([Order] AS NVARCHAR(MAX))
    FROM [dbo].[Menus]
    WHERE [ParentID] = 0 -- root
    UNION ALL
        SELECT
            Children.[ID],
            Children.[ParentID],
            Children.[MenuName],
            Parent.[Hierarchy] + '.' + REPLICATE('0', @Pad - LEN(Children.[Order])) + CAST(Children.[Order] AS NVARCHAR(MAX)) AS [Hierarchy]
        FROM [dbo].[Menus] Children
        INNER JOIN Tree AS Parent
            ON Parent.[ID] = Children.[ParentID]
)
SELECT
    [ID],
    [ParentID],
    [MenuName]
FROM Tree
ORDER BY [Hierarchy]
Run Code Online (Sandbox Code Playgroud)

这看起来要复杂得多.为了确保在菜单中的项目是由父母正确排序,并认为父母的树中的位置,我们需要创建的顺序通过订购的分层表示.我这样做是通过创建一个形式的字符串1.1.1,其中每个项目的顺序基本上附加到父级的层次结构字符串的末尾.我也使用REPLICATE左键填充每个级别的顺序,所以你没有常见的数字字符串排序问题,10之前有类似的东西2,因为它开头1.该@Pad声明只是获取最大长度,我需要垫基于表中的最高顺序号.例如,如果最大订单是类似的123,那么值@Pad将是3,因此订单少于123仍然是三个字符(即001).

一旦你完成了所有这些,SQL的其余部分就非常简单了.您只需选择所有根项目,然后通过遍历树将所有子项联合起来.这并加入每个新的水平.最后,从这个树中选择您需要的信息,按我们创建的层次结构排序字符串排序.

至少对于我的树来说,这个查询是可以接受的快速,但如果复杂性扩展或者需要处理大量的菜单项,可能会比你想要的慢一些.即使使用此查询,对树进行某种缓存也不是一个坏主意.就个人而言,对于像网站导航这样的东西,我建议结合使用儿童动作OutputCache.您可以在布局中调用子操作,导航应该出现在该布局中,它将运行操作以获取菜单,或者从缓存中检索已创建的HTML(如果存在).如果菜单特定于各个用户,则只需确保按自定义更改,并考虑用户ID或自定义字符串中的内容.您也可以只对内存缓存查询本身的结果,但您也可以降低生成HTML的成本.但是,应避免将其存储在会话中.

  • 没关系.我完全确定我在说什么,因为我没有使用延迟加载(没有`virtual`属性,`ProxyCreationEnabled`和`LazyLoadingEnabled`都设置为`false`).借助上下文跟踪服务和导航属性修复功能,这是一种急切的加载方式.所有这些都适用于最新的EF6.1.3(可能在早期版本中有所不同),也适用于当前没有延迟加载的EF Core. (2认同)

Iva*_*oev 5

LINQ to Entities 不支持递归查询。

但是,加载存储在数据库表中的整个树既简单又高效。实体框架的早期版本似乎有一些神话,所以让我们揭开它们的神秘面纱。

您只需要创建一个合适的模型和 FK 关系:

模型:

public class Menu
{
    public int ID { get; set; }
    public int? ParentID { get; set; }
    public string MenuName { get; set; }
    public int OrderNo { get; set; }
    public bool isDisplayInMenu { get; set; }

    public ICollection<Menu> Children { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

流畅配置:

modelBuilder.Entity<Menu>()
    .HasMany(e => e.Children)
    .WithOptional() // EF6
    .WithOne() // EF Core
    .HasForeignKey(e => e.ParentID);
Run Code Online (Sandbox Code Playgroud)

重要的变化是,为了建立这种关系,ParentID必须是可空的,并且根项目应该使用null而不是0.

现在,有了模型,加载整棵树很简单:

var tree = db.Menu.AsEnumerable().Where(e => e.ParentID == null).ToList();
Run Code Online (Sandbox Code Playgroud)

随着AsEnumerable()我们确保在执行查询时,整个表会在内存中一个简单的非递归检索SELECTSQL。然后我们简单地过滤掉根项目。

就这样。最后,我们有一个包含根节点的列表,其中包含它们的子节点、孙子节点等!

这个怎么运作?不需要/使用懒惰、急切或显式加载。整个魔法由DbContext跟踪和导航属性修复系统提供。

  • 不错。我想强调的是,这适用于相对较小的表。我认为这些神话与大型层次结构有关,或者只应查询某些特定级别的层次结构。使用 LINQ to E 永远不会那么优雅。不过,人们一直要求这样做的单行语句。 (2认同)

vas*_*ski 1

我会尝试这样的事情。

此查询将从数据库中获取所有菜单记录,并创建以 ParentId 为键、以特定父 ID 为值的所有菜单的字典。

// if you're pulling the data from database with EF
var map = (from menu in ctx.Menus.AsNoTracking()
           group by menu.ParentId into g
           select g).ToDictionary(x => x.Key, x => x.ToList());
Run Code Online (Sandbox Code Playgroud)

现在我们可以很容易地迭代parentIds并创建MyMenu实例

var menusWithChildren = new List<MyMenu>()
foreach(var parentId in map.Keys)
{
   var menuWithChildren = new MyMenu { ... }
   menuWithChildren.AddRange(map[parentId]);
}
Run Code Online (Sandbox Code Playgroud)

现在你已经有了协会的名单。这样,您将通过引用关联子级和父级(不同嵌套级别之间没有重复引用),但我想知道如果您需要了解根,如何定义它们?我不知道这是否适合你。