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

时间:2017-01-27 13:10:44

标签: asp.net-mvc entity-framework linq

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

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

 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
 }

这个菜单上有很多行;

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

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

public class MyMenu:Menu
{
    public List<MyMenu> Childs { get;set;}
}

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

var result = (...linq..).ToList<MyMenu>();

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

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

更新:

我想将主菜单存储在表格中。此表将用于用户的访问权限控制。有些行会显示在菜单中,有些行只会用来获取访问权限。

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

4 个答案:

答案 0 :(得分: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]

看起来比现在复杂得多。为了确保菜单中的项目由父在父树的位置中正确排序,我们需要创建订单的层次表示。我通过创建1.1.1形式的字符串来实现这一点,其中每个项目的顺序基本上都附加到父级的层次结构字符串的末尾。我也使用REPLICATE左键填充每个级别的顺序,因此您没有常见的数字字符串排序问题,10之类的问题出现在2之前,因为它以1开头。 @Pad声明只根据表中的最高订单号获取我需要填充的最大长度。例如,如果最大订单类似于123,则@Pad的值将为3,因此小于123的订单仍为三个字符(即{{1} })。

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

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

答案 1 :(得分:3)

LINQ to Entities不支持递归查询。

但是,加载存储在数据库表中的整个树非常简单有效。早期版本的Entity Framework似乎有一些神话,所以让我们揭开它们的神秘面纱。

您只需要创建一个合适的模型和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; }
}

流利配置:

modelBuilder.Entity<Menu>()
    .HasMany(e => e.Children)
    .WithOptional()
    .HasForeignKey(e => e.ParentID);

重要的变化是,为了设置此类关系,ParentID必须可以为空,并且根项目应使用null而不是0

现在,拥有模型,加载整个树很简单:

var tree = db.Menu.AsEnumerable().Where(e => e.ParentID == null).ToList();

使用AsEnumerable(),我们确保在执行查询时,将使用简单的非递归SELECT SQL在内存中检索整个表。然后我们只是过滤掉根项目。

就是这样。最后,我们有一个包含根节点的列表,其子节点,大孩子等已填充!

它是如何工作的?不需要/使用懒惰,急切或显式加载。整个魔术由DbContext跟踪和导航属性修复系统提供。

答案 2 :(得分:0)

我会尝试这样的事情。

此查询将获取数据库中的所有菜单记录,并将创建包含ParentId for key和所有特定父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());

现在我们可以非常轻松地迭代parentIds并创建MyMenu实例

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

现在你有了关联列表。通过这种方式,您将通过引用关联子项和父项(不同的嵌套级别没有相关的引用)但我想知道如何根据需要知道它们如何定义根?我不知道这是否适合你。

答案 3 :(得分:0)

[String:[String]]

菜单配置:

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 Menu Parent { get; set; }
    public ICollection<Menu> Children { get; set; }
}

菜单服务:

builder.HasMany(z => z.Children).WithOne(z => z.Parent).HasForeignKey(z => z.ParentId).OnDelete(DeleteBehavior.Cascade);