这是我的第一个问题,抱歉我的语言不好。
我有一张像这个模特的桌子;
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获取菜单树,那么我将不会存储在会话中。
答案 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);