使用entity framework的最有效的自引用树的方法

所以我有一个基本上是一个SQL表

ID, ParentID, MenuName, [Lineage, Depth] 

最后两列是自动计算的,以帮助search,所以我们现在可以忽略它们。

我正在创build一个包含多个类别的下拉菜单系统。

不幸的是,我不认为自我引用表超过1级深。 所以我剩下几个选项

1)创build查询,按深度sorting,然后在C#中创build一个自定义类,一次填充一个深度。

2)find一些方法来急切地加载EF中的数据,我不认为这是可能的无限的水平,只有一个固定的金额。

3)我还没有确定的其他方式。

任何投入都会受到欢迎!

我已经成功地使用EF映射分层数据。

Establishment实体为例。 这可以代表一个公司,大学或更大组织结构中的其他单位:

 public class Establishment : Entity { public string Name { get; set; } public virtual Establishment Parent { get; set; } public virtual ICollection<Establishment> Children { get; set; } ... } 

这是父/子属性如何映射。 这样,当您设置1的实体的父项时,父实体的子集合会自动更新:

 // ParentEstablishment 0..1 <---> * ChildEstablishment HasOptional(d => d.Parent) .WithMany(p => p.Children) .Map(d => d.MapKey("ParentId")) .WillCascadeOnDelete(false); // do not delete children when parent is deleted 

请注意,到目前为止,我还没有包含您的Lineage或Depth属性。 你是对的,EF不能很好地用上面的关系生成嵌套的分层查询。 我最终解决的是增加了一个新的动名词实体,以及两个新的实体属性:

 public class EstablishmentNode : Entity { public int AncestorId { get; set; } public virtual Establishment Ancestor { get; set; } public int OffspringId { get; set; } public virtual Establishment Offspring { get; set; } public int Separation { get; set; } } public class Establishment : Entity { ... public virtual ICollection<EstablishmentNode> Ancestors { get; set; } public virtual ICollection<EstablishmentNode> Offspring { get; set; } } 

在写这篇文章的时候, hazzik发布了一个和这种方法非常相似的答案 。 我会继续写下来,提供一个稍微不同的select。 我喜欢把我的祖先和后代动名词types的实际types,因为它可以帮助我得到先祖和后代之间的分离(你所说的深度)。 这是我如何映射这些:

 private class EstablishmentNodeOrm : EntityTypeConfiguration<EstablishmentNode> { internal EstablishmentNodeOrm() { ToTable(typeof(EstablishmentNode).Name); HasKey(p => new { p.AncestorId, p.OffspringId }); } } 

最后,build立实体中的识别关系:

 // has many ancestors HasMany(p => p.Ancestors) .WithRequired(d => d.Offspring) .HasForeignKey(d => d.OffspringId) .WillCascadeOnDelete(false); // has many offspring HasMany(p => p.Offspring) .WithRequired(d => d.Ancestor) .HasForeignKey(d => d.AncestorId) .WillCascadeOnDelete(false); 

另外,我没有使用sproc来更新节点映射。 相反,我们有一组内部命令将根据Parent&Children属性派生/计算祖先和后代属性。 但最终,你最终可以做一些非常类似的查询,在hazzik的答案:

 // load the entity along with all of its offspring var establishment = dbContext.Establishments .Include(x => x.Offspring.Select(y => e.Offspring)) .SingleOrDefault(x => x.Id == id); 

主要实体与祖先/后代之间的桥梁实体的原因也是因为这个实体让你得到分离。 另外,通过将它声明为一个标识关系,您可以从集合中删除节点,而不必显式调用DbContext.Delete()。

 // load all entities that are more than 3 levels deep var establishments = dbContext.Establishments .Where(x => x.Ancestors.Any(y => y.Separation > 3)); 

您可以使用支持层次结构表来进行无限级别的树的加载。

所以,你需要添加两个集合AncestorsDescendants ,这两个集合应该被映射为支持表的多对多。

 public class Tree { public virtual Tree Parent { get; set; } public virtual ICollection<Tree> Children { get; set; } public virtual ICollection<Tree> Ancestors { get; set; } public virtual ICollection<Tree> Descendants { get; set; } } 

祖先将包含实体的所有祖先(父母,祖父母,祖父母等), Descendants将包含实体的所有后代(子女,孙辈,孙辈等)。

