使用Entity Framework自我引用树的最有效方法

Joh*_*ell 36 c# asp.net-mvc entity-framework-4

所以我基本上有一个SQL表

ID, ParentID, MenuName, [Lineage, Depth]
Run Code Online (Sandbox Code Playgroud)

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

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

不幸的是,我认为自我引用超过1级深度的表格并不好看.所以我留下了一些选择

1)创建查询,按深度排序,然后在C#中创建一个自定义类,一次填充一个深度.

2)找到一些方法来急切加载EF中的数据,我不认为有可能无限量的级别,只有固定的数量.

3)其他一些我甚至不确定的方式.

任何投入都会受到欢迎!

dan*_*wig 40

我已经使用EF成功映射了分层数据.

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

public class Establishment : Entity
{
    public string Name { get; set; }
    public virtual Establishment Parent { get; set; }
    public virtual ICollection<Establishment> Children { get; set; }
    ...
}
Run Code Online (Sandbox Code Playgroud)

以下是父/子属性的映射方式.这样,当您设置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
Run Code Online (Sandbox Code Playgroud)

请注意,到目前为止,我还没有包含您的Lineage或Depth属性.你是对的,EF不能很好地生成具有上述关系的嵌套分层查询.我最终确定的是添加了一个新的gerund实体,以及2个新的实体属性:

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; }

}
Run Code Online (Sandbox Code Playgroud)

在写这篇文章时,hazzik发布了一个非常类似于这种方法的答案.我会继续写作,提供一个稍微不同的选择.我喜欢让我的Ancestor和Offspring动名词类型实际的实体类型,因为它帮助我获得祖先和后代之间的分离(你所谓的深度).以下是我如何映射这些:

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

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

// 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);
Run Code Online (Sandbox Code Playgroud)

此外,我没有使用sproc来更新节点映射.相反,我们有一组内部命令,它们将根据Parent&Children属性派生/计算Ancestors和Offspring属性.但最终,你最终能够像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);
Run Code Online (Sandbox Code Playgroud)

主实体与其祖先/后代之间的桥实体的原因再次是因为该实体允许您获得分离.此外,通过将其声明为标识关系,您可以从集合中删除节点,而无需在它们上显式调用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));
Run Code Online (Sandbox Code Playgroud)

  • 你如何设置`Separation`值? (6认同)
  • 这是一个很好的方法,我只想补充说它被称为闭包表,并在这些幻灯片http://www.slideshare.net/billkarwin/models-for-hierarchical-data中清楚地描述. (5认同)

haz*_*zik 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; }
}
Run Code Online (Sandbox Code Playgroud)

祖先将包含该实体的所有祖先(父母,祖父母,曾祖父母等),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"));
    }    
}
Run Code Online (Sandbox Code Playgroud)

现在有了这个结构,你可以像下面那样进行急切的提取

context.Trees.Include(x => x.Descendants).Where(x => x.Id == id).SingleOrDefault()
Run Code Online (Sandbox Code Playgroud)

此查询将加载实体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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)


小智 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
Run Code Online (Sandbox Code Playgroud)

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

  SET @sql = @sql + 'SELECT t.ChildId, t.ParentId FROM dbo.Tree t '
Run Code Online (Sandbox Code Playgroud)

但应该是

  SET @sql = @sql + 'SELECT h.ChildId, t.ParentId FROM dbo.Tree t '
Run Code Online (Sandbox Code Playgroud)

我还添加了一些代码,允许您不仅在填充时更新TreeHierarchy表.

  SET @sql = @sql + 'EXCEPT SELECT t.ChildId, t.ParentId FROM dbo.TreeHierarchy t '
Run Code Online (Sandbox Code Playgroud)

而且神奇.此过程或更确切地说TreeHierarchy允许您仅通过包含祖先(不是子项而不是后代)来加载子项.

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

现在,YourDbContext将返回带有子节点的rootNode,rootName的子节点(孙子节点)的子节点,依此类推.