与Code First的一对多递归关系

时间:2014-12-31 11:18:29

标签: entity-framework ef-code-first entity-framework-6

我正在尝试使用EF 6.1.2 Code First实现一个简单的自引用关系。

public class Branch 
{
    [Key]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    public int? ParentId { get; set; }

    [ForeignKey("ParentId")]
    public virtual Branch Parent { get; set; }

    public ICollection<Branch> Children { get; set; } // direct successors
}

在我的应用程序中,我只有一个根分支。除了这个单根分支外,每个分支只有一个父分支(根分支的parentId为NULL)。除此之外,每个分支都可以有[0..n]个子分支。

我有两个问题:

  1. 我是否需要在OnModelCreating(DbModelBuilder modelBuilder)中指定任何额外的FluentApi代码,以使EF了解这种一对多的自引用关系?我试过这个:modelBuilder.Entity<Branch>().HasOptional<Branch>(b => b.Parent).WithMany(b => b.Children).HasForeignKey(b => b.ParentId);但我不确定我是否需要这个。
  2. 对于给定的分支,我想要检索所有子级(一直到层次结构)。这就是我到目前为止所提出的:
  3.  public IEnumerable<Branch> GetBranches(Branch anyBranch)
     {
         return anyBranch.Flatten(b => b.Children);
     }
    

     public static IEnumerable<T> Flatten<T>(this T node, Func<T, IEnumerable<T>> selector)
     {
         return selector(node).SelectMany(x => Flatten(x, selector))
                                .Concat(new[] { node });
     }
    

    第二个片段不是来自我。我发现它在StackOverflow上的其他地方。说实话,我几乎不明白它应该如何工作。

    当我运行我的应用程序并调用GetBranches()时(我尝试使用几个不同的分支),我在Flatten()方法中收到一个异常。错误消息显示:&#34;值不能为空。 参数名称:source&#34;。不幸的是,这并不能让我知道这里出了什么问题。

    我希望有人可以帮助我吗?非常感谢!

1 个答案:

答案 0 :(得分:9)

异常原因

异常是由Select集合上的SelectManynull引起的,在您的情况下是

的结果
b => b.Children

对于层次结构中的每个分支,Children集合在到达部分时被访问

selector(node)

selector是lambda表达式b => b.Children,它与方法

相同
IEnumerable<Branch> anonymousMethod(Branch b)
{
    return b.Children;
}

实际发生的是b.Children.SelectMany(...)null.SelectMany(...),它会引发您看到的异常。

预防

但为什么这些Children个集合为空?

这是因为延迟加载不会发生。要启用延迟加载,集合必须为virtual

public virtual ICollection<Branch> Children { get; set; }

当EF从数据库中获取Branch对象时,它会创建一个proxy对象,一个派生自Branch的对象,它通过能够延迟加载的代码覆盖虚拟属性。现在,当b.Children被解决时,EF将执行填充集合的查询。如果没有子节点,则集合将为空,而不是空。

展平解释

那么Flatten方法中发生的事情是,首先获取分支的子项(selector(node)),然后在每个子项(SelectMany)上Flatten再次调用方法(现在只是方法Flatten(x, selector),而不是扩展方法)。

Flatten方法中,每个节点都添加到其子集合中.Concat(new[] { node }),因此最后,返回层次结构中的所有节点(因为Flatten返回节点进入它。)

一些评论

  1. 我想将父节点放在集合的顶部,因此我将Flatten方法更改为

    public static IEnumerable<T> Flatten<T>(this T node, Func<T,IEnumerable<T>> selector)
    {
        return new[] { node }
            .Concat(selector(node).SelectMany(x => Flatten(x, selector)));
    }    
    
  2. 通过延迟加载获取层次结构是非常低效的。实际上,LINQ不是最适合查询层次结构的工具。有效地执行此操作将需要数据库中使用CTE(公用表表达式)的视图。但这是一个不同的故事......