How does Entity Framework work with recursive hierarchies? Include() seems not to work with it

前端 未结 15 1564
再見小時候
再見小時候 2020-11-28 21:53

I have an Item. Item has a Category.

Category has ID, Name, Parent

相关标签:
15条回答
  • 2020-11-28 22:17

    Here is a clever recursive function I found here that would work for this:

    public partial class Category
    {
        public IEnumerable<Category> AllSubcategories()
        {
            yield return this;
            foreach (var directSubcategory in Subcategories)
                foreach (var subcategory in directSubcategory.AllSubcategories())
                {
                    yield return subcategory;
                }
        }
    }
    
    0 讨论(0)
  • 2020-11-28 22:21

    Let me offer my simple solution that fits needs to enable/disable the branch of hierarchical data of the selected department's structure of an organization.

    The table Departments looks according this SQL

    CREATE TABLE [dbo].[Departments](
        [ID] [int] IDENTITY(1,1) NOT NULL,
        [Name] [nvarchar](1000) NOT NULL,
        [OrganizationID] [int] NOT NULL,
        [ParentID] [int] NULL,
        [IsEnabled] [bit] NOT NULL, 
     CONSTRAINT [PK_Departments] PRIMARY KEY CLUSTERED 
    (
        [ID] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    GO
    

    C# code provides a very simple approach that work fine for me. 1. It returns the complete table asynchronously. 2. It changes property for the linked rows.

    public async Task<bool> RemoveDepartmentAsync(int orgID, int depID)
                {
                    try
                    {
                        using (var db = new GJobEntities())
                        {
                            var org = await db.Organizations.FirstOrDefaultAsync(x => x.ID == orgID); // Check if  the organization exists
                            if (org != null)
                            {
                                var allDepartments = await db.Departments.ToListAsync(); // get all table items
                                var isExisting = allDepartments.FirstOrDefault(x => x.OrganizationID == orgID && x.ID == depID);
                                if (isExisting != null) // Check if the department exists
                                {
                                    isExisting.IsEnabled = false; // Change the property of visibility of the department
                                    var all = allDepartments.Where(x => x.OrganizationID == orgID && x.ID == isExisting.ID).ToList();
                                    foreach (var item in all)
                                    {
                                        item.IsEnabled = false;
                                        RecursiveRemoveDepartment(orgID, item.ID, ref allDepartments); // Loop over table data set to change property of the linked items
                                    }
                                    await db.SaveChangesAsync();
                                }
                                return true;
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        logger.Error(ex);
                    }
    
                    return false;
                }
    
                private void RecursiveRemoveDepartment(int orgID, int? parentID, ref List<Department> items)
                {
                    var all = items.Where(x => x.OrganizationID == orgID && x.ParentID == parentID);
                    foreach (var item in all)
                    {
                        item.IsEnabled = false;
                        RecursiveRemoveDepartment(orgID, item.ID, ref items);
                    }
                }
    

    This approach works very fast for relative small amount of records I guess less 100000. Probably for big set of data you have to implement server side stored function.

    Enjoy!

    0 讨论(0)
  • 2020-11-28 22:29

    try this

    List<SiteActionMap> list = this.GetQuery<SiteActionMap>()
                    .Where(m => m.Parent == null && m.Active == true)
                    .Include(m => m.Action)
                    .Include(m => m.Parent).ToList();    
    
    if (list == null)
        return null;
    
    this.GetQuery<SiteActionMap>()
        .OrderBy(m => m.SortOrder)
        .Where(m => m.Active == true)
        .Include(m => m.Action)
        .Include(m => m.Parent)
        .ToList();
    
    return list;
    
    0 讨论(0)
  • 2020-11-28 22:30

    If you definitely want the whole hierarchy loaded, then if it was me I'd try writing a stored procedure who's job it is to return all the items in a hierarchy, returning the one you ask for first (and its children subsequently).

    And then let the EF's relationship fixup ensure that they are all hooked up.

    i.e. something like:

    // the GetCategoryAndHierarchyById method is an enum
    Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();
    

    If you've written your stored procedure correctly, materializing all the items in the hierarchy (i.e. ToList()) should make EF relationship fixup kicks in.

    And then the item you want (First()) should have all its children loaded and they should have their children loaded etc. All be populated from that one stored procedure call, so no MARS problems either.

    Hope this helps

    Alex

    0 讨论(0)
  • 2020-11-28 22:30

    You don't want to do recursive loading of the hierarchy, unless you are allowing a user to iteratively drill down/up the tree: Every level of recursion is another trip to the database. Similarly, you'll want lazy loading off to prevent further DB trips as you're traversing the hierarchy when rendering to a page or sending over a webservice.

    Instead, flip your query: Get Catalog, and Include the items in it. This will get you all items both hierarchically (navigation properties) and flattened, so now you just need to exclude the non-root elements present at the root, which should be pretty trivial.

    I had this problem and provided a detailed example of this solution to another, here

    0 讨论(0)
  • 2020-11-28 22:31

    It could be dangerous if you did happen to load all recursive entities, especially on category, you could end up with WAY more than you bargained for:

    Category > Item > OrderLine > Item
                      OrderHeader > OrderLine > Item
             > Item > ...
    

    All of a sudden you've loaded most of your database, you could have also loaded invoices lines, then customers, then all their other invoices.

    What you should do is something like the following:

    var qryCategories = from q in ctx.Categories
                        where q.Status == "Open"
                        select q;
    
    foreach (Category cat in qryCategories) {
        if (!cat.Items.IsLoaded)
            cat.Items.Load();
        // This will only load product groups "once" if need be.
        if (!cat.ProductGroupReference.IsLoaded)
            cat.ProductGroupReference.Load();
        foreach (Item item in cat.Items) {
            // product group and items are guaranteed
            // to be loaded if you use them here.
        }
    }
    

    A better solution however is to construct your query to build an anonymous class with the results so you only need to hit your datastore once.

    var qryCategories = from q in ctx.Categories
                        where q.Status == "Open"
                        select new {
                            Category = q,
                            ProductGroup = q.ProductGroup,
                            Items = q.Items
                        };
    

    This way you could return a dictionary result if required.

    Remember, your contexts should be as short lived as possible.

    0 讨论(0)
提交回复
热议问题