前言
树形结构是一种很常见的数据结构,类似于现实生活中的树的结构,具有根节点、父子关系和层级结构。
所谓根节点,就是整个树的起始节点。
节点则是树中的元素,每个节点可以有零个或多个子节点,节点按照层级排列,根节点属于第一层,其子节点属于第二层,以此类推,没有子节点的节点,则称为叶子,是最后一层。
父子关系就是节点之间的关系,每个节点都有父节点。
树形结构的应用非常广泛,例如在数据库中用来表示组织结构、目录结构,还用于实现树状菜单、文件系统等。
树形结构的灵活性和层次性使其成为处理具有层级关系数据的有力工具。
常见的树形结构包括二叉树、平衡树、B树等,它们在各个领域都有不同的应用场景和算法实现。
下面分享 EF6 查询并返回树形结构数据的 2 个思路和具体实现方法。
1. EF 生成数据表的实体类
/// <summary>
/// HTFP14 表实体类
/// </summary>
public partial class HTFP14
{public string COMPHT14 { get; set; }public string ACDEHT14 { get; set; }public string PGRPHT14 { get; set; }public string PKEYHT14 { get; set; }public string DESCHT14 { get; set; }public Nullable<decimal> PVALHT14 { get; set; }public string PSTRHT14 { get; set; }public string GLNOHT14 { get; set; }public string PCDEHT14 { get; set; }public string ATLVHT14 { get; set; }public string USERHT14 { get; set; }public System.DateTime LMDTMHT14 { get; set; }
}
2. 创建用于前端的 ViewModel 类
/// <summary>
/// 主菜单 UI 树形结构 ViewModel 类
/// </summary>
public class MainMenuViewModel
{[Description("菜单层次")]public int MenuLevel { get; set; }[Description("菜单码")]public string MenuCode { get; set; }[Description("菜单名称")]public string MenuName { get; set; }[Description("菜单对外显示名称")]public string MenuLabel{get{return $"{MenuCode} - {MenuName}";}}[Description("父菜单码")]public string ParentMenuCode { get; set; }[Description("菜单URL")]public string MenuUrl { get; set; }[Description("菜单授权用户")]public string MenuUser { get; set; }[Description("是否禁止")]public bool Disabled{get{if (string.IsNullOrEmpty(MenuUser)){return true;}return false;}}[Description("菜单排序")]public decimal MenuOrder { get; set; }[Description("子级菜单")]public IList<MainMenuViewModel> Children { get; set; }
}
3. 数据准备
-
获取初级菜单
/// <summary> /// 查询第一级菜单 /// </summary> /// <returns></returns> private IQueryable<MainMenuViewModel> GetFirstMenu() {var query = from t1 in _dbContext.HTFP14 where t1.PGRPHT14 == "MNGP" select new MainMenuViewModel{MenuCode = t1.PKEYHT14,MenuName = t1.DESCHT14,ParentMenuCode = "",MenuUrl = "",MenuUser = ""};return query; }
-
获取二级菜单
/// <summary> /// 查询第二级菜单 /// </summary> /// <param name="companyCode"></param> /// <returns></returns> private IQueryable<MainMenuViewModel> GetSecondMenu(string companyCode) {var query = from t1 in _dbContext.HTFP14where t1.PGRPHT14 == "MUGP" && t1.COMPHT14 == companyCodeselect new MainMenuViewModel{MenuCode = t1.PKEYHT14,MenuName = t1.DESCHT14,ParentMenuCode = t1.PCDEHT14,MenuUrl = "",MenuUser = ""};return query; }
-
获取三级(最终)菜单
/// <summary> /// 查询第三级(最终)菜单 /// </summary> /// <param name="companyCode"></param> /// <returns></returns> private IQueryable<MainMenuViewModel> GetThirdMenu(string menuUser, string companyCode) {var query = from t1 in _dbContext.HTFP02where t1.COMPHT02 == companyCode && t1.STSHT02 == "A"join t2 in (from t21 in _dbContext.HTFP03 where t21.USRMNHT03==menuUser && t21.COMPHT03==companyCode select t21) on t1.MNUCDHT02 equals t2.MNUCDHT03 into t1_t2from t12 in t1_t2.DefaultIfEmpty()select new MainMenuViewModel{MenuCode = t1.MNUCDHT02,MenuName = t1.DESCHT02,ParentMenuCode = t1.MNUGPHT02,MenuUrl = t1.URLHT02,MenuUser = t12.USRMNHT03};return query; }
-
解释:这样分开查询,简化代码,避免太复杂的 Linq 拼接
方法一
思路:使用 Linq 语法拼接查询,具体步骤如下:
-
在数据层用 Linq 拼接写查询方法
/// <summary> /// 查询主菜单树形结构数据 /// </summary> /// <param name="companyCode"></param> /// <returns></returns> public IQueryable<object> QueryMainMenus(string menuUser, string companyCode) {var query1 = GetFirstMenu();var query2 = GetSecondMenu(companyCode);var query3 = GetThirdMenu(menuUser, companyCode);var query = from t1 in query1select new{MenuCode = t1.MenuCode,MenuName = t1.MenuName,ParentMenuCode = t1.ParentMenuCode,MenuUrl = t1.MenuUrl,MenuUser = t1.MenuUser,Children = (from t2 in query2where t2.ParentMenuCode == t1.MenuCodeselect new{MenuCode = t2.MenuCode,MenuName = t2.MenuName,ParentMenuCode = t2.ParentMenuCode,MenuUrl = t2.MenuUrl,MenuUser = t2.MenuUser,Children = (from t3 in query3where t3.ParentMenuCode == t2.MenuCodeselect new{MenuCode = t3.MenuCode,MenuName = t3.MenuName,ParentMenuCode = t3.ParentMenuCode,MenuUrl = t3.MenuUrl,MenuUser = t3.MenuUser})})};return query; }
-
在业务层直接调用方法生成 List 返回给前端
-
总结
逻辑比较简单,有多个菜单可以一直添加下去,但代码会变得很长,所以比较适合事先知道层级并且层级数量不多的场景。
方法二(推荐)
思路:实体类 + 递归方法,具体步骤如下:
-
数据层 EF 使用 Union 方法返回整个树形结构数据:
/// <summary> /// 查询主菜单树形结构数据 /// </summary> /// <param name="companyCode"></param> /// <returns></returns> public IQueryable<MainMenuViewModel> QueryMainMenus(string menuUser, string companyCode) {var query1 = GetFirstMenu();var query2 = GetSecondMenu(companyCode);var query3 = GetThirdMenu(menuUser, companyCode);var query = query1.Union(query2).Union(query3);return query; }
-
业务层递归处理并返回集合数据给前端
public List<MainMenuViewModel> QueryMainMenus(string menuUser, string companyCode) {var list = hTFP02Reposition.QueryMainMenus(menuUser, companyCode).ToList();var list2 = GetData(list);return list2; }/// <summary> /// 处理树形结构数据 /// </summary> /// <param name="source"></param> /// <returns></returns> private List<MainMenuViewModel> GetData(List<MainMenuViewModel> source) {List<MainMenuViewModel> nodes = source.Where(x => x.ParentMenuCode == "").Select(x => x).ToList();foreach (MainMenuViewModel item in nodes){item.Children = GetChildren(source, item);}return nodes; }/// <summary> /// 递归处理树形结构数据 /// </summary> /// <param name="source"></param> /// <param name="node"></param> /// <returns></returns> private IList<MainMenuViewModel> GetChildren(List<MainMenuViewModel> source, MainMenuViewModel node) {IList<MainMenuViewModel> childrens = source.Where(c => c.ParentMenuCode == node.MenuCode).Select(x => x).ToList();foreach (MainMenuViewModel item in childrens){item.Children = GetChildren(source, item);}return childrens; }
-
总结
代码比较简单,但逻辑相对不如第一种方法好理解,递归方法的性能略逊于第一种方法,但可扩展性比较强,适用于无法事先知道层级数量的树形数据结构。
往期精彩
- 分享一个 .NET 通过监听器拦截 EF 消息写日志的详细例子
- 不会使用 EF Core 的 Code First 模式?来看看这篇文章,手把手地教你
- EF Core 性能很差?试试这 6 个小技巧
- 如何在 EF Core 中使用乐观并发控制
- EF Core 在实际开发中,如何分层?
我是老杨,一个奋斗在一线的资深研发老鸟,让我们一起聊聊技术,聊聊程序人生,共同学习,共同进步