这是一个老问题,但其他答案要么有 n+1 个数据库命中,要么他们的模型有利于自下而上(树干到叶子)方法。在这种情况下,标签列表作为树加载,并且标签可以有多个父级。我使用的方法只有两个数据库命中:第一个获取所选文章的标签,然后另一个急切加载连接表。因此,这使用了自上而下(从树干到树干)的方法;如果您的连接表很大,或者如果结果不能真正被缓存以供重用,那么急切加载整个事物就会开始显示这种方法的权衡。
首先,我初始化了两个HashSets:一个保存根节点(结果集),另一个保存对每个被“命中”的节点的引用。
var roots = new HashSet<AncestralTagDto>(); //no parents
var allTags = new HashSet<AncestralTagDto>();
接下来,我抓取客户请求的所有叶子,将它们放入一个包含子集合的对象中(但在此步骤之后该集合将保持为空)。
var startingTags = await _dataContext.ArticlesTags
.Include(p => p.Tag.Parents)
.Where(t => t.Article.CategoryId == categoryId)
.GroupBy(t => t.Tag)
.ToListAsync()
.ContinueWith(resultTask =>
resultTask.Result.Select(
grouping => new AncestralTagDto(
grouping.Key.Id,
grouping.Key.Name)));
现在,让我们抓取标签自连接表,并将其全部加载到内存中:
var tagRelations = await _dataContext.TagsTags.Include(p => p.ParentTag).ToListAsync();
现在,对于startingTags 中的每个标签,将该标签添加到allTags 集合中,然后沿着树向下递归地获取祖先:
foreach (var tag in startingTags)
{
allTags.Add(tag);
GetParents(tag);
}
return roots;
最后,这是构建树的嵌套递归方法:
void GetParents(AncestralTagDto tag)
{
var parents = tagRelations.Where(c => c.ChildTagId == tag.Id).Select(p => p.ParentTag);
if (parents.Any()) //then it's not a root tag; keep climbing down
{
foreach (var parent in parents)
{
//have we already seen this parent tag before? If not, instantiate the dto.
var parentDto = allTags.SingleOrDefault(i => i.Id == parent.Id);
if (parentDto is null)
{
parentDto = new AncestralTagDto(parent.Id, parent.Name);
allTags.Add(parentDto);
}
parentDto.Children.Add(tag);
GetParents(parentDto);
}
}
else //the tag is a root tag, and should be in the root collection. If it's not in there, add it.
{
//this block could be simplified to just roots.Add(tag), but it's left this way for other logic.
var existingRoot = roots.SingleOrDefault(i => i.Equals(tag));
if (existingRoot is null)
roots.Add(tag);
}
}
在幕后,我依靠HashSet 的属性来防止重复。为此,您使用的中间对象(我在这里使用 AncestralTagDto,它的 Children 集合也是 HashSet)非常重要,根据您的用例覆盖 Equals 和 GetHashCode 方法。