现在您必须将其与EF Code First对应:

 public class TreeConfiguration : EntityTypeConfiguration<Tree> { public TreeConfiguration() { HasOptional(x => x.Parent) .WithMany(x => x.Children) .Map(m => m.MapKey("PARENT_ID")); HasMany(x => x.Children) .WithOptional(x => x.Parent); HasMany(x => x.Ancestors) .WithMany(x => x.Descendants) .Map(m => m.ToTable("Tree_Hierarchy").MapLeftKey("PARENT_ID").MapRightKey("CHILD_ID")); HasMany(x => x.Descendants) .WithMany(x => x.Ancestors) .Map(m => m.ToTable("Tree_Hierarchy").MapLeftKey("CHILD_ID").MapRightKey("PARENT_ID")); } } 

现在有了这个结构,你可以像下面这样做

 context.Trees.Include(x => x.Descendants).Where(x => x.Id == id).SingleOrDefault() 

这个查询将加载具有id实体以及它的所有后代。

您可以使用以下存储过程填充支持表:

 CREATE PROCEDURE [dbo].[FillHierarchy] (@table_name nvarchar(MAX), @hierarchy_name nvarchar(MAX)) AS BEGIN DECLARE @sql nvarchar(MAX), @id_column_name nvarchar(MAX) SET @id_column_name = '[' + @table_name + '_ID]' SET @table_name = '[' + @table_name + ']' SET @hierarchy_name = '[' + @hierarchy_name + ']' SET @sql = '' SET @sql = @sql + 'WITH Hierachy(CHILD_ID, PARENT_ID) AS ( ' SET @sql = @sql + 'SELECT ' + @id_column_name + ', [PARENT_ID] FROM ' + @table_name + ' e ' SET @sql = @sql + 'UNION ALL ' SET @sql = @sql + 'SELECT e.' + @id_column_name + ', e.[PARENT_ID] FROM ' + @table_name + ' e ' SET @sql = @sql + 'INNER JOIN Hierachy eh ON e.' + @id_column_name + ' = eh.[PARENT_ID]) ' SET @sql = @sql + 'INSERT INTO ' + @hierarchy_name + ' ([CHILD_ID], [PARENT_ID]) ( ' SET @sql = @sql + 'SELECT [CHILD_ID], [PARENT_ID] FROM Hierachy WHERE [PARENT_ID] IS NOT NULL ' SET @sql = @sql + ') ' EXECUTE (@sql) END GO 

甚至可以将支持表映射到视图:

 CREATE VIEW [Tree_Hierarchy] AS WITH Hierachy (CHILD_ID, PARENT_ID) AS ( SELECT [MySuperTree_ID], [PARENT_ID] FROM [MySuperTree] AS e UNION ALL SELECT e.[MySuperTree_ID], e.[PARENT_ID] FROM [MySuperTree] AS e INNER JOIN Hierachy AS eh ON e.[MySuperTree_ID] = eh.[PARENT_ID] ) SELECT [CHILD_ID], [PARENT_ID] FROM Hierachy WHERE [PARENT_ID] IS NOT NULL GO 

我已经花了一段时间试图解决您的解决scheme中的错误。 存储过程真的不会产生子孙,等等。下面你会发现固定的存储过程:

 CREATE PROCEDURE dbo.UpdateHierarchy AS BEGIN DECLARE @sql nvarchar(MAX) SET @sql = '' SET @sql = @sql + 'WITH Hierachy(ChildId, ParentId) AS ( ' SET @sql = @sql + 'SELECT t.Id, t.ParentId FROM dbo.Tree t ' SET @sql = @sql + 'UNION ALL ' SET @sql = @sql + 'SELECT h.ChildId, t.ParentId FROM dbo.Tree t ' SET @sql = @sql + 'INNER JOIN Hierachy h ON t.Id = h.ParentId) ' SET @sql = @sql + 'INSERT INTO dbo.TreeHierarchy (ChildId, ParentId) ( ' SET @sql = @sql + 'SELECT DISTINCT ChildId, ParentId FROM Hierachy WHERE ParentId IS NOT NULL ' SET @sql = @sql + 'EXCEPT SELECT t.ChildId, t.ParentId FROM dbo.TreeHierarchy t ' SET @sql = @sql + ') ' EXECUTE (@sql) END 

错误:错误的参考。 翻译@hazzik代码是:

  SET @sql = @sql + 'SELECT t.ChildId, t.ParentId FROM dbo.Tree t ' 

