无限级分类之Laravel-nestedset扩展包的使用

Laravel-nestedset是Laravel框架中的一个无限级分类的扩展包,它的实现有别于传统的邻接表模型,采用的是一种新的分层数据模型叫嵌套集合模型,这种模型能够实现快速查询,运用在少修改、频查询的业务场景中能够提升较大的查询效率。下面我就带着大家一步一步来熟悉这个扩展。

u=3352133920,2187839175&fm=26&gp=0.jpg

一、首先安装扩展包

1.进入到Laravel项目的根目录中,用composer安装kalnoy/nestedset。

composer require kalnoy/nestedset 

二、创建模型以及对应的分类表

1.创建好数据迁移文件。

php artisan make:migration create_category_table 

2.进入database/migrations目录下,打开生成的迁移文件,并添加需要的字段。

<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; //引入NestedSet类 use Kalnoy\Nestedset\NestedSet; class CreateCategoryTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('category', function (Blueprint $table) { //主键ID $table->id(); //新增分类的名称字段 $table->string('name')->default(''); //在此添加此方法,nestedset将会自动生成:_lft、_rgt、parent_id三个字段 NestedSet::columns($table); //根据实际场景需要决定需不需要软删除 $table->softDeletes(); //添加创建时间、更新时间字段 $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('category'); } } 

3.迁移文件调整好之后,我们就可以执行迁移操作,创建出分类表,执行之后如下所示。

php artisan migrate 

image.png

image.png

4.创建模型文件

php artisan make:model CategoryModel 

5.打开该模型文件,引入NodeTrait。

<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; //引入NodeTrait use Kalnoy\Nestedset\NodeTrait; class CategoryModel extends Model { //使用NodeTrait use HasFactory,NodeTrait; protected $table = 'category'; protected $fillable = ['name']; } 

6.若想要替换字段_lft、_rgt包括parent_id的名称可以做如下操作

image.png

image.png

三、创建用于测试的路由以及控制器

1.新增路由

//显示节点 Route::get('node/show',[CategoryController::class,'showNodes']); //创建节点 Route::get('node/create',[CategoryController::class,'createNode']); //移动节点 Route::get('node/move',[CategoryController::class,'moveNode']); //删除节点 Route::get('node/delete',[CategoryController::class,'deleteNode']); 

2.新增控制器

