所以我有一个基本上
的SQL表ID, ParentID, MenuName, [Lineage, Depth]
最后两列是自动计算的,以帮助搜索,所以我们暂时可以忽略它们。
我正在创建一个包含多个类别的下拉菜单系统。
不幸的是EF我认为自我引用超过1级深度的表格并不好看。所以我留下了几个选项
1)创建查询,按深度排序,然后在C#中创建自定义类,一次填充一个深度。
2)找到一些方法来急切加载EF中的数据,我不认为有可能无限量的级别,只有固定的数量。
3)其他一些我甚至不确定的方式。
欢迎任何投入!
答案 0 :(得分:37)
我已成功使用EF映射分层数据。
以Establishment
实体为例。这可以代表公司,大学或更大组织结构中的其他单位:
public class Establishment : Entity
{
public string Name { get; set; }
public virtual Establishment Parent { get; set; }
public virtual ICollection<Establishment> Children { get; set; }
...
}
以下是父/子属性的映射方式。这样,当您设置Parent of 1实体时,Parent实体的Children集合将自动更新:
// 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不能很好地生成具有上述关系的嵌套分层查询。我最终确定的是添加了一个新的gerund实体,以及两个新的实体属性:
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 posted an answer that is very similar to this approach。我会继续写作,提供一个稍微不同的选择。我喜欢让我的Ancestor和Offspring动名词类型实际的实体类型,因为它帮助我获得祖先和后代之间的分离(你所谓的深度)。以下是我如何映射这些:
private class EstablishmentNodeOrm : EntityTypeConfiguration<EstablishmentNode>
{
internal EstablishmentNodeOrm()
{
ToTable(typeof(EstablishmentNode).Name);
HasKey(p => new { p.AncestorId, p.OffspringId });
}
}
......最后,建立实体中的识别关系:
// 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来更新节点映射。相反,我们有一组内部命令,它们将根据父母和父母来推导/计算祖先和后代属性。儿童财产。但最终,你最终能够像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));
答案 1 :(得分:13)
您可以使用支持层次结构表来急切加载无限级别的树。
因此,您需要添加两个集合Ancestors
和Descendants
,这两个集合应该映射为支持表的多对多。
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
答案 2 :(得分:5)
我已经花了一段时间尝试修复解决方案中的错误。 存储过程实际上不会生成子孙,孙子等。 您将在下面找到固定的存储过程:
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表。
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的子节点(孙子节点)的子节点,依此类推。
答案 3 :(得分:2)
我知道这个解决方案一定有问题。这并不简单。使用此解决方案,EF6需要另一个黑客程序包来管理一个简单的树(删除删除)。最后,我找到了一个简单的解决方案,但结合了这种方法。
首先让实体简单:只需父母和儿童名单即可。映射应该很简单:
HasOptional(x => x.Parent)
.WithMany(x => x.Children)
.Map(m => m.MapKey("ParentId"));
HasMany(x => x.Children)
.WithOptional(x => x.Parent);
然后添加迁移(代码优先:迁移:包控制台:添加迁移层次结构)或以其他方式添加存储过程:
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();
这样做,而不是通过将子节点添加到根节点。
答案 4 :(得分:2)
我最近工作过的另一个实现选项...
我的树非常简单。
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; }
}
我的要求,而不是。
给定一组叶子和一个祖先,显示该祖先的孩子,他们的后代在该组中有叶子
类比是磁盘上的文件结构。当前用户可以访问系统上的文件子集。当用户打开文件系统树中的节点时,我们只想显示那些最终将它们引导到他们可以看到的文件的用户节点。我们不希望向他们展示他们无权访问的文件的文件路径(出于安全原因,例如泄漏某种类型的文档的存在)。
我们希望能够将此过滤器表示为IQueryable<T>
,因此我们可以将其应用于任何节点查询,过滤掉不需要的结果。
为此,我创建了一个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);
}
具有复杂的返回类型(无法重用Node,查看它)
[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();
}
}
重要的是要注意该函数是在服务器上执行的,而不是在应用程序中执行。这是执行的查询
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
)
))
注意上面查询中的函数调用。
答案 5 :(得分:0)
@danludwig感谢您的回答
我为更新节点编写了一些功能,它的工作非常完美。我的代码是好的还是我应该以其他方式编写它?
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;
}