但应该是

  SET @sql = @sql + 'SELECT h.ChildId, t.ParentId FROM dbo.Tree t ' 

我也添加了代码,允许您不仅在您填充TreeHierarchy表时更新TreeHierarchy表。

  SET @sql = @sql + 'EXCEPT SELECT t.ChildId, t.ParentId FROM dbo.TreeHierarchy t ' 

而魔法。 这个过程,或者说TreeHierarchy允许你只需要包含祖先(不是儿童而不是后代)来加载儿童。

  using (var context = new YourDbContext()) { rootNode = context.Tree .Include(x => x.Ancestors) .SingleOrDefault(x => x.Id == id); } 

现在,YourDbContext将返回一个带有加载的子元素的rootNode,rootName的子元素(孙子)的子元素等等。

我知道这个解决scheme肯定有什么问题。 这并不简单。 使用这个解决scheme,EF6需要另一套黑客来pipe理一个简单的树(删除)。 所以最后我find了一个简单的解决scheme,但结合这种方法。

首先让实体简单:只要父母和孩子的名单就够了。 映射也应该很简单:

  HasOptional(x => x.Parent) .WithMany(x => x.Children) .Map(m => m.MapKey("ParentId")); HasMany(x => x.Children) .WithOptional(x => x.Parent); 

然后添加迁移(代码优先:migrations:package console:Add-Migration Hierarchy)或以其他方式存储过程:

 CREATE PROCEDURE [dbo].[Tree_GetChildren] (@Id int) AS BEGIN WITH Hierachy(ChildId, ParentId) AS ( SELECT ts.Id, ts.ParentId FROM med.MedicalTestSteps ts UNION ALL SELECT h.ChildId, ts.ParentId FROM med.MedicalTestSteps ts INNER JOIN Hierachy h ON ts.Id = h.ParentId ) SELECT h.ChildId FROM Hierachy h WHERE h.ParentId = @Id END 

那么当你试图从数据库接收你的树节点时,只需要两步:

 //Get children IDs var sql = $"EXEC Tree_GetChildren {rootNodeId}"; var children = context.Database.SqlQuery<int>(sql).ToList<int>(); //Get root node and all it's children var rootNode = _context.TreeNodes .Include(s => s.Children) .Where(s => s.Id == id || children.Any(c => s.Id == c)) .ToList() //MUST - get all children from database then get root .FirstOrDefault(s => s.Id == id); 

这一切。 此查询可帮助您获取根节点并加载所有子节点。 不玩引进先祖和后代。

还记得当你试图保存子节点的时候,就这样做:

 var node = new Node { ParentId = rootNode }; //Or null, if you want node become a root context.TreeNodess.Add(node); context.SaveChanges(); 

这样做,而不是通过添加孩子到根节点。

我最近处理的另一个实现选项

我的树很简单

 public class Node { public int NodeID { get; set; } public string Name { get; set; } public virtual Node ParentNode { get; set; } public int? ParentNodeID { get; set; } public virtual ICollection<Node> ChildNodes { get; set; } public int? LeafID { get; set; } public virtual Leaf Leaf { get; set; } } public class Leaf { public int LeafID { get; set; } public string Name { get; set; } public virtual ICollection<Node> Nodes { get; set; } } 

我的要求,不是那么多。

给定一组叶子和一个单独的祖先,展示那个祖先的子孙,这个祖先的后代有叶子

类比将是磁盘上的文件结构。 当前用户可以访问系统上的一部分文件。 当用户打开文件系统树中的节点时,我们只想显示那些将最终导致他们看到的文件的用户节点。 我们不希望将文件path显示给他们无权访问的文件(出于安全原因,例如泄漏某种types文档的存在)。

我们希望能够将此filter表示为IQueryable<T> ,因此我们可以将其应用于任何节点查询,从而过滤掉不需要的结果。

为此,我创build了一个Table Valued Function,它返回树中节点的后代。 它通过CTE来完成。

 CREATE FUNCTION [dbo].[DescendantsOf] ( @parentId int ) RETURNS TABLE AS RETURN ( WITH descendants (NodeID, ParentNodeID, LeafID) AS( SELECT NodeID, ParentNodeID, LeafID from Nodes where ParentNodeID = @parentId UNION ALL SELECT n.NodeID, n.ParentNodeID, n.LeafID from Nodes n inner join descendants d on n.ParentNodeID = d.NodeID ) SELECT * from descendants ) 

