[Entity Framework][LINQ] 使用LINQ尋訪關聯性樹狀資料

使用LINQ尋訪關聯性樹狀資料

前言

對於Menu資料處理上,客戶希望可以從末節點(ex. Baseball)來取出所有父節點的資料;反之,亦可以從父節點(ex. Prpduct)來獲得所有子節點資料。由於各個Menu項目都會有從屬關係,因此可利用此特性來尋訪出一系列關聯性資料,以下說明。

image

 

實作

首先簡述一下Menu資料庫表單資料,其中ParentMenuId表示該Menu項目所屬之父層Menu項目流水號,因此我們就可以透過ParentMenuId一路追尋父層Menu,來達到階層式Menu畫面的呈現。

image

在 Entity Framework 中,當 Menu 表單中具有外來鍵(Foreign Key) 時,會額外在 Menu 物件中建立 SubMenus 與 ParentMenu 兩個導航屬性(Navigation Property),以此作為兩關聯性類型間的連結;其中 SubMenus 表示此 Menu 項目所包含的下一層子Menu項目(多筆),而 ParentMenu 則表示此 Menu 項目的父 Menu 項目(單筆)。Menu資料模型如下所示。

public class Menu
{
    // Constructor
    public Menu()
    {
        this.SubMenus = new HashSet<Menu>();
    }

    // Properties
    public int MenuId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Nullable<int> ParentMenuId { get; set; }


    // Navigation Properties
    public virtual ICollection<Menu> SubMenus { get; set; }
    public virtual Menu ParentMenu { get; set; }
}

 

接著實作尋訪擴充功能,主要利用Stack作為尋訪節點的中繼站,以此不斷追尋父/子節點資料。

public static class EnumerableExtensions
{
	// 向上尋訪所有父節點
	public static IEnumerable<T> Traverse<T>(this IEnumerable<T> items, Func<T, T> parentSelector)
	{
		var stack = new Stack<T>(items);
		while(stack.Any())
		{
			var item = stack.Pop();
			yield return item;
			
			// get item's parent
			var parent = parentSelector(item);
			if (parent != null)
			{
				// push parent in stack
				// find parent's parent later
				stack.Push(parent);
			}
		}
	}
	
	// 向下尋訪所有子節點
	public static IEnumerable<T> Traverse<T>(this IEnumerable<T> items, Func<T, IEnumerable<T>> childSelector)
	{
		var stack = new Stack<T>(items);
		while(stack.Any())
		{
			var item = stack.Pop();
			yield return item;
			
			// get item's children
			foreach(var child in childSelector(item))
			{
				// push each child in stack
				// find child's children later
				stack.Push(child);
			}
		}
	}
	
}

完成後即可利用上述尋訪擴充功能來查詢資料

 

子節點 to 父節點

預期取出Baseball之所有父結點Menu項目

void Main()
{
	var db = new TESTEREntities();
			
	// 從棒球向上尋訪所有父級Menu項目
	db.Menus.Where(u => u.MenuId == 8)
			.Traverse(m => m.ParentMenu)
			.Dump();
}

確實從Baseball向父節點取出所有關聯資料

image

 

父節點 to 子節點

預期取出Product之所有子結點Menu項目

void Main()
{
	var db = new TESTEREntities();
			
	// 從產品向下尋訪所有子級Menu項目			
	db.Menus.Where(u => u.MenuId == 2)
			.Traverse(m => m.SubMenus)
			.OrderBy(m=>m.MenuId)
			.Dump();
}

確實從Prodcut向子節點取出所有關聯資料

image

 

最後順道紀錄一下直接使用CTE的SQL處理方式,參考如下。

;WITH menu_recursive(iMenuId,iParentMenuId,iName,LEVEL)
AS
(
    SELECT MenuId, ParentMenuId, Name, 0 AS LEVEL FROM dbo.Menu WHERE MenuId = 8
    UNION ALL
    SELECT MenuId, ParentMenuId, Name, Level + 1 AS LEVEL FROM dbo.Menu 
    INNER JOIN menu_recursive ON menu_recursive.iParentMenuId = dbo.Menu.MenuId 
)
SELECT iMenuId, iParentMenuId, iName, LEVEL FROM menu_recursive

 

image

 

參考資訊

http://stackoverflow.com/questions/18260780/how-to-best-traverse-children-of-childrens-children-to-an-unknown-depth

http://www.codeproject.com/Tips/674287/Recursive-Queries-in-Microsoft-SQL-Server

https://msdn.microsoft.com/zh-tw/library/9k7k7cf0.aspx


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !