【问题标题】:Get parent department node in Entity Framework获取实体框架中的父部门节点
【发布时间】:2016-10-22 11:52:47
【问题描述】:

我有一个这样的 SQL 表:

DepartmentID 是部门的父级。我已经通过这个表构建了一个树(在 ASP.net (C#) 项目中):

上面树中的记录是:

我需要在这棵树上找到父母。

我可以在 SQL Server 中这样做(例如 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 中得到这个结果。

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

【问题讨论】:

  • 您可以将您的 SQL 添加到存储过程中并从 EntityFramework 调用它或使用_dbContext.Database.SqlQuery 直接调用 SQL
  • @HaithamShaddad 我知道我可以通过 SP 做到这一点,但我正在寻找在 EF 中做到这一点的方法。
  • @HaithamShaddad 我无法更改 SQL 表的设计。
  • 最好的方法是在递归查询上创建一个视图(使用 CTE)并在 EF 中映射该视图。

标签: c# sql-server entity-framework database-first


【解决方案1】:

这些帖子与您的问题相似。请参阅:

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。

看到这个帖子: common-table-expression-in-entityframework

【讨论】:

    【解决方案2】:

    我能想到的最简单的方法是在 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
    }
    

    【讨论】:

    • 感谢您的回复,但我在edmx 中有Departments,我可以在EF 中获得root(main)。我需要通过 id 并得到像 Output 这样的父母。
    【解决方案3】:

    尝试其中一种,

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

    结果(不是同一张图片,查看列标题文字);

    编辑 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 是首选但这里有一个解决方案,可能有点推。无论如何,它给出了结果。

     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 级)

    希望有帮助,

    【讨论】:

    • 感谢您的回答。我测试了你的代码。它可以在更高级别 (id=2) 下正常工作,但在两个或更多级别上无法正常工作(id=4 (A1) 请再次查看问题)。
    【解决方案4】:

    下面是简单的控制台项目Program类代码。

    您可以使用不同的 ID 检查 GetParentSet 方法的输入参数。

    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,则表示我们已到达树的根,我们可以中断循环。

    【讨论】:

      【解决方案5】:

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

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

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

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

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

        当你扩展分部类时,确保你把它放在同一个命名空间中。在我的例子中,我将我的项目命名为 CustomEF,并将 edmx 文件放在 EFStuff 子文件夹中,因此生成器将自动生成的类放在 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 但没有对象的代码中获取层次结构,您可以另外创建另一个类(*.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;
                }
            }
        }
        

        或者在这种情况下,您可能希望将实现完全移至 Context 类,而根本不扩展 Departments 类(并且您不必创建上下文的其他实例,您将拥有 @987654328 @ 使用)。

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

      我不知道你对数据库中的数据有多少信任。您可能希望实施一些检查,包括交叉引用部门以防止无限循环。

      请注意,正式术语“扩展类”可能适用于扩展方法而不是partial 类。我用这个词是因为没有更好的词。如果由于某种原因您需要返回 EF 本机 DbSet&lt;&gt; 而不是 List&lt;&gt; 的方法/属性,则可能需要使用扩展方法。在这种情况下,您可能需要查看:https://shelakel.co.za/entity-framework-repository-pattern/

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          【解决方案7】:

          使用包含关键字。

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-09-16
            • 1970-01-01
            • 2022-01-11
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多