现在,我使用Code First,所以我不得不使用

https://www.nuget.org/packages/EntityFramework.Functions

为了将函数添加到我的DbContext

 [TableValuedFunction("DescendantsOf", "Database", Schema = "dbo")] public IQueryable<NodeDescendant> DescendantsOf(int parentID) { var param = new ObjectParameter("parentId", parentID); return this.ObjectContext().CreateQuery<NodeDescendant>("[DescendantsOf](@parentId)", param); } 

与一个复杂的返回types(不能重用节点,看着这个)

 [ComplexType] public class NodeDescendant { public int NodeID { get; set; } public int LeafID { get; set; } } 

把它放在一起允许我,当用户展开树中的一个节点,得到筛选的子节点的列表。

 public static Node[] GetVisibleDescendants(int parentId) { using (var db = new Models.Database()) { int[] visibleLeaves = SuperSecretResourceManager.GetLeavesForCurrentUserLol(); var targetQuery = db.Nodes as IQueryable<Node>; targetQuery = targetQuery.Where(node => node.ParentNodeID == parentId && db.DescendantsOf(node.NodeID).Any(x => visibleLeaves.Any(y => x.LeafID == y))); // Notice, still an IQueryable. Perform whatever processing is required. SortByCurrentUsersSavedSettings(targetQuery); return targetQuery.ToArray(); } } 

请注意, 该function在服务器上执行,而不是在应用程序中执行 。 这是执行的查询

 SELECT [Extent1].[NodeID] AS [NodeID], [Extent1].[Name] AS [Name], [Extent1].[ParentNodeID] AS [ParentNodeID], [Extent1].[LeafID] AS [LeafID] FROM [dbo].[Nodes] AS [Extent1] WHERE ([Extent1].[ParentNodeID] = @p__linq__0) AND ( EXISTS (SELECT 1 AS [C1] FROM ( SELECT [Extent2].[LeafID] AS [LeafID] FROM [dbo].[DescendantsOf]([Extent1].[NodeID]) AS [Extent2] ) AS [Project1] WHERE EXISTS (SELECT 1 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable1] WHERE [Project1].[LeafID] = 17 ) )) 

请注意上面查询中的函数调用。

@danludwig谢谢你的回答

我为update Node写了一些函数,工作很完美。 我的代码是好的,或者我应该用其他方式写它?

  public void Handle(ParentChanged e) { var categoryGuid = e.CategoryId.Id; var category = _context.Categories .Include(cat => cat.ParentCategory) .First(cat => cat.Id == categoryGuid); if (null != e.OldParentCategoryId) { var oldParentCategoryGuid = e.OldParentCategoryId.Id; if (category.ParentCategory.Id == oldParentCategoryGuid) { throw new Exception("Old Parent Category mismatch."); } } (_context as DbContext).Configuration.LazyLoadingEnabled = true; RemoveFromAncestors(category, category.ParentCategory); var newParentCategoryGuid = e.NewParentCategoryId.Id; var parentCategory = _context.Categories .First(cat => cat.Id == newParentCategoryGuid); category.ParentCategory = parentCategory; AddToAncestors(category, category.ParentCategory, 1); _context.Commit(); } private static void RemoveFromAncestors(Model.Category.Category mainCategory, Model.Category.Category ancestorCategory) { if (null == ancestorCategory) { return; } while (true) { var offspring = ancestorCategory.Offspring; offspring?.RemoveAll(node => node.OffspringId == mainCategory.Id); if (null != ancestorCategory.ParentCategory) { ancestorCategory = ancestorCategory.ParentCategory; continue; } break; } } private static int AddToAncestors(Model.Category.Category mainCategory, Model.Category.Category ancestorCategory, int deep) { var offspring = ancestorCategory.Offspring ?? new List<CategoryNode>(); if (null == ancestorCategory.Ancestors) { ancestorCategory.Ancestors = new List<CategoryNode>(); } var node = new CategoryNode() { Ancestor = ancestorCategory, Offspring = mainCategory }; offspring.Add(node); if (null != ancestorCategory.ParentCategory) { deep = AddToAncestors(mainCategory, ancestorCategory.ParentCategory, deep + 1); } node.Separation = deep; return deep; }