第一種方案:
使用遞歸算法,也是使用頻率最多的,大部分開源程序也是這麼處理,不過通常都只用到四級分類。 這種算法的數據庫結構設計最爲簡單。category表中一個字段id,一個字段fid(父id)。這樣能夠根據WHERE id = fid來判斷上一級內容,運用遞歸至最頂層。
分析:經過這種數據庫設計出的無限級,能夠說讀取的時候至關費勁,因此大部分的程序最多3-4級分類,這就足以知足需求,從而一次性讀出全部的數據,再對獲得數組或者對象進行遞歸。自己負荷仍是沒太大問題。可是若是分類到更多級,那是不可取的辦法。
這樣看來這種分類有個好處,就是增刪改的時候輕鬆了…然而就二級分類而言,採用這種算法就應該算最優先了。html
第二種方案:
設置fid字段類型爲varchar,將父類id都集中在這個字段裏,用符號隔開,好比:1,3,6
這樣能夠比較容易獲得各上級分類的ID,並且在查詢分類下的信息的時候,
可使用:SELECT * FROM category WHERE pid LIKE 「1,3%」。git
分 析:相比於遞歸算法,在讀取數據方面優點很是大,可是若查找該分類的全部 父分類 或者 子分類 查詢的效率也不是很高,至少也要二次query,從某種意義看上,我的以爲不太符合數據庫範式的設計。假若遞增到無限級,還需考慮字段是否達到要求,並且 在修改分類和轉移分類的時候操做將很是麻煩。
暫時,在本身項目中用的就是相似第二種方案的解決辦法。就該方案在個人項目中存在這樣的問題, 若是當全部數據記錄達到上萬甚至10W以上後,一次性將因此分類,有序分級的現實出來,效率很低。極有多是項目處理數據代碼效率低帶來的。如今正在改良。github
第三種方案:
無限級分類----改進前序遍歷樹
那 麼理想中的樹型結構應具有哪些特色呢?數據存儲冗餘小、直觀性強;方便返回整個樹型結構數據;能夠很輕鬆的返回某一子樹(方便分層加載);快整獲以某節點 的祖譜路徑;插入、刪除、移動節點效率高等等。帶着這些需求我查找了不少資料,發現了一種理想的樹型結構數據存儲及操做算法,改進的前序遍歷樹模型 (The Nested Set Model)。
原理:
咱們先把樹按照水平方式擺開。從根節點開始(「Food」),而後他的左邊寫 上1。而後按照樹的順序(從上到下)給「Fruit」的左邊寫上2。這樣,你沿着樹的邊界走啊走(這就是「遍歷」),而後同時在每一個節點的左邊和右邊寫上 數字。最後,咱們回到了根節點「Food」在右邊寫上18。下面是標上了數字的樹,同時把遍歷的順序用箭頭標出來了。算法
我 們稱這些數字爲左值和右值(如,「Food」的左值是1,右值是18)。正如你所見,這些數字按時了每一個節點之間的關係。由於「Red」有3和6兩個值, 因此,它是有擁有1-18值的「Food」節點的後續。一樣的,咱們能夠推斷全部左值大於2而且右值小於11的節點,都是有2-11的「Fruit」 節點的後續。這樣,樹的結構就經過左值和右值儲存下來了。這種數遍整棵樹算節點的方法叫作「改進前序遍歷樹」算法。sql
表結構設計:數據庫
那 麼咱們怎樣才能經過一個SQL語句把全部的分類都查詢出來呢,並且要求若是是子類的話前面要打幾個空格以表現是子分類。要想查詢出全部分類很好 辦:SELECT * FROM category WHERE lft>1 AND lft<18 ORDER BY lft這樣的話全部的分類都出來了,可是誰是誰的子類卻分不清,那麼怎麼辦呢?咱們仔細看圖不難發現若是相鄰的兩條記錄的右值第一條的右值比第二條的大那 麼就是他的父類,好比food的右值是18而fruit的右值是11 那麼food是fruit的父類,可是又要考慮到多級目錄。因而有了這樣的設計,咱們用一個數組來存儲上一條記錄的右值,再把它和本條記錄的右值比較,如 果前者比後者小,說明不是父子關係,就用array_pop彈出數組,不然就保留,以後根據數組的大小來打印空格。json
以上內容引用出處:http://www.javashuo.com/article/p-wqxqdbty-mk.html數組
關於第三種設計的更多資料請點擊查看原文,由於過於複雜(太重)被使用的頻率不高。數據庫設計
無限級分類(父子)是一種比較經常使用的表設計,每種設計方式突出優點的同時也帶來缺陷,如:單元測試
第一種方案的設計最簡單,本文後面的內容是在該基礎上,使用 FreeSql 實現 ToTreeList(內存加工樹型)、AsTreeCte(實現遞歸向下/向上查詢),知足大衆平常使用。
FreeSql 是功能強大的對象關係映射技術(O/RM),支持 .NETCore 2.1+ 或 .NETFramework 4.0+ 或 Xamarin,以 MIT 開源協議託管於 github,單元測試數量 4528個,nuget 下載量 151K,支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/達夢/人大金倉/神州通用/Access;
源碼地址:https://github.com/dotnetcore/FreeSql
做者說過:每個功能表明他的一撮頭髮!
FreeSql 導航屬性之中,有針對父子關係的設置方式,ToTreeList/AsTreeCte 依賴該設置,以下:
public class Area { [Column(IsPrimary = true)] public string Code { get; set; } public string Name { get; set; } public string ParentCode { get; set; } [Navigate(nameof(ParentCode)), JsonIgnore] //JsonIgnore 是 json.net 的特性 public Area Parent { get; set; } [Navigate(nameof(ParentCode))] public List<Area> Childs { get; set; } }
定義 Parent 屬性,在表達式中能夠這樣:
fsql.Select<Area>() .Where(a => a.Parent.Parent.Parent.Name == "中國") .First();
定義 Childs 屬性,在表達式中能夠這樣(子查詢):
fsql.Select<Area>() .Where(a => a.Childs.AsSelect().Any(c => c.Name == "北京")) .First();
定義 Childs 屬性,還可使用【級聯保存】、【貪婪加載】 等等操做。
fsql.Delete<Area>().Where("1=1").ExecuteAffrows(); var repo = fsql.GetRepository<Area>(); repo.DbContextOptions.EnableAddOrUpdateNavigateList = true; repo.DbContextOptions.NoneParameter = true; repo.Insert(new Area { Code = "100000", Name = "中國", Childs = new List<Area>(new[] { new Area { Code = "110000", Name = "北京", Childs = new List<Area>(new[] { new Area{ Code="110100", Name = "北京市" }, new Area{ Code="110101", Name = "東城區" }, }) } }) });
配置好父子屬性以後,就能夠這樣用了:
var t1 = fsql.Select<Area>().ToTreeList(); Assert.Single(t1); Assert.Equal("100000", t1[0].Code); Assert.Single(t1[0].Childs); Assert.Equal("110000", t1[0].Childs[0].Code); Assert.Equal(2, t1[0].Childs[0].Childs.Count); Assert.Equal("110100", t1[0].Childs[0].Childs[0].Code); Assert.Equal("110101", t1[0].Childs[0].Childs[1].Code);
查詢數據原本是平面的,ToTreeList 方法將返回的平面數據在內存中加工爲樹型 List 返回。
[ { "ParentCode": null, "Childs": [ { "ParentCode": "100000", "Childs": [ { "ParentCode": "110000", "Childs": [], "Code": "110100", "Name": "北京市" }, { "ParentCode": "110000", "Childs": [], "Code": "110101", "Name": "東城區" } ], "Code": "110000", "Name": "北京" } ], "Code": "100000", "Name": "中國" } ]
若不作數據冗餘的無限級分類表設計,遞歸查詢少不了,AsTreeCte 正是解決遞歸查詢的封裝,方法參數說明:
參數 | 描述 |
---|---|
(可選) pathSelector | 路徑內容選擇,能夠設置查詢返回:中國 -> 北京 -> 東城區 |
(可選) up | false(默認):由父級向子級的遞歸查詢,true:由子級向父級的遞歸查詢 |
(可選) pathSeparator | 設置 pathSelector 的鏈接符,默認:-> |
(可選) level | 設置遞歸層級 |
經過測試的數據庫:MySql8.0、SqlServer、PostgreSQL、Oracle、Sqlite、達夢、人大金倉
姿式一:AsTreeCte() + ToTreeList
var t2 = fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte() //查詢 中國 下的全部記錄 .OrderBy(a => a.Code) .ToTreeList(); //非必須,也可使用 ToList(見姿式二) Assert.Single(t2); Assert.Equal("100000", t2[0].Code); Assert.Single(t2[0].Childs); Assert.Equal("110000", t2[0].Childs[0].Code); Assert.Equal(2, t2[0].Childs[0].Childs.Count); Assert.Equal("110100", t2[0].Childs[0].Childs[0].Code); Assert.Equal("110101", t2[0].Childs[0].Childs[1].Code); // WITH "as_tree_cte" // as // ( // SELECT 0 as cte_level, a."Code", a."Name", a."ParentCode" // FROM "Area" a // WHERE (a."Name" = '中國') // union all // SELECT wct1.cte_level + 1 as cte_level, wct2."Code", wct2."Name", wct2."ParentCode" // FROM "as_tree_cte" wct1 // INNER JOIN "Area" wct2 ON wct2."ParentCode" = wct1."Code" // ) // SELECT a."Code", a."Name", a."ParentCode" // FROM "as_tree_cte" a // ORDER BY a."Code"
姿式二:AsTreeCte() + ToList
var t3 = fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte() .OrderBy(a => a.Code) .ToList(); Assert.Equal(4, t3.Count); Assert.Equal("100000", t3[0].Code); Assert.Equal("110000", t3[1].Code); Assert.Equal("110100", t3[2].Code); Assert.Equal("110101", t3[3].Code); //執行的 SQL 與姿式一相同
姿式三:AsTreeCte(pathSelector) + ToList
設置 pathSelector 參數後,如何返回隱藏字段?
var t4 = fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte(a => a.Name + "[" + a.Code + "]") .OrderBy(a => a.Code) .ToList(a => new { item = a, level = Convert.ToInt32("a.cte_level"), path = "a.cte_path" }); Assert.Equal(4, t4.Count); Assert.Equal("100000", t4[0].item.Code); Assert.Equal("110000", t4[1].item.Code); Assert.Equal("110100", t4[2].item.Code); Assert.Equal("110101", t4[3].item.Code); Assert.Equal("中國[100000]", t4[0].path); Assert.Equal("中國[100000] -> 北京[110000]", t4[1].path); Assert.Equal("中國[100000] -> 北京[110000] -> 北京市[110100]", t4[2].path); Assert.Equal("中國[100000] -> 北京[110000] -> 東城區[110101]", t4[3].path); // WITH "as_tree_cte" // as // ( // SELECT 0 as cte_level, a."Name" || '[' || a."Code" || ']' as cte_path, a."Code", a."Name", a."ParentCode" // FROM "Area" a // WHERE (a."Name" = '中國') // union all // SELECT wct1.cte_level + 1 as cte_level, wct1.cte_path || ' -> ' || wct2."Name" || '[' || wct2."Code" || ']' as cte_path, wct2."Code", wct2."Name", wct2."ParentCode" // FROM "as_tree_cte" wct1 // INNER JOIN "Area" wct2 ON wct2."ParentCode" = wct1."Code" // ) // SELECT a."Code" as1, a."Name" as2, a."ParentCode" as5, a.cte_level as6, a.cte_path as7 // FROM "as_tree_cte" a // ORDER BY a."Code"
更多姿式...請根據代碼註釋進行嘗試