• 基于左序遍历的数据存储实践


    参考文章

    超赞 ! 老外的一种避免递归查询所有子部门的树数据表设计与实现!

    但是实践的时候,发现这篇文章有个地方有错误:
    在这里插入图片描述
    它是先更新再删除,发现更新会把要删除节点的节点右值也更新掉。
    所以正确的做法是:
    删除对应的Sql:

    SET @lft := 7;/*要删除的节点左值*/
    SET @rgt := 8;/*要删除的节点右值*/
    begin;
    
    /*先删除节点*/
    DELETE FROM department WHERE lft=@lft AND rgt=@rgt;
    
    /*再更新节点 */
    UPDATE department SET lft=lft-2 WHERE lft > @lft;
    UPDATE department SET rgt=rgt-2 WHERE rgt > @lft;
    
    /*删除影响行数为0时,必须回滚*/
    commit;
    /*rollback*/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    实践

    使用的技术背景

    本文实践采用go语言1.18.1+gormv1.23.6+sqlitev1.3.5
    本次实践是想做一个云配置功能。节点顺序如下:
    客户 -> 项目 -> 站别 -> 电脑
    客户端展示的效果如下:
    在这里插入图片描述

    实现的目的是按照如上的节点去储存每个电脑的软件配置。

    节点模型

    type NodeInfo struct {
    	Id    uint   `gorm:"primary_key;Column:Id"`
    	Name  string `gorm:"column:Name"`
    	Level uint   `gorm:"column:Level"`
    	Left  uint   `gorm:"column:Left"`
    	Right uint   `gorm:"column:Right"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对于Level的定义:

    • 1:客户
    • 2:项目
    • 3:站别
    • 4:电脑

    查询

    客户名称是唯一的,不能重复,其他节点可以重复。

    按照客户名称查询客户节点信息:

    func getCustomer(customer string) (*models.NodeInfo, error) {
    	info := &models.NodeInfo{}
    	if err := databases.DB.Where("Name=? and Level=1", customer).First(info).Error; err != nil {
    		return nil, err
    	} else {
    		return info, nil
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    查询所有的客户节点:只需要查询Level=1即可

    func getCustomers() ([]models.NodeInfo, error) {
    	infos := make([]models.NodeInfo, 0)
    	if err := databases.DB.Where("Level=1").Find(&infos).Error; err != nil {
    		return nil, err
    	} else {
    		return infos, nil
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    查询指定客户名称下的所有项目节点:

    先获取客户节点,拿到客户的左值与右值,然后通过左值与右值获取所有的项目子节点

    func getProjects(customer string) ([]models.NodeInfo, error) {
    	customerModel := &models.NodeInfo{}
    	var err error
    	if customerModel, err = getCustomer(customer); err != nil {
    		return nil, err
    	}
    
    	infos := make([]models.NodeInfo, 0)
    	if err := databases.DB.Where("Level=2 and Left>? and Right, customerModel.Left, customerModel.Right).Find(&infos).Error; err != nil {
    		return nil, err
    	}
    	return infos, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    查询站别电脑与以上类似。

    插入一个节点

    func insertNode(name string, level uint, left uint) (*models.NodeInfo, error) {
    
    	model := &models.NodeInfo{
    		Name:  name,
    		Level: level,
    		Left:  uint(left),
    		Right: left + 1,
    	}
    	if err := databases.DB.Transaction(func(tx *gorm.DB) error {
    		if err := tx.Exec("update NodeInfo set Right=Right+2 where Right>=?", left).Error; err != nil {
    			return err
    		}
    		if err := tx.Exec("update NodeInfo set Left=Left+2 where Left>?", left).Error; err != nil {
    			return err
    		}
    
    		if err := tx.Create(model).Error; err != nil {
    			return err
    		}
    		return nil
    	}); err != nil {
    		return nil, err
    	}
    	return model, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    步骤是:先更新其他节点,再插入节点
    其中:
    name为要插入节点的名称
    level为要插入节点的级别,这决定着是插入平行节点还是子节点,
    left为要插入节点的左值,通常为父节点或者左节点右值+1
    比如一个节点都没有的时候,要插入一个客户名称为AAA的节点:

    insertNode("AAA",1,1)
    
    • 1

    比如在客户名称为 AAA,左值为1,右值为2的节点下增加一个项目名称为PPP的节点:

    //项目节点:level为2,左值为客户节点“AAA”的右值+1
    insertNode("PPP",2,3)
    
    • 1
    • 2

    删除一个节点

    func removeNode(model *models.NodeInfo) error {
    	return databases.DB.Transaction(func(tx *gorm.DB) error {
    		if err := tx.Delete(models.NodeInfo{}, "Left=? and Right=?", model.Left, model.Right).Error; err != nil {
    			return err
    		}
    		if err := tx.Exec("update NodeInfo Set Left=Left-2 where Left>?", model.Left).Error; err != nil {
    			return err
    		}
    		if err := tx.Exec("update NodeInfo set Right=Right-2 where Right>?", model.Right).Error; err != nil {
    			return err
    		}
    		return nil
    	})
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    步骤是:先删除节点,再更新其他节点,与增加节点操作相反,微信文章上的删除节点是错误的!!!

    注意:此函数只能删除没有子节点的单个节点

    批量删除节点

    func removeNodes(ms []models.NodeInfo) error {
    	return databases.DB.Transaction(func(tx *gorm.DB) error {
    		for _, model := range ms {
    			if err := tx.Delete(models.NodeInfo{}, "Left=? and Right=?", model.Left, model.Right).Error; err != nil {
    				return err
    			}
    		}
    		for _, model := range ms {
    			if err := tx.Exec("update NodeInfo Set Left=Left-2 where Left>?", model.Left).Error; err != nil {
    				return err
    			}
    			if err := tx.Exec("update NodeInfo set Right=Right-2 where Right>?", model.Left).Error; err != nil {
    				return err
    			}
    		}
    
    		return nil
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    注意:此函数虽然可以批量删除节点,但是要注意使用场景:当选中某个父节点时,父节点下的所有子节点都会被删除,传入时,需要按照Level字段降序排列传入,否则会出错!
    如下图所示:
    在这里插入图片描述
    当删除客户节点时,会查询客户节点下的所有子节点,然后把子节点按照Level降序排列传入。
    删除客户节点的代码如下:

    func removeCustomer(customer string) error {
    	info := &models.NodeInfo{}
    	var err error
    	if info, err = getCustomer(customer); err != nil {
    		return err
    	}
    
    	//查询customer下的子孙节点,然后全部移除,还要移除文件夹
    	infos := make([]models.NodeInfo, 0)
    	if err = databases.DB.Where("Left>=? and Left<=?", info.Left, info.Right).Find(&infos).Error; err != nil {
    		return err
    	}
    
    	//排序,先移除最下面的节点,再移除上面的节点
    	linq.From(infos).OrderByDescending(func(i interface{}) interface{} { return i.(models.NodeInfo).Level }).ToSlice(&infos)
    
    	if err = removeNodes(infos); err != nil {
    		return err
    	}
    	return nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    步骤是:先获取客户节点信息,查询客户节点下的所有子节点(包含自己),然后降序排列节点,调用removeNodes方法

    总结

    以上实践,在查询方面确实比传统的通过ParentId来关联好的多,避免了递归查询,但是插入和删除会麻烦一些,各有利弊,但是左序遍历是一种创新的做法,多实践才能感觉到它的好处

  • 相关阅读:
    Element Plus el-form表单自定义插槽如何使用
    使用pytorch实现一个线性回归训练函数
    直接在*.vue文件(SFC)中使用JSX/TSX渲染函数,真香!
    Unity --- 网格链接与动态障碍物
    产品思维训练 | 你的项目总是不能按期上线,你会如何解决?
    列表和元组
    无线传感器网络:传输层
    高薪程序员&面试题精讲系列138之如何生成分布式ID?如何生成全局唯一ID?你了解雪花算法吗?
    【SpringCloud】八、SpringCloudAlibaba-流量防卫兵Sentinel
    王道数据结构【链表】部分代码实现(C语言)
  • 原文地址:https://blog.csdn.net/lishuangquan1987/article/details/126051045