樹形結構數據庫表設計與django結合


樹形結構數據庫表設計

  • 樹形結構我們經常會用它表征某些數據關聯,比如商品分類,企業管理系統菜單或上下級關系等,但在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語句,如果執行順序不對,會對整個樹形結構產生巨大破壞,建議用臨時表,或者將表備份。

  • 參考:

    參考鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM