完整源码在文末,并附带详细的安装说明,源码均亲测可用
无限级分类是很常见的功能,算法的好坏对于获取分类树的性能起到决定性的作用。尤其当分类数据和层级多时,一个糟糕的算法将使服务器不堪重负
以下用laravel实现无限级分类功能,包括:
-
数据表设计
-
填充模拟数据
-
生成分类树
-
分类树的后台维护
数据表设计
字段名 | 描述 |
---|---|
id | 主键id |
name | 类目名称 |
parent_id | 父类目 ID |
is_directory | 是否拥有子类目 |
level | 当前类目层级 |
path | 该类目所有父类目 id |
为什么要用level与path
无限级分类中,我们经常需要获取一个分类的所有祖先类目或者后代类目,以及判断两个类目是否存在层级关系。倘若都使用递归查询,会产生极多的sql查询。level和path这两个冗余字段便应运而生
以下面的模拟数据为例:
[
[
"id" => 1,
"name" => "手机配件",
"parent_id" => null,
"level" => 0,
"path" => "-"
],
[
"id" => 2,
"name" => "耳机",
"parent_id" => 1,
"level" => 1,
"path" => "-1-"
],
[
"id" => 3,
"name" => "蓝牙耳机",
"parent_id" => 2,
"level" => 2,
"path" => "-1-2-"
],
[
"id" => 4,
"name" => "移动电源",
"parent_id" => 1,
"level" => 1,
"path" => "-1-"
],
];
- 场景1:查询
蓝牙耳机
的所有祖先类目
根据path
字段的值获取其祖先id为[1, 2]
,用 Category::whereIn('id', [1, 2])->orderBy('level')->get()
即可获取结果
- 场景2:查询
手机配件
的所有后代类目
将id
字段追加到path
字段,得到-1-
, 用Category::where('path', 'like', '-1-%')->get()
即可获取结果
- 场景3:判断
移动电源
与蓝牙耳机
是否有层级关系
$highLevelPath = '-1-2'; // 蓝牙耳机的path
$lowLevelPath = '-1-' . '4-'; // 移动电源的path值拼接id值
if (strpos($highLevelPath, $lowLevelPath) === 0) { // 判断蓝牙耳机的path值是否以移动电源的path值为开头
echo '存在层级关系';
} else {
echo '并无层级关系';
}
创建数据表
$ php artisan make:model Models/Category -m
编写迁移文件:database/migrations/2019_04_21_140243_create_categories_table.php
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->comment('分类名称');
$table->unsignedInteger('parent_id')->nullable()->comment('父类id');
$table->foreign('parent_id')->references('id')->on('categories')->onDelete('cascade');
$table->boolean('is_directory')->comment('是否有子类目');
$table->unsignedInteger('level')->comment('当前类目层级');
$table->string('path')->comment('该分类的所有父类id, 用 - 连接');
$table->timestamps();
});
}
执行迁移创建数据表
$ php artisan migrate
填充模拟数据
调整模型类app/Models/Category.php
代码:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = ['name', 'is_directory', 'level', 'path'];
protected $casts = [
'is_directory' => 'boolean',
];
protected static function boot()
{
parent::boot();
// 当创建Category时,自动初始化 path 和 level
static::creating(function (Category $category) {
if (is_null($category->parent_id)) { // 创建的是根目录
$category->level = 0; // 将层级设为0
$category->path = '-'; // 将 path 设为 -
} else { // 创建的并非根目录
$category->level = $category->parent->level + 1; // 将层级设为父类层级+1
$category->path = $category->parent->path . $category->parent_id . '-'; // 将path值设为父类path+父类id
}
});
}
public function parent()
{
return $this->belongsTo(Category::class);
}
public function children()
{
return $this->hasMany(Category::class, 'parent_id');
}
/**
* 获取所有祖先分类id
* @date 2019-04-21
*/
public function getPathIdsAttribute()
{
$path = trim($this->path, '-'); // 过滤两端的 -
$path = explode('-', $path); // 以 - 为分隔符切割为数组
$path = array_filter($path); // 过滤空值元素
return $path;
}
/**
* 获取所有祖先分类且按层级正序排列
* @date 2019-04-21
*/
public function getAncestorsAttribute()
{
return Category::query()
->whereIn('id', $this->path_ids) // 调用 getPathIdsAttribute 获取祖先类目id
->orderBy('level') // 按层级排列
->get();
}
/**
* 获取所有祖先类目名称以及当前类目的名称
* @date 2019-04-21
*/
public function getFullNameAttribute()
{
return $this->ancestors // 调用 getAncestorsAttribute 获取祖先类目
->pluck('name') // 将所有祖先类目的 name 字段作为一个数组
->push($this->name) // 追加当前类目的name字段到数组末尾
->implode(' - '); // 用 - 符号将数组的值组装成一个字符串
}
}
创建填充文件:
$ php artisan make:seeder CategoriesSeeder
调整database/seeds/CategoriesSeeder.php
代码:
<?php
use Illuminate\Database\Seeder;
use App\Models\Category;
class CategoriesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$categories = [
[
'name' => '手机配件',
'children' => [
['name' => '手机壳'],
['name' => '贴膜'],
['name' => '存储卡'],
['name' => '数据线'],
['name' => '充电器'],
[
'name' => '耳机',
'children' => [
['name' => '有线耳机'],
['name' => '蓝牙耳机'],
],
],
],
],
[
'name' => '电脑配件',
'children' => [
['name' => '显示器'],
['name' => '显卡'],
['name' => '内存'],
['name' => 'CPU'],
['name' => '主板'],
['name' => '硬盘'],
],
],
[
'name' => '电脑整机',
'children' => [
['name' => '笔记本'],
['name' => '台式机'],
['name' => '平板电脑'],
['name' => '一体机'],
['name' => '服务器'],
['name' => '工作站'],
],
],
[
'name' => '手机通讯',
'children' => [
['name' => '智能机'],
['name' => '老人机'],
['name' => '对讲机'],
],
],
];
foreach ($categories as $data) {
$this->createCategory($data);
}
}
protected function createCategory($data, $parent = null)
{
$category = new Category(['name' => $data['name']]);
$category->is_directory = isset($data['children']);
if (!is_null($parent)) { // 如果存在parent,代表有父类目
$category->parent()->associate($parent);
}
$category->save(); // 数据入库
if (isset($data['children']) && is_array($data['children'])) {
foreach ($data['children'] as $child) {
// 递归调用 createCategory 方法
$this->createCategory($child, $category); // $category 为刚创建的类目,作为子类目的父级类目参数
}
}
}
}
执行数据填充:
$ php artisan db:seed --class=CategoriesSeeder
查看结果:
网友评论