这是一个老问题,但其他答案要么有n+1个数据库查询,要么它们的模型适合自下而上(从树干到叶子)的方法。在这种情况下,标签列表被加载为一棵树,一个标签可以有多个父级。我使用的方法只有两个数据库查询:第一个是获取所选文章的标签,然后是另一个贪婪地加载连接表。因此,这使用自上而下(从叶子到树干)的方法;如果您的连接表很大或者结果不能真正缓存以供重用,则整个事物的急切加载开始显示出这种方法的权衡。
首先,我初始化了两个HashSet:一个用于保存根节点(结果集),另一个用于保留每个已“命中”的节点的引用。
var roots = new HashSet<AncestralTagDto>()
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())
{
foreach (var parent in parents)
{
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
{
var existingRoot = roots.SingleOrDefault(i => i.Equals(tag));
if (existingRoot is null)
roots.Add(tag);
}
}
在底层,我依赖于 HashSet 的属性来防止重复。为此,你使用的中间对象(我在这里使用了 AncestralTagDto,它的 Children 集合也是一个 HashSet)必须根据你的用例适当地重写 Equals 和 GetHashCode 方法。