php artisan make:controller CategoryController 
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\CategoryModel; class CategoryController extends Controller { //显示节点 public function showNodes() { } //创建节点 public function createNode() { } //移动节点 public function moveNode() { } //删除节点 public function deleteNode() { } } 

三、创建或插入分类节点

(1)创建节点并添加到树节点的末端

1.通过模型的构造方法传参的形式创建单节点

$model = new CategoryModel(['name' => '节点A']); $model->save(); 

2.直接针对模型的属性赋值来创建单节点

$model = new CategoryModel(); $model->name = '节点B'; $model->save(); 

3.调用模型的create方法创建单节点或者多节点(可包含子节点)

//创建一个单节点,默认创建的节点在根节点 CategoryModel::create(['name' => '节点C']); //一次性创建多个节点 CategoryModel::create([ 'name' => '节点D', //使用children来定义子节点 'children' => [ [ 'name' => '节点E', 'children' => [ ['name' => '节点F'] ] ], [ 'name' => '节点G' ] ] ]); 

4.创建之后数据库数据以及树节点显示如下

image.png

[ { "id": 1, "name": "节点A", "_lft": 1, "_rgt": 2, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:12:43.000000Z", "updated_at": "2020-12-18T03:12:43.000000Z", "children": [] }, { "id": 2, "name": "节点B", "_lft": 3, "_rgt": 4, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:13:11.000000Z", "updated_at": "2020-12-18T03:13:11.000000Z", "children": [] }, { "id": 3, "name": "节点C", "_lft": 5, "_rgt": 6, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:14:22.000000Z", "updated_at": "2020-12-18T03:14:22.000000Z", "children": [] }, { "id": 4, "name": "节点D", "_lft": 7, "_rgt": 14, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [ { "id": 5, "name": "节点E", "_lft": 8, "_rgt": 11, "parent_id": 4, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [ { "id": 6, "name": "节点F", "_lft": 9, "_rgt": 10, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [] } ] }, { "id": 7, "name": "节点G", "_lft": 12, "_rgt": 13, "parent_id": 4, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [] } ] } ] 

(2)为指定的父节点添加子节点,并且添加在子节点列表的尾部(若子节点已经存在,则会将该子节点移动到父节点的子节点列表中)

1.使用子节点的appendToNode方法

//首先创建一个子节点 $node = new CategoryModel(['name' => '节点H']); //然后找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //将子节点添加到父节点中 $node->appendToNode($parentNode)->save(); 

2.使用父节点的appendNode方法

//首先创建一个子节点 $node = new CategoryModel(['name' => '节点I']); //然后找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //使用父节点的appendNode方法添加子节点 $parentNode->appendNode($node); 

3.使用父节点的children()关系

//首先找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //使用父节点的children()方法添加子节点 $parentNode->children()->create(['name' => '节点J']); 

4.使用子节点的parent()关系的

//首先创建一个子节点 $node = new CategoryModel(['name' => '节点K']); //然后找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //使用子节点的parent()关系 $node->parent()->associate($parentNode)->save(); 

5.使用子节点的parent_id属性设置将子节点添加到指定的父节点

//首先创建一个子节点 $node = new CategoryModel(['name' => '节点L']); //然后找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //设置子节点的parent_id为父节点 $node->parent_id = $parentNode->id; //设置完成之后保存 $node->save(); 

6.使用模型的静态方法将子节点添加到父节点中

//首先找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //调用模型的create静态方法 CategoryModel::create(['name' => '节点M'],$parentNode); 

7.从1~6方法执行之后,最终的树结构图如下所示

//执行上面的代码之后,我们来查看一下树节点 //新增加的节点都依次被添加到了节点E的children列表的尾部 { "id": 4, "name": "节点D", "_lft": 7, "_rgt": 28, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [ { "id": 5, "name": "节点E", "_lft": 8, "_rgt": 25, "parent_id": 4, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [ { "id": 6, "name": "节点F", "_lft": 9, "_rgt": 10, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [] }, { "id": 9, "name": "节点H", "_lft": 11, "_rgt": 12, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:27:21.000000Z", "updated_at": "2020-12-18T05:27:21.000000Z", "children": [] }, { "id": 11, "name": "节点I", "_lft": 15, "_rgt": 16, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:40:55.000000Z", "updated_at": "2020-12-18T05:40:55.000000Z", "children": [] }, { "id": 12, "name": "节点J", "_lft": 17, "_rgt": 18, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:45:48.000000Z", "updated_at": "2020-12-18T05:45:48.000000Z", "children": [] }, { "id": 13, "name": "节点K", "_lft": 19, "_rgt": 20, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:53:22.000000Z", "updated_at": "2020-12-18T05:53:22.000000Z", "children": [] }, { "id": 14, "name": "节点L", "_lft": 21, "_rgt": 22, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:57:52.000000Z", "updated_at": "2020-12-18T05:57:52.000000Z", "children": [] }, { "id": 15, "name": "节点M", "_lft": 23, "_rgt": 24, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T06:02:43.000000Z", "updated_at": "2020-12-18T06:02:43.000000Z", "children": [] } ] }, { "id": 7, "name": "节点G", "_lft": 26, "_rgt": 27, "parent_id": 4, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [] } ] } 

(3)为指定的父节点添加子节点,并且添加在子节点列表的头部(若子节点已经存在,则会将该子节点移动到父节点的子节点列表中)

1.使用子节点的prependToNode方法

//首先创建一个子节点 $node = new CategoryModel(['name' => '节点N']); //然后找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //通过子节点的prependToNode方法 $node->prependToNode($parentNode)->save(); 

2.使用父节点的prependNode方法

//首先创建一个子节点 $node = new CategoryModel(['name' => '节点O']); //然后找一个父节点(假如以节点E作为父节点,id是5) $parentNode = CategoryModel::find(5); //通过父节点的prependNode方法 $parentNode->prependNode($node); 

3.执行以上两个操作之后的数据结构如下

{ "id": 4, "name": "节点D", "_lft": 7, "_rgt": 10, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [ { "id": 7, "name": "节点G", "_lft": 8, "_rgt": 9, "parent_id": 4, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [] }, { "id": 5, "name": "节点E", "_lft": 12, "_rgt": 33, "parent_id": 4, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T06:51:59.000000Z", "children": [ { "id": 18, "name": "节点O", "_lft": 13, "_rgt": 14, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T07:01:04.000000Z", "updated_at": "2020-12-18T07:01:04.000000Z", "children": [] }, { "id": 17, "name": "节点N", "_lft": 15, "_rgt": 16, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T06:53:11.000000Z", "updated_at": "2020-12-18T06:53:11.000000Z", "children": [] }, { "id": 6, "name": "节点F", "_lft": 17, "_rgt": 18, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T03:14:46.000000Z", "updated_at": "2020-12-18T03:14:46.000000Z", "children": [] }, { "id": 9, "name": "节点H", "_lft": 19, "_rgt": 20, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:27:21.000000Z", "updated_at": "2020-12-18T05:27:21.000000Z", "children": [] }, { "id": 11, "name": "节点I", "_lft": 23, "_rgt": 24, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:40:55.000000Z", "updated_at": "2020-12-18T05:40:55.000000Z", "children": [] }, { "id": 12, "name": "节点J", "_lft": 25, "_rgt": 26, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:45:48.000000Z", "updated_at": "2020-12-18T05:45:48.000000Z", "children": [] }, { "id": 13, "name": "节点K", "_lft": 27, "_rgt": 28, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:53:22.000000Z", "updated_at": "2020-12-18T05:53:22.000000Z", "children": [] }, { "id": 14, "name": "节点L", "_lft": 29, "_rgt": 30, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T05:57:52.000000Z", "updated_at": "2020-12-18T05:57:52.000000Z", "children": [] }, { "id": 15, "name": "节点M", "_lft": 31, "_rgt": 32, "parent_id": 5, "deleted_at": null, "created_at": "2020-12-18T06:02:43.000000Z", "updated_at": "2020-12-18T06:02:43.000000Z", "children": [] } ] } ] } 

(4)新增节点并插入到指定节点的前面或后面

1.新增节点插入到指定节点的前面

/*************************显性 save****************************/ # 首先创建一个子节点 $node = new CategoryModel(['name' => '节点Q']); # 然后找一个指定的节点(假如指定节点B,id是2) $neighbor = CategoryModel::find(2); # 将该节点插入到节点B的前面 $node->beforeNode($neighbor)->save(); /*************************显性 save****************************/ //或者 /*************************隐性 save****************************/ # 首先创建一个子节点 $node = new CategoryModel(['name' => '节点Q']); # 然后找一个指定的节点(假如指定节点B,id是2) $neighbor = CategoryModel::find(2); # 将该节点插入到节点B的前面 $node->insertBeforeNode($neighbor); /*************************隐性 save****************************/ 
// 执行之后树结构如下 [ { "id": 1, "name": "节点A", "_lft": 1, "_rgt": 2, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:12:43.000000Z", "updated_at": "2020-12-18T03:12:43.000000Z", "children": [] }, { "id": 20, "name": "节点Q", "_lft": 3, "_rgt": 4, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T09:41:48.000000Z", "updated_at": "2020-12-18T09:41:48.000000Z", "children": [] }, { "id": 2, "name": "节点B", "_lft": 5, "_rgt": 6, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:13:11.000000Z", "updated_at": "2020-12-18T03:13:11.000000Z", "children": [] }, ... ] 

2.新增节点插入到指定节点的后面

/*************************显性 save****************************/ # 首先创建一个子节点 $node = new CategoryModel(['name' => '节点S']); # 然后找一个指定的节点(假如指定节点B,id是2) $neighbor = CategoryModel::find(2); # 将该节点插入到节点B的后面 $node->afterNode($neighbor)->save(); /*************************显性 save****************************/ //或者 /*************************隐性 save****************************/ # 首先创建一个子节点 $node = new CategoryModel(['name' => '节点S']); # 然后找一个指定的节点(假如指定节点B,id是2) $neighbor = CategoryModel::find(2); # 将该节点插入到节点B的后面 $node->insertAfterNode($neighbor); /*************************隐性 save****************************/ 
//执行之后树结构如下 [ { "id": 1, "name": "节点A", "_lft": 1, "_rgt": 2, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:12:43.000000Z", "updated_at": "2020-12-18T03:12:43.000000Z", "children": [] }, { "id": 20, "name": "节点Q", "_lft": 3, "_rgt": 4, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T09:41:48.000000Z", "updated_at": "2020-12-18T09:41:48.000000Z", "children": [] }, { "id": 2, "name": "节点B", "_lft": 7, "_rgt": 8, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:13:11.000000Z", "updated_at": "2020-12-18T03:13:11.000000Z", "children": [] }, { "id": 22, "name": "节点S", "_lft": 9, "_rgt": 10, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T09:57:56.000000Z", "updated_at": "2020-12-18T09:57:56.000000Z", "children": [] }, { "id": 3, "name": "节点C", "_lft": 11, "_rgt": 12, "parent_id": null, "deleted_at": null, "created_at": "2020-12-18T03:14:22.000000Z", "updated_at": "2020-12-18T07:29:22.000000Z", "children": [] }, ... ] 

四、获取分类节点

1.将集合数据树形展现

//树形结构展现(默认情况下,节点未进行排序展示,也就是说未根据depth字段进行排序) $tree = CategoryModel::get()->toTree(); //采用默认排序再转换成树形(默认排序是根据depth字段从小到大的顺序) $tree = CategoryModel::defaultOrder()->get()->toTree(); //采用倒序排序再转换成树形(倒序排序是根据depth字段从大到小的顺序) $tree = CategoryModel::reversed()->get()->toTree(); 

2.获取节点数据的同时附带每个节点的深度

//展现节点的时候,使用withDepth()方法,输出的数据会带一个深度字段depth $result = CategoryModel::withDepth()->get(); //按照深度值来筛选出节点 $result = CategoryModel::withDepth()->having('depth', '=', 1)->get(); 

image.png

3.获取兄弟节点

//获取某个节点的兄弟节点 $result = $node->getSiblings(); $result = $node->siblings()->get(); 
// 获取相邻的下一个兄弟节点 $result = $node->getNextSibling(); // 获取后面的所有兄弟节点 $result = $node->getNextSiblings(); // 使用查询获得所有兄弟节点 $result = $node->nextSiblings()->get(); 
// 获取相邻的前一个兄弟节点 $result = $node->getPrevSibling(); // 获取前面的所有兄弟节点 $result = $node->getPrevSiblings(); // 使用查询获得所有兄弟节点 $result = $node->prevSiblings()->get(); 

4.获取祖先和后代节点

// 获取该节点的所有祖先节点 $node->ancestors; // 获取该节点的所有后代节点 $node->descendants; 
//获取$id这个节点的所有祖先节点 $result = CategoryModel::ancestorsOf($id); //获取$id这个节点的所有祖先节点包括本节点 $result = CategoryModel::ancestorsAndSelf($id); //获取$id这个节点的所有子节点 $result = CategoryModel::descendantsOf($id); //获取$id这个节点的所有子节点包括本节点 $result = CategoryModel::descendantsAndSelf($id); 

5.查询数据的条件约束

//仅获取根节点 $result = CategoryModel::whereIsRoot(); //获取特定$id的节点后面的所有节点(不仅是兄弟节点)。 $result = CategoryModel::whereIsAfter($id); //获取特定$id的节点前面的所有节点(不仅是兄弟节点)。 $result = CategoryModel::whereIsBefore($id); 
//查询祖先的条件约束 $result = CategoryModel::whereAncestorOf($node)->get(); $result = CategoryModel::whereAncestorOrSelf($id)->get(); 
//查询后代的条件约束 $result = CategoryModel::whereDescendantOf($node)->get(); $result = CategoryModel::whereNotDescendantOf($node)->get(); $result = CategoryModel::orWhereDescendantOf($node)->get(); $result = CategoryModel::orWhereNotDescendantOf($node)->get(); $result = CategoryModel::whereDescendantAndSelf($id)->get(); //结果集合中包含目标node自身 $result = Category::whereDescendantOrSelf($node)->get(); 

五、移动分类节点

1.向上移动节点

//获取需要移动的节点 $node = CategoryModel::find(1); //将该节点向上移动1个位置 $node->up(); //将该节点向上移动3个位置(如果节点向上移动的位置超过了范围,则移动无效) $node->up(3); 

2.向下移动节点

//获取需要移动的节点 $node = CategoryModel::find(1); //将该节点向下移动1个位置 $node->down(); //将该节点向下移动3个位置(如果节点向下移动的位置超过了范围,则移动无效) $node->down(3); 

3.将一个已存在的节点设置为根节点

//将节点E设置为根节点 $node = CategoryModel::find(5); // 隐性 save $node->saveAsRoot(); // 或者 // 显性 save $node->makeRoot()->save(); 

4.将一个已经存在的节点移动到指定节点的前面或后面(该方法与前述插入的方法一致,区别在于如果节点不存在则会新创建节点再移动,而如果节点已经存在则直接移动)

$node = CategoryModel::find(5); $neighbor = CategoryModel::find(3); //显性save //将ID是5的节点移动到ID是3的节点的后面 $node->afterNode($neighbor)->save(); //将ID是5的节点移动到ID是3的节点的前面 $node->beforeNode($neighbor)->save(); //或者 // 隐性 save //将ID是5的节点移动到ID是3的节点的后面 $node->insertAfterNode($neighbor); //将ID是5的节点移动到ID是3的节点的前面 $node->insertBeforeNode($neighbor); 

5.将一个节点移动到某个节点的子节点列表中(可以是列表的头部,也可以是列表的尾部),该部分参考「三」-(2)以及「三」-(3)。

六、删除分类节点

1.使用模型的delete方法删除,节点的所有后代元素将一并删除

//删除 $node = CategoryModel::find(19); $node->delete(); 

2.不可以使用以下的语句删除,否则会破会树结构

//不可以这样删除,请谨慎操作,否则破坏树结构 🚫 CategoryModel::where('id', '=', $id)->delete(); 

3.模型也支持软删除,在模型里面添加SoftDeletes trait

<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Kalnoy\Nestedset\NodeTrait; class CategoryModel extends Model { use HasFactory,NodeTrait,SoftDeletes; protected $table = 'category'; protected $fillable = ['name']; } 

4.软删除之后我们可以查看表数据

image.png

七、帮助方法

1.检查节点是否为其他节点的子节点

$bool = $node->isDescendantOf($parent); 

2.检查是否为根节点

$bool = $node->isRoot(); 

3.检查树节点是否被破环

$bool = CategoryModel::isBroken(); 

4.检查当前节点是否是另外一个节点的子节点

$bool = $node−>isChildOf($otherNode); 

5.检查当前节点是否是另外一个节点的

$bool = $node−>isAncestorOf($otherNode); 

6.检查当前节点是否是另外一个节点的兄弟节点

$bool = $node−>isSiblingOf($otherNode); 

7.检查当前节点是否是叶子节点

$bool = $node->isLeaf();
本作品采用《CC 协议》,转载必须注明作者和本文链接
最美的不是下雨天,而是和你一起躲过的屋檐!
本帖由系统于 1年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3
playmaker

分销出身吧?

4年前 评论
晏南风 3年前

写的超级详细,感谢

3年前 评论