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

Posted lishuangquan1987

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于左序遍历的数据存储实践相关的知识,希望对你有一定的参考价值。

参考文章

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

但是实践的时候,发现这篇文章有个地方有错误:

它是先更新再删除,发现更新会把要删除节点的节点右值也更新掉。
所以正确的做法是:
删除对应的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*/

实践

使用的技术背景

本文实践采用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"`

对于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
	

查询所有的客户节点:只需要查询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
	

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

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

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

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

插入一个节点

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

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

insertNode("AAA",1,1)

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

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

删除一个节点

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
	)


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

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

批量删除节点

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
	)

注意:此函数虽然可以批量删除节点,但是要注意使用场景:当选中某个父节点时,父节点下的所有子节点都会被删除,传入时,需要按照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

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

总结

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

以上是关于基于左序遍历的数据存储实践的主要内容,如果未能解决你的问题,请参考以下文章

算法第4章实践报告

hive order by是升序还是降序

如何按“值”的降序遍历 berkeley-db 数据库?

SRE 工程实践——基于时间序列存储数据的报警

庞晓曦:基于Hadoop和HBase的大数据存储实践

基于Ceph对象存储构建实践