Most efficient method of self referencing tree using Entity Framework

后端 未结 6 2147
萌比男神i
萌比男神i 2020-11-28 02:12

So I have a SQL table which is basically

ID, ParentID, MenuName, [Lineage, Depth]

The last two columns are auto-computed to help with sear

6条回答
  •  无人及你
    2020-11-28 02:32

    Another implementation option that I've recently worked on...

    My tree is very simple.

    public class Node
    {
        public int NodeID { get; set; }
        public string Name { get; set; }
        public virtual Node ParentNode { get; set; }
        public int? ParentNodeID { get; set; }
        public virtual ICollection ChildNodes { get; set; }
        public int? LeafID { get; set; }
        public virtual Leaf Leaf { get; set; }
    }
    public class Leaf
    {
        public int LeafID { get; set; }
        public string Name { get; set; }
        public virtual ICollection Nodes { get; set; }
    }
    

    My requirements, not so much.

    Given a set of leaves and a single ancestor, show children of that ancestor who have descendants that have leaves within the set

    An analogy would be a file structure on disk. The current user has access to a subset of files on the system. As the user opens nodes in the file system tree, we only want to show that user nodes that will, eventually, lead them to the files they can see. We don't want to show them file paths to files they do not have access to (for security reasons, e.g., leaking the existence of a document of a certain type).

    We want to be able to express this filter as an IQueryable, so we can apply it to any node query, filtering out unwanted results.

    To do this, I created a Table Valued Function that returns the descendants for a node in the tree. It does this via a CTE.

    CREATE FUNCTION [dbo].[DescendantsOf]
    (   
        @parentId int
    )
    RETURNS TABLE 
    AS
    RETURN 
    (
        WITH descendants (NodeID, ParentNodeID, LeafID) AS(
            SELECT NodeID, ParentNodeID, LeafID from Nodes where ParentNodeID = @parentId
            UNION ALL
            SELECT n.NodeID, n.ParentNodeID, n.LeafID from Nodes n inner join descendants d on n.ParentNodeID = d.NodeID
        ) SELECT * from descendants
    )
    

    Now, I'm using Code First, so I had to use

    https://www.nuget.org/packages/EntityFramework.Functions

    in order to add the function to my DbContext

    [TableValuedFunction("DescendantsOf", "Database", Schema = "dbo")]
    public IQueryable DescendantsOf(int parentID)
    {
        var param = new ObjectParameter("parentId", parentID);
        return this.ObjectContext().CreateQuery("[DescendantsOf](@parentId)", param);
    }
    

    with a complex return type (couldn't reuse Node, looking into that)

    [ComplexType]
    public class NodeDescendant
    {
        public int NodeID { get; set; }
        public int LeafID { get; set; }
    }
    

    Putting it all together allowed me, when the user expands a node in the tree, to get the filtered list of child nodes.

    public static Node[] GetVisibleDescendants(int parentId)
    {
        using (var db = new Models.Database())
        {
            int[] visibleLeaves = SuperSecretResourceManager.GetLeavesForCurrentUserLol();
    
            var targetQuery = db.Nodes as IQueryable;
    
            targetQuery = targetQuery.Where(node =>
                    node.ParentNodeID == parentId &&
                    db.DescendantsOf(node.NodeID).Any(x => 
                                    visibleLeaves.Any(y => x.LeafID == y)));
    
            // Notice, still an IQueryable.  Perform whatever processing is required.
            SortByCurrentUsersSavedSettings(targetQuery);
    
            return targetQuery.ToArray();
        }
    }
    

    It's important to note that the function is executed on the server, not in the application. Here's the query that gets executed

    SELECT 
        [Extent1].[NodeID] AS [NodeID], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[ParentNodeID] AS [ParentNodeID], 
        [Extent1].[LeafID] AS [LeafID]
        FROM [dbo].[Nodes] AS [Extent1]
        WHERE ([Extent1].[ParentNodeID] = @p__linq__0) AND ( EXISTS (SELECT 
            1 AS [C1]
            FROM ( SELECT 
                [Extent2].[LeafID] AS [LeafID]
                FROM [dbo].[DescendantsOf]([Extent1].[NodeID]) AS [Extent2]
            )  AS [Project1]
            WHERE  EXISTS (SELECT 
                1 AS [C1]
                FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]
                WHERE [Project1].[LeafID] = 17
            )
        ))
    

    Note the function call within the query above.

提交回复
热议问题