樹形結構數據庫表設計
- 樹形結構我們經常會用它表征某些數據關聯,比如商品分類,企業管理系統菜單或上下級關系等,但在mysql都是以二維表形式生成的數據。設計合適Schema及其對應CRUD算法是實現關系型數據庫中存儲樹。這里我們用django演示
1.簡單版:
- 首先我們要生成如下屬性結構圖:
-
這里拿django演示,好像又說了一遍。
-
常規操作,創建model,添加數據:
class Menu(models.Model): name = models.CharField(max_length=32,verbose_name="名字") # parent為自關聯,關聯父id parent = models.ForeignKey('self', verbose_name='關聯父', blank=True, null=True) class Meta: db_table = "Menu"
-
簡單寫序列化器:
class MenuSerializer(serializers.ModelSerializer): pid = serializers.CharField(source="parent_id") class Meta: model = models.Menu # 這里為了演示,只序列化功能實現基本字段,可自定義序列化字段 fields = ['id',"name","pid"] depth = 1
-
視圖函數:
class Node(object): """ 構造節點 """ def __init__(self,parent,name): self.parent = parent self.name = name def build_tree(nodes,parent): """ 將樹形列表構建dict結構 :param nodes: 查詢的節點列表 :param parent: 當前節點父節點 :return: """ # 用來記錄該節點下所有節點列表 node_list = list() # 用於構建dict結構 tree = dict() build_tree_recursive(tree,parent,nodes,node_list) return tree,node_list def build_tree_recursive(tree,parent,nodes,node_list): """ 遞歸方式構建 :param tree: 構建dict結構 :param parent: 當前父節點 :param nodes: 查詢節點列表 :param node_list: 記錄該節點下所有節點列表 :return: """ # 遍歷循環所有子節點 children = [n for n in nodes if n.parent == parent] node_list.extend([c.name for c in children]) for child in children: # 子節點內創建新的子樹 tree[child.name] = {} # 遞歸去構建子樹 build_tree_recursive(tree[child.name], child, nodes, node_list) def make_mode_list(menu_list): """ json形式生成一個個node對象,且關聯父節點 :param menu_list: 序列化后的菜單表 :return: """ # 返回node root_dict = {} while menu_list: term = menu_list.pop() pid = term.get("pid") if not pid: root_dict[term.get("id")] = Node(None, term.get("name")) continue parent = root_dict.get(int(pid)) if not parent: menu_list.insert(0, term) continue root_dict[term.get("id")] = Node(parent, term.get("name")) return root_dict.values() class Menu(APIView): def get(self,request,*args,**kwargs): # 這里獲取所有數據 menu_obj = models.Menu.objects.all() # 序列化 ser_menu = MenuSerializer(instance=menu_obj,many=True) ser_menu_list = ser_menu.data # 序列化后數據,生成node node_list = make_mode_list(ser_menu_list) tree_dict,tree_list = build_tree(node_list,None) return Response({"code":200,"msg":"OK","tree_dict":tree_dict,"tree_list":tree_list})
- 這里將屬性結構列表轉換成json數據並返回:
-
這種方法實現,簡單直觀,方便。但存在着很大缺點,由於直接記錄節點之間繼承關系,所以Tree的任何CRUD操作很低效,因為這里頻繁的"遞歸"操作,加之不斷訪問數據庫,會增加很大時間開銷。但是對於Tree的規模比較小時,我們可以通過緩存機制來做優化,將Tree的信息載入內存進行處理,避免直接對數據庫IO操作性能開銷。
2.基於左右值Schema設計
-
為了避免對於樹形結構查詢時"遞歸"慚怍,基於Tree的謙虛遍歷設計是一個無遞歸查詢。來報錯該樹數據。
-
創建model:
class TreeMenu(models.Model): name = models.CharField(max_length=32,verbose_name="名字") left = models.IntegerField(verbose_name="左") right = models.IntegerField(verbose_name="右") class Meta: db_table = "TreeMenu"
-
如下展示數據:
-
剛來時看這樣表結構,大部分人不清楚left和right是什么鬼,如何得到的?這樣表設計並沒有保存父子節點繼承關系,但是你去數如下的圖,你會發現,你數的順序就是這棵樹進行前序遍歷順序。
-
舉個例子:這樣可以得到left大於2,並且right小於9的節點都是員工信息子節點。這樣通過left,right就可以找到某個節點所擁有的子節點了,但這僅僅是不夠的。那么怎樣增刪改查呢?
-
比如我們需要
員工信息
管理節點及其子節點們:import os import django from django.db.models import Q os.environ.setdefault("DJANGO_SETTINGS_MODULE", "asyn_test.settings") django.setup() # 啟動django項目 # 引入models.py模型模塊 from api.models import TreeMenu #查詢 left大於等於2,right小於等於9 tree = TreeMenu.objects.filter(Q(left__gte=2)&Q(right__lte=9)).values() print(tree) """ <QuerySet [{'id': 2, 'name': '員工信息', 'left': 2, 'right': 9}, {'id': 3, 'name': '更改信息', 'left': 3, 'right': 8}, {'id': 4, 'name': '更換頭像', 'left': 4, 'right': 5}, {'id': 5, 'name': '綁定手機', 'left': 6, 'right': 7}]> """
-
那么如何查詢某個節點的子節點個數呢,通過左,右值可以得到,子節點總數=(右值-左值-1)/2, 以
員工信息
為例子,它的子節點總數為(9-2-1)/2=3,並且我們為了更直觀展示樹的結構,我們有時需要知道樹種所處的層次,我們可以這么查詢,這里還是以員工信息
為例:layer = TreeMenu.objects.filter(Q(left__lte=2)&Q(right__gte=9)).count() print(layer) """ 2 """ # 可以知道,它是第二層的
-
通過上面方式我們可以得到每一個節點所在層數,這樣我們通過ORM構造一個數據結構,如下:
tree_obj = TreeMenu.objects.all() treemenu_list = [] for term in tree_obj: layer = TreeMenu.objects.filter(Q(left__lte=term.left)&Q(right__gte=term.right)).count() treemenu_list.append( {"id":term.id,"name":term.name,"left":term.left,"right":term.right,"layer":layer} ) print(treemenu_list) # layer 為當前節點所在層數,你會發現獲得所有節點所在的層數 """ [{ 'id': 1, 'name': '首頁', 'left': 1, 'right': 40, 'layer': 1 }, { 'id': 2, 'name': '員工信息', 'left': 2, 'right': 9, 'layer': 2 }, ...] """
-
我們如何獲得某個節點的父節點結構,也很簡單,這里拿
員工信息
舉例:employ = TreeMenu.objects.filter(Q(left__lte=2)&Q(right__gte=9)).values() print(employ) """ <QuerySet [{'id': 1, 'name': '首頁', 'left': 1, 'right': 40}, {'id': 2, 'name': '員工信息', 'left': 2, 'right': 9}]> """
-
如果說我們想在某個節點下添加一個新的子節點,比如我在
更改信息
下插入一個子節點綁定郵箱
.如果田間玩,此時樹形結構應該如下: -
代碼:
edit_info_obj = TreeMenu.objects.filter(pk=3).first() # 獲取"更改信息"當前對象right值 node_point = edit_info_obj.right # 更新 所有節點left大於等於node_point的值 TreeMenu.objects.filter(left__gte=node_point).update(left=F('left') + 2) # 更新 所有節點right大於等於node_point的值 TreeMenu.objects.filter(right__gte=node_point).update(right=F('right') + 2) # 插入數據 為當前 node_point,node_point+1 TreeMenu.objects.create(name="綁定郵箱",left=node_point,right=node_point+1)
-
刪除某個節點,如果想刪除某個節點,會同時刪除該節點的所有子節點,而被刪除的節點個數應該為:(被刪除節點右側值-被刪除節點的左側值+1)/2,而剩下節點左,右值在大於被刪除節點左、右值的情況下進行調整。
-
這樣以刪除
更改信息
為例,那么首先找到該節點左側值和右側值,並且大於等於左側值小於等於右側值的節點都是要刪除節點,也就是left=3,right=10,left<=3 and right<=10,節點都要刪除,其他節點left的值大於刪除節點left值應該減去(被刪除節點右側值-被刪除節點的左側值+1),其他節點right值大於刪除節點right值也應該減去(被刪除節點右側值-被刪除節點的左側值+1)。如下圖: -
代碼:
edit_info_obj = TreeMenu.objects.filter(pk=3).first() left_ponit = edit_info_obj.left#3 right_ponit = edit_info_obj.right#10 # 刪除該節點下大於等於左側值,小於等於右側值的對象 TreeMenu.objects.filter(Q(left__gte=left_ponit)&Q(right__lte=right_ponit)).delete() # 當其他節點左側值大於刪除節點左側值,更新其他節點左側值。 TreeMenu.objects.filter(left__gt=left_ponit).update(left=F('left')-(right_ponit-left_ponit+1)) # 當其他節點右側值大於刪除節點右側值,更新其他節點右側值。 TreeMenu.objects.filter(right__gt=right_ponit).update(right=F('right')-(right_ponit-left_ponit+1))
-
那么django中如何將數據封裝json格式取出呢?上代碼
class SchemaMenuSerializer(serializers.ModelSerializer): layer = serializers.SerializerMethodField() class Meta: model = models.TreeMenu fields = "__all__" depth = 1 # 用於計算當前節點在第幾層。 def get_layer(self,obj): left = obj.left right = obj.right count_layer = models.TreeMenu.objects.filter(Q(left__lte=left) & Q(right__gte=right)).count() return count_layer def parse_data(layer_dict,tree_dict): left = layer_dict.get("left") right = layer_dict.get("right") layer = layer_dict.get("layer") id = layer_dict.get("id") tree_dict["left"] = left tree_dict["right"] = right tree_dict["layer"] = layer tree_dict["id"] = id # 獲取子節點數據 tree = models.TreeMenu.objects.filter(Q(left__gt=left) & Q(right__lt=right)) ser_tree = SchemaMenuSerializer(instance=tree, many=True).data sub_ser_tree = [dict(i) for i in ser_tree if i.get("layer") == layer + 1] # 遞歸創建子節點 for sub in sub_ser_tree: tree_dict[sub.get("name")] = {} parse_data(sub, tree_dict[sub.get("name")]) def make_node_list(ser_schemamenu): # 構建首頁字典 tree_dict = { ser_schemamenu.get("name"):{}, "id":ser_schemamenu.get("id"), "layer":ser_schemamenu.get("layer"), "left":ser_schemamenu.get("left"), "right":ser_schemamenu.get("right"), } # 交給parse_data,它會遞歸取出第二層,第三層。。。。數據 parse_data(ser_schemamenu,tree_dict[ser_schemamenu.get("name")]) return tree_dict class SchemaMenu(APIView): def get(self,request,*args,**kwargs): # 這里第一層最高節點 也就是首頁 schema_menu_obj = models.TreeMenu.objects.filter(pk=1).first() # 首頁字段序列化 獲取layer層數 ser_schema_menu = SchemaMenuSerializer(instance=schema_menu_obj).data # 創建節點 tree_dict = make_node_list(ser_schema_menu) return Response({"code":200,"msg":"OK","data":tree_dict})
-
效果:
3.最后總結:
-
Schema設計方法優點消除了遞歸操作的恰提實現無限分組,而且查詢條件基於整形數字,效率高。
-
但是缺點就是節點的添加,刪除和修改代價比較大,會設計到表中很多數據改動。
-
當然你還可以添加比如同層節點評議,節點下移,節點上移等操作。不過實現這些算法相對比較繁瑣,會有很多update語句,如果執行順序不對,會對整個樹形結構產生巨大破壞,建議用臨時表,或者將表備份。
-
參考: