如何用C#和EF Core实现无限嵌套论坛的子版块主题总数统计
PHPBB递归版块主题统计的实现逻辑
PHPBB这类支持无限层级版块的论坛,没有冗余存储版块主题统计值,核心是依靠**数据库递归CTE(公共表表达式)**实现单次查询计算:
- 先通过递归查询拿到目标版块的所有后代子版块ID集合
- 直接聚合该ID集合下所有关联主题的数量即可,不需要程序端多次循环查询数据库,避免了N+1性能问题
- 实时计算的方案也避免了新增/删除主题时需要逐层更新父版块计数的一致性问题
EF Core 下的实现方案
针对你遇到的EF Core需要手动指定层级加载深度的问题,有两种成熟的实现方案,可根据站点规模选择:
方案1:全量内存聚合(适合中小站点,版块总数<1万)
仅需2次数据库查询,自动适配无限层级,实现成本极低:
实体定义参考
public class Forum { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int? ParentId { get; set; } // 子版块集合 public List<Forum> Children { get; set; } = new(); // 含所有子版块的总主题数 public int TotalTopicCount { get; set; } // 自身主题数(可选) public int SelfTopicCount { get; set; } }
业务逻辑代码
// 1. 一次性查询所有版块数据 var allForums = await dbContext.Forums.AsNoTracking().ToListAsync(); // 2. 一次性查询所有版块的自身主题数量 var forumSelfTopicCount = await dbContext.Topics .GroupBy(t => t.ForumId) .ToDictionaryAsync(g => g.Key, g => g.Count()); // 3. 构建版块字典,用于拼接层级 var forumDict = allForums.ToDictionary(f => f.Id); // 4. 自动拼接无限层级父子关系 foreach (var forum in allForums) { if (forum.ParentId.HasValue && forumDict.TryGetValue(forum.ParentId.Value, out var parentForum)) { parentForum.Children.Add(forum); } } // 5. 递归计算每个版块的总主题数 int CalculateTotalTopics(Forum forum) { // 先取自身主题数 var total = forumSelfTopicCount.TryGetValue(forum.Id, out var selfCnt) ? selfCnt : 0; // 累加所有子版块的总主题数 foreach (var child in forum.Children) { total += CalculateTotalTopics(child); } forum.TotalTopicCount = total; return total; } // 6. 对根版块执行计算即可自动递归所有子层级 var rootForums = allForums.Where(f => !f.ParentId.HasValue).ToList(); foreach (var root in rootForums) { CalculateTotalTopics(root); }
该方案完全不需要手动写嵌套的Select/Include,不管版块有多少层级都能自动处理,性能远高于DFS循环查询数据库的方案。如果流量较大,可将计算后的版块树缓存几分钟,进一步降低数据库压力。
方案2:递归CTE单次查询(适合大站点/单版块查询场景)
如果仅需要查询单个版块的总主题数,或者版块总数极大,可直接执行递归CTE原生查询:
/// <summary> /// 查询指定版块含所有子版块的总主题数 /// </summary> public async Task<int> GetForumTotalTopicsAsync(int forumId) { // 注意:SQL Server不需要加RECURSIVE关键字,MySQL/PostgreSQL需要保留 const string sql = @" WITH RECURSIVE ForumHierarchy AS ( SELECT Id FROM Forums WHERE Id = {0} UNION ALL SELECT f.Id FROM Forums f INNER JOIN ForumHierarchy fh ON f.ParentId = fh.Id ) SELECT COUNT(*) FROM Topics t INNER JOIN ForumHierarchy fh ON t.ForumId = fh.Id"; return await dbContext.Database.ExecuteScalarAsync<int>(sql, forumId); }
优化方案:物化路径(可选)
如果版块移动频率低,可给Forum表新增Path字符串字段,存储版块的层级路径,如根版块ID为1路径是/1/,子版块ID为2路径是/1/2/,孙版块路径是/1/2/3/,查询时直接用Path LIKE '/1/%'即可拿到所有后代版块ID,连CTE都不需要,查询性能更高,仅需在新增/移动版块时同步更新所有子版块的Path字段即可。
内容的提问来源于stack exchange,提问作者Knightwalker
相关产品推荐
相关产品推荐

