如何使用实体框架进行递归加载?
Posted
技术标签:
【中文标题】如何使用实体框架进行递归加载?【英文标题】:How to do recursive load with Entity framework? 【发布时间】:2010-02-15 14:17:39 【问题描述】:我在 DB 中有一个带有 TreeNodes 表的树结构。该表具有 nodeId、parentId 和 parameterId。在 EF 中,结构类似于 TreeNode.Children,其中每个孩子都是一个 TreeNode... 我还有一个包含 id、name 和 rootNodeId 的 Tree 表。
一天结束时,我想将树加载到 TreeView 中,但我不知道如何一次将其全部加载。 我试过了:
var trees = from t in context.TreeSet.Include("Root").Include("Root.Children").Include("Root.Children.Parameter")
.Include("Root.Children.Children")
where t.ID == id
select t;
这将使我获得前 2 代,但不会更多。 如何加载包含所有世代和附加数据的整个树?
【问题讨论】:
如果这对您来说仍然是个问题,我已经提供了另一个答案,它比喋喋不休的数据库调用更好,并且能够填充整个层次结构而无需指定无限包含。 【参考方案1】:我最近遇到了这个问题,在我想出了一个简单的方法来实现结果之后偶然发现了这个问题。我提供了对 Craig 的答案的编辑,提供了第 4 种方法,但权力决定它应该是另一个答案。这对我来说很好:)
My original question / answer can be found here.
只要您在表中的项目都知道它们属于哪棵树(在您的情况下看起来像:t.ID
),这就会起作用。也就是说,目前尚不清楚您真正拥有哪些实体,但即使您拥有多个实体,您也必须在实体 Children
中拥有一个 FK,如果那不是 TreeSet
基本上不要使用Include()
:
var query = from t in context.TreeSet
where t.ID == id
select t;
// if TreeSet.Children is a different entity:
var query = from c in context.TreeSetChildren
// guessing the FK property TreeSetID
where c.TreeSetID == id
select c;
这将带回树的所有项目并将它们全部放在集合的根目录中。此时,您的结果集将如下所示:
-- Item1
-- Item2
-- Item3
-- Item4
-- Item5
-- Item2
-- Item3
-- Item5
由于您可能希望您的实体仅分层地从 EF 中出来,这不是您想要的,对吧?
.. 然后,排除存在于根级别的后代:
幸运的是,由于您的模型中有导航属性,子实体集合仍将被填充,如上面结果集的插图所示。通过使用foreach()
循环手动迭代结果集,并将这些根项添加到new List<TreeSet>()
,您现在将拥有一个包含根元素和所有后代正确嵌套的列表。
如果您的树变大并且性能受到关注,您可以按ParentID
(它是Nullable
,对吗?)对返回集 ASCENDING 进行排序,以便所有根项都排在第一位。像以前一样迭代和添加,但是一旦你到达一个非空的循环就会中断。
var subset = query
// execute the query against the DB
.ToList()
// filter out non-root-items
.Where(x => !x.ParentId.HasValue);
现在subset
将如下所示:
-- Item1
-- Item2
-- Item3
-- Item4
-- Item5
关于 Craig 的解决方案:
您真的不想为此使用延迟加载!围绕 n+1 查询的必要性构建的设计将是一个主要的性能吸盘。 *********(好吧,公平地说,如果您要允许用户有选择地向下钻取树,那么它可能是合适的。只是不要使用延迟加载来将它们全部提前!)我从未尝试过嵌套设置东西,我也不建议破解 EF 配置来完成这项工作,因为有一个更简单的解决方案。 Another reasonable suggestion 正在创建一个提供自链接的数据库视图,然后将该视图映射到中间连接/链接/m2m 表。就个人而言,我发现这个解决方案比必要的复杂,但它可能有它的用途。
【讨论】:
JoeBrockhaus,我没有测试您的代码,但有几件事:1)该代码只是将数据作为单个节点而不是作为树节点进行迭代和填充。 2)永远不会添加根节点。也许您应该尝试在 ICollectionable problery 中分配子对象。这样做你会创建一棵树。我不会投反对票,但请改进您的答案。 @GabrielAndrésBrancolini 最初的问题缺乏关于模型的一些细节,但我的回答解决了我提到的两种可能的情况。至于孩子的集合,这些将自动填充,因为(并且只要)为实体定义了导航属性。其中之一将是根节点。最终的变化在于逻辑:不要考虑查询父级并获取其递归子级,而只需查询所有子级并在需要时独立获取父级。 另外,关于否决票的评论是因为原始答案在进行任何其他投票或 cmet 之前被否决了,至少有些人认为这个答案是对该问题的更好答案。跨度> 这个答案对我很有用,尽管在使用延迟加载时需要一个额外的步骤。我将Entity Framework Core 3.1
与延迟加载代理一起使用。因此,即使在填充树时,代理也会在访问 Children
时触发额外的负载。因此,解决方案是为每个返回的节点添加dbContext.Entry(treeNode).Collection(n => n.Children).IsLoaded = true;
。【参考方案2】:
当您使用Include()
时,您要求实体框架将您的查询转换为 SQL。所以想一想:你将如何编写返回任意深度的树的 SQL 语句?
回答:除非您使用数据库服务器的特定层次结构功能(这些功能不是 SQL 标准,但某些服务器支持,例如 SQL Server 2008,尽管它的实体框架提供程序不支持),否则您不会。在 SQL 中处理任意深度树的常用方法是使用 the nested sets model 而不是父 ID 模型。
因此,您可以使用三种方法来解决此问题:
-
使用嵌套集模型。这需要更改您的元数据。
使用 SQL Server 的层次结构功能,并破解实体框架以理解它们(很棘手,但 this technique might work)。同样,您需要更改元数据。i
使用显式加载或 EF 4 的延迟加载而不是急切加载。这将导致很多数据库查询,而不是一个。
【讨论】:
最后,我添加了一个递归调用,该调用在子级和其他引用对象上调用 Load。谢谢 仅供参考...“嵌套集模型”链接无效。 InformationWeek 说“无效的 URL”。【参考方案3】:我想发布我的答案,因为其他人没有帮助我。
我的数据库有点不同,基本上我的表有一个 ID 和一个 ParentID。该表是递归的。以下代码获取所有子级并将它们嵌套到最终列表中。
public IEnumerable<Models.MCMessageCenterThread> GetAllMessageCenterThreads(int msgCtrId)
var z = Db.MCMessageThreads.Where(t => t.ID == msgCtrId)
.Select(t => new MCMessageCenterThread
Id = t.ID,
ParentId = t.ParentID ?? 0,
Title = t.Title,
Body = t.Body
).ToList();
foreach (var t in z)
t.Children = GetChildrenByParentId(t.Id);
return z;
private IEnumerable<MCMessageCenterThread> GetChildrenByParentId(int parentId)
var children = new List<MCMessageCenterThread>();
var threads = Db.MCMessageThreads.Where(x => x.ParentID == parentId);
foreach (var t in threads)
var thread = new MCMessageCenterThread
Id = t.ID,
ParentId = t.ParentID ?? 0,
Title = t.Title,
Body = t.Body,
Children = GetChildrenByParentId(t.ID)
;
children.Add(thread);
return children;
为了完整起见,这是我的模型:
public class MCMessageCenterThread
public int Id get; set;
public int ParentId get; set;
public string Title get; set;
public string Body get; set;
public IEnumerable<MCMessageCenterThread> Children get; set;
【讨论】:
今天这对我有帮助,谢谢! 这是递归加载方式。它将产生对数据库的 n 个查询。代码存在一些问题:1.
GetAllMessageCenterThreads
返回一个列表,但只有一个项目 (.Where(t=>t.ID == msgCtrId)
)。 2.
你的 ParentID
属性应该可以为空(int?
与 int
),并且 ParentId = t.ParentID ?? 0,
将始终返回“0”,因为 ParentID 不能为空(这不能是代码优先?)。 3.
看起来您的模型缺少一个属性:MessageCenterId
。所有项目都应该有这个字段,所以你可以通过那个 id 获取所有项目。
如果您在这种情况下的目标是获得对特定线程的所有回复(相对于特定博客的所有线程),那么您应该查看公用表表达式(或 CTE),它们专门对于递归场景。在这种情况下,您将创建一个递归联合+自连接。看到这个答案:***.com/a/19915206/237723
这段代码取自一个数据库优先的例子;其次,如果您有更好的解决方案,也许您应该发布它,嗯?我想抨击别人的工作解决方案总是更有趣;)【参考方案4】:
我最近写了一个 N+1 选择加载整个树的东西,其中 N 是源对象中最深路径的层数。
鉴于以下自引用类,这就是我所做的
public class SomeEntity
public int Id get; set;
public int? ParentId get; set;
public string Name get; set;
我写了以下 DbSet 助手
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace Microsoft.EntityFrameworkCore
public static class DbSetExtensions
public static async Task<TEntity[]> FindRecursiveAsync<TEntity, TKey>(
this DbSet<TEntity> source,
Expression<Func<TEntity, bool>> rootSelector,
Func<TEntity, TKey> getEntityKey,
Func<TEntity, TKey> getChildKeyToParent)
where TEntity: class
// Keeps a track of already processed, so as not to invoke
// an infinte recursion
var alreadyProcessed = new HashSet<TKey>();
TEntity[] result = await source.Where(rootSelector).ToArrayAsync();
TEntity[] currentRoots = result;
while (currentRoots.Length > 0)
TKey[] currentParentKeys = currentRoots.Select(getEntityKey).Except(alreadyProcessed).ToArray();
alreadyProcessed.AddRange(currentParentKeys);
Expression<Func<TEntity, bool>> childPredicate = x => currentParentKeys.Contains(getChildKeyToParent(x));
currentRoots = await source.Where(childPredicate).ToArrayAsync();
return result;
当你需要加载整个树时,你只需调用这个方法,传入三个东西
-
根对象的选择标准
如何获取对象的主键属性(SomeEntity.Id)
如何获取引用其父级 (SomeEntity.ParentId) 的子级属性
例如
SomeEntity[] myEntities = await DataContext.SomeEntity.FindRecursiveAsync(
rootSelector: x => x.Id = 42,
getEntityKey: x => x.Id,
getChildKeyToParent: x => x.ParentId).ToArrayAsync();
);
或者,如果您可以将RootId
列添加到表中,那么对于每个非根条目,您可以将此列设置为树根的 ID。然后,您可以通过一次选择获取所有内容
DataContext.SomeEntity.Where(x => x.Id == rootId || x.RootId == rootId)
【讨论】:
这对于 RPS 非常低的小型集合来说可能没问题,但如果必须,您确实应该在数据库中使用 CTE 来递归加载整个集合。 比DataContext.SomeEntity.Where(x => x.Id == rootId || x.RootId == rootId)
好吗?
如果您有rootId
,那么您不需要数据库中的 CTE。 CTE 的价值只是在数据库中执行所有递归迭代,这样您就不会产生连接和往返开销。【参考方案5】:
对于加载子对象的示例,我将给出一个包含注释的 Comment 对象的示例。每个评论都有一个可能的子评论。
private static void LoadComments(<yourObject> q, Context yourContext)
if(null == q | null == yourContext)
return;
yourContext.Entry(q).Reference(x=> x.Comment).Load();
Comment curComment = q.Comment;
while(null != curComment)
curComment = LoadChildComment(curComment, yourContext);
private static Comment LoadChildComment(Comment c, Context yourContext)
if(null == c | null == yourContext)
return null;
yourContext.Entry(c).Reference(x=>x.ChildComment).Load();
return c.ChildComment;
现在,如果您拥有自己的集合,您将需要使用 Collection 而不是 Reference 并进行相同的潜水。至少这是我在处理实体和 SQLite 时在这种情况下采用的方法。
【讨论】:
【参考方案6】:这是一个老问题,但其他答案要么有 n+1 个数据库命中,要么他们的模型有利于自下而上(树干到叶子)方法。在这种情况下,标签列表作为树加载,并且标签可以有多个父级。我使用的方法只有两个数据库命中:第一个获取所选文章的标签,然后另一个急切加载连接表。因此,这使用了自上而下(从树干到树干)的方法;如果您的连接表很大,或者如果结果不能真正被缓存以供重用,那么急切加载整个事物就会开始显示这种方法的权衡。
首先,我初始化了两个HashSet
s:一个保存根节点(结果集),另一个保存对每个被“命中”的节点的引用。
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 方法。
【讨论】:
以上是关于如何使用实体框架进行递归加载?的主要内容,如果未能解决你的问题,请参考以下文章