高性能无限级分类实现思路

表结构

高性能无限级分类实现思路

废话不多说,直接上结果

  • example:查询北京下所有子节点(包含北京)
    select * from cy_address where tree like '0-1-2-%' or id = 2 order by id asc

高性能无限级分类实现思路

model代码

<?php

namespace App\Models;

use Illuminate\Support\Facades\DB;


class Address extends BaseModel
{
    protected $table = 'address';
    protected $guarded = [];
    protected $primaryKey = 'id';
    public $timestamps = false;

    public static function boot()
    {
        parent::boot();

        # 更新节点信息
        static::created(function ($model) {
            if ($model->parentid) {
                $parent = static::find($model->parentid);
                static::find($model->id)->update([
                    'keyword' => implode(',', parse_pinyin($model->name)),
                    'tree'    => $parent->tree . '-' . $model->id
                ]);
                $parent->update(['child' => 1]);
            } else {
                static::find($model->id)->update([
                    'keyword' => implode(',', parse_pinyin($model->name)),
                    'tree'    => '0-' . $model->id,
                ]);
            }
        });

        static::updating(function ($model) {
            $dirty = $model->getDirty();

            if (isset($dirty['name'])) {
                $model->keyword = implode(',', parse_pinyin($model->name));
            }

            # 移动节点,更新自身tree
            if (isset($dirty['parentid'])) {
                $parent      = static::find($model->parentid);
                $model->tree = $parent->tree . '-' . $model->id;
                $parent->update(['child' => 1]);
            }
        });

        static::updated(function ($model) {
            $dirty    = $model->getDirty();
            $original = $model->getRawOriginal();

            # 移动节点
            if (isset($dirty['parentid'])) {
                DB::select(DB::raw("update " . DB::getTablePrefix() . "address set tree = CONCAT('{$model->tree}-',id) where tree like '{$original['tree']}-%'"));
                $child = static::where('parentid', $original['parentid'])->first();
                if (!$child) {
                    static::find($original['parentid'])->update(['child' => 0]);
                }
            }
        });

        static::deleting(function ($model) {
            $tree = $model->tree;
            static::where('tree', 'like', "{$tree}-%")->delete();
        });

        // 更新child状态
        static::deleted(function ($model) {
            $parent = static::find($model->parentid);
            if ($parent->getAllChild()->count() == 1) {
                $parent->update(['child' => 0]);
            }
        });
    }


    # 获取所有子节点
    public function getAllChild()
    {
        return static::where('tree', 'like', "{$this->tree}-%")->orWhere('id', $this->id)->orderBy('id', 'ASC')->get();
    }
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 11

不懂就问,直接查parentid,比查tree这个字段低效吗?

2周前 评论
91hero (楼主) 2周前

全路径的设计没有问题,是为了提高查询性能,你这个SQL存在问题 like '%0-1-2-%',需要改为’0-1-2-%'才能走索引

2周前 评论
91hero (楼主) 2周前

查询一时爽,维护火葬场,,,

不过这个省市三级,不需要变更层级关系,用这个确实可以,,,

2周前 评论
91hero (楼主) 2周前

mysql 8 支持通用表达式,可以实现递归查询。

2周前 评论
bing8u 1周前

移动节点和修改层级后的全路径如何维护呢,能否指点一下

2周前 评论
91hero (楼主) 2周前

一次查出来 php 处理也行啊

function arr2tree($list, $id = 'id', $pid = 'pid', $son = 'sub')
 { 
     list($tree, $map) = [[], []]; 
     foreach ($list as $item) { 
         $map[$item[$id]] = $item; 
     } 
     foreach ($list as $item) { 
         if (isset($item[$pid]) && isset($map[$item[$pid]])) { 
             $map[$item[$pid]][$son][] = &$map[$item[$id]]; 
         } else {
          $tree[] = &$map[$item[$id]]; 
         } 
     } 
     unset($map); 
     return $tree; 
 }
2周前 评论
pi_phq 1周前
DogLoML 4天前
ware (作者) 3天前
ware (作者) 3天前

我目前的做法是用parentid . 一次性读取全部目录出来然后再内存递归组装。这种方式没有任何性能问题。只是增加了代码复杂度,要各种拼接。所以我也蛮希望有一种更好的解决方案。不过目前来看,好像你这种方式并不算更优的解决方案,还不如内存组装。

1周前 评论
91hero (楼主) 1周前

可以考虑使用预排序遍历树实现,感兴趣可以查看: github.com/lazychaser/laravel-nest...

查找所有后代或所有祖先:

$result = Category::whereDescendantOf($id)->get();
$result = Category::whereAncestorOf($id)->get();

组装树形结构:

$tree = Category::get()->toTree();

使用模型的新增更新和删除会自动维护字段,代码无感知,但内部可能会执行多条sql,数据量大时效率低

1周前 评论
91hero (楼主) 1周前

@91hero 就是将数据一次性全部取出,然后放在内存中进行遍历,只不过这个方法用的是引用传值方式,会相应的减少一些内存(相比较递归来说),而且效率高一些。代码不难,你可以试着理解一下

$temp = [
        ['id' => 1, 'pid' => 0, 'name' => '商品管理'],
        ['id' => 2, 'pid' => 1, 'name' => '平台自营'],
        ['id' => 3, 'pid' => 2, 'name' => '图书品类'],
        ['id' => 4, 'pid' => 2, 'name' => '3C品类'],
        ['id' => 5, 'pid' => 0, 'name' => '第三方'],
        ['id' => 6, 'pid' => 5, 'name' => '家私用品'],
        ['id' => 7, 'pid' => 5, 'name' => '书法品赏'],
        ['id' => 8, 'pid' => 7, 'name' => '行书'],
        ['id' => 9, 'pid' => 8, 'name' => '行楷'],
        ['id' => 10, 'pid' => 9, 'name' => '张山行楷字帖'],
        ['id' => 11, 'pid' => 22, 'name' => '李四行楷字帖'],
    ];


    /**
     * 一维数据数组生成数据树
     * @param array $list 数据列表
     * @param string $id 父ID Key
     * @param string $pid ID Key
     * @param string $son 定义子数据Key
     * @return array
     */
    function arr2tree($list, $id = 'id', $pid = 'pid', $son = 'sub')
    {
        list($tree, $map) = [[], []];
        foreach ($list as $item) {
            $map[$item[$id]] = $item;
        }

        foreach ($list as $item) {
            if (isset($item[$pid]) && isset($map[$item[$pid]])) {
                $map[$item[$pid]][$son][] = &$map[$item[$id]];
            } else {
                $tree[] = &$map[$item[$id]];
            }
        }
        unset($map);
        return $tree;
    }


echo '<pre>';
$t1 = microtime(true);
print_r(arr2tree($temp));
$t2 = microtime(true);
echo '耗时:' . round($t2-$t1,3) . '秒<br>';
echo '内存:' . memory_get_usage() / (1024*1024) . 'mb<br/>';
echo '内存:' . memory_get_usage() / 1024 . 'kb<br/>';
echo '--------------------------------<br>';
// die;
1周前 评论
VictorWang 3天前
haoge 1周前
ware (作者) 3天前
ware (作者) 3天前

@91hero tree 字段如果加了索引,SQL其实不需要 or id = 2

select * from cy_address where tree like '0-1-2%' order by id asc
1周前 评论
xiaohuasheng 3天前
ware (作者) 3天前

其实这种就是路径查询 tree建议写成/id/,就是写入的时候加入路径,查询的时候比较方便

3天前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!