【问题标题】:One to many recursive relationship with Code First与 Code First 的一对多递归关系
【发布时间】:2015-02-27 11:58:10
【问题描述】:

我正在尝试使用 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&lt;Branch&gt;().HasOptional&lt;Branch&gt;(b =&gt; b.Parent).WithMany(b =&gt; b.Children).HasForeignKey(b =&gt; b.ParentId); 但我不确定我是否需要这个。
  2. 对于给定的分支,我想检索所有子级(一直到层次结构)。到目前为止,这是我想出的:

.

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

第二个 sn-p 不是我写的。我在 StackOverflow 的其他地方找到了它。老实说,我几乎不明白它应该如何工作。

当我运行我的应用程序并调用 GetBranches()(我尝试了几个不同的分支)时,我在 Flatten() 方法中收到了一个异常。错误消息说:“值不能为空。 参数名称:源”。不幸的是,这并没有给我任何线索这里出了什么问题。

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

【问题讨论】:

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


    【解决方案1】:

    异常原因

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

    b => b.Children
    

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

    selector(node)
    

    selector是lambda表达式b =&gt; 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 将执行填充集合的查询。如果没有孩子,则集合将为空,而不是 null。

    扁平化解释

    所以在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(公用表表达式)的视图。但那是另一回事...

    【讨论】:

    • 一切都如您所描述的那样完美。我无法想象我忘记了“virtual”关键字,因为我知道它对延迟加载的影响。但是你解释它的方式——以及你必须投入的工作量:我真的不知道该说什么。太感谢了。我希望我能给你 100 学分或其他任何东西 ;-) 祝你新年快乐。再次感谢!!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多