使用LINQ尋訪關聯性樹狀資料
前言
對於Menu資料處理上,客戶希望可以從末節點(ex. Baseball)來取出所有父節點的資料;反之,亦可以從父節點(ex. Prpduct)來獲得所有子節點資料。由於各個Menu項目都會有從屬關係,因此可利用此特性來尋訪出一系列關聯性資料,以下說明。
實作
首先簡述一下Menu資料庫表單資料,其中ParentMenuId表示該Menu項目所屬之父層Menu項目流水號,因此我們就可以透過ParentMenuId一路追尋父層Menu,來達到階層式Menu畫面的呈現。
在 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向父節點取出所有關聯資料
父節點 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向子節點取出所有關聯資料
最後順道紀錄一下直接使用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
參考資訊
http://www.codeproject.com/Tips/674287/Recursive-Queries-in-Microsoft-SQL-Server
https://msdn.microsoft.com/zh-tw/library/9k7k7cf0.aspx
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !