在 Entity Framework 中获取 parent 个部门节点

Get parent department node in Entity Framework

我有一个 SQL table 这样的:

DepartmentID 是部门的 parent。我通过这个 table(在 ASP.net (C#) 项目中):

构建了一棵树

上面树中的记录是:

我需要在这棵树中获得 parents。

我可以在 SQL 服务器中这样做(例如 id=2id 是输入参数):

with cte1
as
(
select id,name,DepartmentID, 0 AS level 
from Department 
where id =2
union all 
select Department.ID,Department.name,Department.DepartmentID, level+1  
from Department 
inner join cte1 on Department.ID=cte1.DepartmentID
)
select * from cte1

输出(id=2 (A))

输出(id=4 (A1))

我知道 EF 不支持 cte,但我需要在 EF 中得到这个结果。

如果有人能解释这个问题的解决方案,那将非常有帮助。

我能想到的最简单的方法是在 EF 中映射关系,然后检索所有部门,然后从该列表中获取根父级。所有这些都应该加载到内存中,EF 将通过映射处理树结构。或者,您可以启用延迟加载并只获取父项,但随后对于每个子项或子集,EF 在检索期间将执行一个查询。

型号

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int? DepartmentId { get; set; }
    public Department ParentDepartment { get; set; }
    public virtual ICollection<Department> ChildDepartments { get; set; }
}

映射(使用流利的)

public DbSet<Department> Departments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    // other mapping code

    modelBuilder.Entity<Department>()
      .HasOptional(x => x.ParentDepartment)
      .WithMany(x => x.ChildDepartments)
      .HasForeignKey(x => x.DepartmentId);

    // other mapping code
}

急切检索根父级

using (var context = new YourDbContext())
{
    var allDepartments = context.Departments.ToList(); // eagerly return everything
    var rootDepartment = allDepartments.Single(x => x.DepartmentId == null);
}

仅检索根父级然后使用延迟加载,请注意 DbContext 需要可用于延迟加载才能工作,并且还必须在 DbContext 上启用它

using (var context = new YourDbContext())
{
    var rootDepartment = context.Departments.Single(x => x.DepartmentId == null);
   // do other stuff, as soon as context is disposed you cant lazy load anymore
}

尝试其中之一,

1-

int _ID = 2; // ID criteria
List<object> result = new List<object>(); // we will use this to split parent at child, it is object type because we need Level

 var departments = entites.Departments.Where(x => x.ID == _ID).SelectMany(t => entites.Departments.Where(f => f.ID == t.DepartmentID),
            (child, parent) => new { departmentID = child.DepartmentID, Name = child.Name, ID = child.ID, level = 0,
                Parent = new { DepartmentID = parent.DepartmentID, Name = parent.Name, ID = parent.ID, level = 1 }});
        // first we check our ID (we take A from where criteria), then with selectmany T represents the Department A, we need
        // department A's departmentID to find its parent, so another where criteria that checks ID == DepartmentID, so we got T and the new list 
        // basically child from first where parent from second where, and object created.

        // for showing the results
        foreach (var item in departments)
        {
            result.Add(new { DepartmentID = item.departmentID,ID = item.ID, level= item.level,Name = item.Name}); // child added to list
            result.Add(new { DepartmentID = item.Parent.DepartmentID, ID = item.Parent.ID, level = item.Parent.level, Name = item.Parent.Name }); // parent added to list
        }

结果;

2-

List<object> childParent = new List<object>();
// basically get the child first
Departments child1 = entites.Departments.Where(x => x.ID == _ID).FirstOrDefault();
// find parent with child object
Departments parent1 = entites.Departments.Where(x => x.ID == child1.DepartmentID).FirstOrDefault();
// create child object with level
childParent.Add(new { child1.DepartmentID, child1.ID,child1.Name , level = 0});
// create parent object with level
childParent.Add(new { parent1.DepartmentID,parent1.ID,parent1.Name, level = 1 });

Result(不是同一张图片,检查列Header文本);

编辑 1:

3- 另一种方法是,通过将 ID 作为输入并假设 ID 列是唯一的,因此数组中始终有 2 个值,并且通过返回列表,项目的索引实际上代表了它们的级别。 (不会添加结果,因为它们相同 :))。顺便说一句,您也可以使用 Union 而不是 Concat

var ress = list.Where(x=> x.ID ==2)
               .SelectMany(x=> list.Where(c=> c.ID == x.ID).Concat(list.Where(s => s.ID == x.DepartmentID))).ToList();

            DataTable dt = new DataTable();
            dt.Columns.Add("DepartmentID");
            dt.Columns.Add("ID");
            dt.Columns.Add("Name");
            dt.Columns.Add("Level");
            for (int i = 0; i < ress.Count(); i++)
            {
                dt.Rows.Add(ress[i].DepartmentID, ress[i].ID, ress[i].Name, i);
            }
            dataGridView1.DataSource = dt;

编辑 2

linq没有cte,基本都是用view,sp是首选,这里有一个解决办法,可能有点push。无论如何它给出了结果。

 List<Departments> childParent = new List<Departments>();
 // or basically get the child first
 Departments child1 = entites.Departments.Where(x => x.ID == 7).FirstOrDefault();
 // find parent with child object
 Departments parent1 = entites.Departments.Where(x => x.ID == child1.DepartmentID).FirstOrDefault();
 // create child object with level
 Departments dep = new Departments(); // I add to department class a string level field
 dep.DepartmentID = child1.DepartmentID;
 dep.ID = child1.ID;
 dep.Name = child1.Name;
 dep.level = 0; // first item
 childParent.Add(dep);
 // create parent object with level
 dep = new Departments();
 dep.DepartmentID = parent1.DepartmentID;
 dep.ID = parent1.ID;
 dep.Name = parent1.Name;
 dep.level = 1; // parent one
 childParent.Add(dep);

 while (childParent.Select(t => t.DepartmentID).Last() != null) // after added to list now we always check the last one if it's departmentID is null, if null we need to stop searching list for another parent
 {
       int? lastDepID = childParent.Last().DepartmentID; // get last departmentID 
       Departments tempDep = entites.Departments.Single(x => x.ID == lastDepID); // find as object
       tempDep.level = childParent.Last().level + 1; // increase last level
       childParent.Add(tempDep); // add to list          
 }

(添加了另一个C1来检查第4级)

希望有所帮助,

下面是简单的控制台项目程序 class 代码。

GetParentSet方法的入参可以用不同的ID来查看。

class Program
{
    static void Main(string[] args)
    {
      Program p = new Program();
      var result=  p.GetParentSet(6);
        foreach(var a in result)
        {
            Console.WriteLine(string.Format("{0} {1} {2}",a.ID,a.Name,a.DepartmentId));
        }
       Console.Read();
    }



    private List<Department> GetParentSet(int id)
    {
        List<Department> result = new List<Department>(); //Result set
        using (RamzDBEntities context = new RamzDBEntities())
        {
            var nodeList = context.Departments.Where(t=>t.ID<=id).ToList(); //Get All the the entries where ID is below or greater than the given to the list
            var item = nodeList.Where(a => a.ID == id).SingleOrDefault(); //Get the default item for the given ID
            result.Add(item); //Add it to the list. This will be the leaf of the tree

            int size = nodeList.Count(); //Get the nodes count
            for (int i = size;  i >= 1;i--)
            {
                var newItem=    nodeList.Where(j => j.ID == item.DepartmentId).SingleOrDefault(); //Get the immediate parent. This can be done by matching the leaf Department ID against the parent ID
                if (item!=null && !result.Contains(newItem)) //If the selcted immediate parent item is not null and it is not alreday in the list
                {
                    result.Add(newItem); //Add immediate parent item  to the list
                }
                if (newItem.ID == 1) //If the immediate parent item  ID is 1 that means we have reached the root of the tree and no need to iterate any more.
                    break;
                item = newItem; //If the immediate parent item ID is not 1 that means there are more iterations. Se the immediate parent as the leaf and continue the loop to find its parent

            }
        }
        return result; //return the result set
    }
}

代码本身是不言自明的。但是下面是解释。希望这会有所帮助!

  • 首先,ID小于或等于给定ID的所有条目是 分配给列表
  • 然后获取树的叶子并将其添加到名为result的列表中。这是我们结果集的第一个元素
  • 我们按降序遍历检索到的条目。通过将父 ID 等同于叶的部门 ID
  • 来获取叶的直接父级
  • 如果此直接父级不为 null 并且它不在列表中,请将其添加到列表中。
  • 将直接父项作为叶子继续循环,这样我们就可以得到直接父项的父项。
  • 继续这个直到我们到达树的根。
  • 如果直接父 ID=1,这意味着我们已经到达树的根,我们可以打破循环。

由于您生成了 edmx,因此您已经为 DbContext 和模型 Classes 生成了代码,包括 this 屏幕截图中的部门。

您不应修改它们,因为它们可能(将)在任何模型操作中被 EF 工具覆盖。幸运的是,两个 classes 都生成为 partial,因此创建者考虑了希望安全地自定义它的人们。

下面的示例是为了简化实施而不是为了获得最佳性能。我假设包含 Departments 的 table 不是很大,层次结构中的嵌套级别也不是很深。

  1. 在您的项目中创建一个新的 Class(*.cs 文件)并通过您的自定义方法或 属性 扩展自动生成的部门 class:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace CustomEF.EFStuff
    {
    
    
        public partial class Departments
        {
            public  List<Departments> Hierarchy {
                get {
                    List<Departments> retVal = new List<Departments>();
                    retVal.Add(this);
                    using (YourAutoGeneratedContext ctx = new YourAutoGeneratedContext())
                    {
                        Departments tmp = this;
                        while(tmp.DepartmentID != null)
                        {
                            tmp = ctx.Departments.First(d => d.ID == tmp.DepartmentID);
                            retVal.Add(tmp);
                        }
                    }
                    return retVal;
    
                }
                private set { }
            }
        }
    }
    

    当您扩展部分 class 时,请确保将其放在相同的命名空间中。在我的例子中,我将我的项目命名为 CustomEF,并将 edmx 文件放在 EFStuff 子文件夹中,因此生成器将自动生成的 class 放在 CustomEF.EFStuff 命名空间中。

    上面的示例将允许您获取任何 Departments 对象的层次结构,例如

    int level = 0;
    foreach(Departments d in someDepartmentObject.Hierarchy)
    {
        Console.WriteLine(d.ID.ToString() + ", " + d.DepartmentID.ToString() + ", " + d.Name +", " +(level++).ToString());
    }
    
  2. 如果您还需要从一些您有 ID 但没有对象的代码中获取层次结构,您可以另外创建另一个 class(*.cs 文件)我将扩展自动生成的上下文。

    using System.Collections.Generic;
    using System.Linq;
    
    namespace CustomEF.EFStuff
    {
    
    
        public partial class YourAutoGeneratedContext
        {
            public List<Departments> GetDepartmentHierarchy(int departmentId)
            {
                Departments mydep = this.Departments.FirstOrDefault(d => d.ID == departmentId);
                if (mydep == null)
                {
                    throw new System.Data.Entity.Core.ObjectNotFoundException("There is no department with ID = " + departmentId.ToString());
                }
                return mydep.Hierarchy;
            }
        }
    }
    

    或者在这种情况下,您可能希望将实现完全移至上下文 class,而根本不扩展部门 class(并且您不必创建额外的实例您的上下文,您将可以使用 this

    using System.Collections.Generic;
    using System.Linq;
    
    namespace CustomEF.EFStuff
    {
    
    
        public partial class YourAutoGeneratedContext
        {
            public List<Departments> GetDepartmentHierarchy(int departmentId)
            {
                Departments tmp = this.Departments.FirstOrDefault(d => d.ID == departmentId);
                if (tmp == null)
                {
                    throw new System.Data.Entity.Core.ObjectNotFoundException("There is no department with ID = " + departmentId.ToString());
                }
                List<Departments> retVal = new List<Departments>();
                retVal.Add(tmp);
    
                while (tmp.DepartmentID != null)
                {
                    tmp = this.Departments.First(d => d.ID == tmp.DepartmentID);
                    retVal.Add(tmp);
                }
    
                return retVal;
            }
        }
    }
    

    作为另一个简单的使用示例:

    YourAutoGeneratedContext ctx = new YourAutoGeneratedContext();
    level = 0;
    foreach (Departments currentHier in ctx.GetDepartmentHierarchy(10))
    {
        Console.WriteLine(currentHier.ID.ToString() + ", " + currentHier.DepartmentID.ToString() + ", " + currentHier.Name + ", " + (level++).ToString());
    }
    

我不知道您对数据库中的数据有多信任。您可能需要执行一些检查,包括交叉引用部门以防止无限循环。

请注意,正式术语 'to extend a class' 可能适用于扩展方法,而不是 partial classes。我用这个词是因为找不到更好的词了。如果出于某种原因,您需要 method/property 返回 EF 本机 DbSet<> 而不是 List<>,那么您可能想要使用扩展方法。在这种情况下,您可能需要查看:https://shelakel.co.za/entity-framework-repository-pattern/

这些 post 与您的 question.please 相似,请参阅:

writing-recursive-cte-using-entity-framework-fluent-syntax-or-inline-syntax
converting-sql-statement-that-contains-with-cte-to-linq

我认为没有办法编写单个 LINQ to SQL 查询来获取所有的查询 但是,LINQ 支持执行查询的方法(奇怪的是称为 DataContext.ExecuteQuery)。看起来您可以使用它来调用 SQL 的任意片段并将其映射回 LINQ。

看到这个post: common-table-expression-in-entityframework

在 EF6 中获取所有父节点到根节点的示例。

public class Department
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Name { get; set; }

    public int? ParentId { get; set; }

    public virtual Department Parent { get; set; }

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

    private IList<Department> allParentsList = new List<Department>();

    public IEnumerable<Department> AllParents()
    {
        var parent = Parent;
        while (!(parent is null))
        {
            allParentsList.Add(parent);
            parent = parent.Parent;
        }
        return allParentsList;
    }
}

使用 include 关键字。

_context.Invoices.Include(x => x.Users).Include(x => x.Food).ToList();