laravel-nestedset:多级无限分类正确姿势

 

laravel-nestedset是一个关系型数据库遍历树的larvel4-5的插件包

目录:

  • Nested Sets Model简介
  • 安装要求
  • 安装
  • 开始使用
    • 迁移文件
    • 插入节点
    • 获取节点
    • 删除节点
    • 一致性检查和修复
    • 作用域

Nested Sets Model简介

Nested Set Model 是一种实现有序树的高明的方法,它快速且不需要递归查询,例如不管树有多少层,你可以仅使用一条查询来获取某个节点下的所有的后代,缺点是它的插入、移动、删除需要执行复杂的sql语句,但是这些都在这个插件内处理了! 更多关于详见维基百科!Nested set model 及它中文翻译!嵌套集合模型

安装要求

  • PHP>=5.4
  • laravel>=4.1
  • v4.3版本以后支持Laravel-5.5
  • v4版本支持Laravel-5.2、5.3、5.4
  • v3版本支持Laravel-5.1
  • v2版本支持Laravel-4 强烈建议使用支持事物功能的数据引擎(像MySql的innoDb)来防止可能的数据损坏。

安装

composer.json文件中加入下面代码:

"kalnoy/nestedset": "^4.3",

运行composer install 来安装它。

或者直接在命令行输入

composer require kalnoy/nestedset

如需安装历史版本请点击更多版本

开始使用

迁移文件

你可以使用NestedSet类的columns方法来添加有默认名字的字段:

...
use Kalnoy\Nestedset\NestedSet; Schema::create('table', function (Blueprint $table) {
...
NestedSet::columns($table);
});

删除字段:

...
use Kalnoy\Nestedset\NestedSet; Schema::table('table', function (Blueprint $table) {
NestedSet::dropColumns($table);
});

默认的字段名为:_lft_rgtparent_id,源码如下:

public static function columns(Blueprint $table)
{
$table->unsignedInteger(self::LFT)->default(0);
$table->unsignedInteger(self::RGT)->default(0);
$table->unsignedInteger(self::PARENT_ID)->nullable(); $table->index(static::getDefaultColumns());
}

模型

你的模型需要使用Kalnoy\Nestedset\NodeTraittrait 来实现nested sets

use Kalnoy\Nestedset\NodeTrait;

class Foo extends Model {
use NodeTrait;
}

迁移其他地方已有的数据

从其他的nested set 模型库迁移

public function getLftName()
{
return 'left';
} public function getRgtName()
{
return 'right';
} public function getParentIdName()
{
return 'parent';
} // Specify parent id attribute mutator
public function setParentAttribute($value)
{
$this->setParentIdAttribute($value);
}

从其他的具有父子关系的模型库迁移

如果你的数据库结构树包含 parent_id 字段信息,你需要添加下面两栏字段到你的蓝图文件:

$table->unsignedInteger('_lft');
$table->unsignedInteger('_rgt');

设置好你的模型后你只需要修复你的结构树来填充_lft_rgt字段:

MyModel::fixTree();

关系

Node具有以下功能,他们功能完全且被预加载:

  • Node belongs to parent
  • Node has many children
  • Node has many ancestors
  • Node has many descendants

假设我们有一个Category模型;变量$node是该模型的一个实例是我们操作的node(节点)。它可以为一个新创建的node或者是从数据库中取出的node

插入节点(node)

每次插入或者移动一个节点都要执行好几条数据库操作,所有强烈推荐使用transaction.

注意! 对于v4.2.0版本不是自动开启transaction的,另外node的结构化操作需要在模型上手动执行save,但是有些方法会隐性执行save并返回操作后的布尔类型的结果。

创建节点(node)

当你简单的创建一个node,它会被添加到树的末端。

Category::create($attributes); // 自动save为一个根节点(root)

或者

$node = new Category($attributes);
$node->save(); // save为一个根节点(root)

在这里node被设置为root,意味着它没有父节点

将一个已存在的node设置为root

// #1 隐性 save
$node->saveAsRoot(); // #2 显性 save
$node->makeRoot()->save();

添加子节点到指定的父节点末端或前端

如果你想添加子节点,你可以添加为父节点的第一个子节点或者最后一个子节点。 *在下面的例子中, $parent 为已存在的节点

添加到父节点的末端的方法包括:

// #1 使用延迟插入
$node->appendToNode($parent)->save(); // #2 使用父节点
$parent->appendNode($node); // #3 借助父节点的children关系
$parent->children()->create($attributes); // #5 借助子节点的parent关系
$node->parent()->associate($parent)->save(); // #6 借助父节点属性
$node->parent_id = $parent->id;
$node->save(); // #7 使用静态方法
Category::create($attributes, $parent);

添加到父节点的前端的方法

// #1
$node->prependToNode($parent)->save(); // #2
$parent->prependNode($node);

插入节点到指定节点的前面或后面

你可以使用下面的方法来将$node添加为指定节点$neighbor的相邻节点

$neighbor必须存在,$node可以为新创建的节点,也可以为已存在的,如果$node为已存在的节点,它将移动到新的位置与$neighbor相邻,必要时它的父级将改变。

# 显性save
$node->afterNode($neighbor)->save();
$node->beforeNode($neighbor)->save(); # 隐性 save
$node->insertAfterNode($neighbor);
$node->insertBeforeNode($neighbor);

将数组构建为树

但使用create静态方法时,它将检查数组是否包含children键,如果有的话,将递归创建更多的节点。

$node = Category::create([
'name' => 'Foo', 'children' => [
[
'name' => 'Bar', 'children' => [
[ 'name' => 'Baz' ],
],
],
],
]);

现在$node->children包含一组已创建的节点。

将数组重建为树

你可以轻松的重建一个树,这对于大量的修改的树结构的保存非常有用。 Category::rebuildTree($data, $delete);

$data为代表节点的数组

$data = [
[ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ],
[ 'name' => 'bar' ],
];

上面有一个namefoo的节点,它有指定的id,代表这个已存在的节点将被填充,如果这个节点不存在,就好抛出一个ModelNotFoundException ,另外,这个节点还有children数组,这个数组也会以相同的方式添加到foo节点内。 bar节点没有主键,就是不存在,它将会被创建。 $delete 代表是否删除数据库中已存在的但是$data中不存在的数据,默认为不删除。

重建子树 对于4.3.8版本以后你可以重建子树

Category::rebuildSubtree($root, $data);

这将限制只重建$root子树

检索节点

在某些情况下我们需要使用变量$id代表目标节点的主键id

祖先和后代

Ancestors 创建一个节点的父级链,这对于展示当前种类的面包屑很有帮助。 Descendants 是一个父节点的所有子节点。 Ancestors和Descendants都可以预加载。

// Accessing ancestors
$node->ancestors; // Accessing descendants
$node->descendants;

通过自定义的查询加载ancestors和descendants:

$result = Category::ancestorsOf($id);
$result = Category::ancestorsAndSelf($id);
$result = Category::descendantsOf($id);
$result = Category::descendantsAndSelf($id);

大多数情况下,你需要按层级排序:

$result = Category::defaultOrder()->ancestorsOf($id);

祖先集合可以被预加载:

$categories = Category::with('ancestors')->paginate(30);

// 视图模板中面包屑:
@foreach($categories as $i => $category)
<small> $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' </small><br>
$category->name
@endforeach

将祖先的name全部取出后转换为数组,在用>拼接为字符串输出。

兄弟节点

有相同父节点的节点互称为兄弟节点

$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();

获取表的相关model

假设每一个category has many goods, 并且 hasMany 关系已经建立,怎么样简单的获取$category 和它所有后代下所有的goods?

// 获取后代的id
$categories = $category->descendants()->pluck('id'); // 包含Category本身的id
$categories[] = $category->getKey(); // 获得goods
$goods = Goods::whereIn('category_id', $categories)->get();

包含node深度(depth)

如果你需要知道node的出入那一层级:

$result = Category::withDepth()->find($id);

$depth = $result->depth;

根节点(root)是第0层(level 0),root的子节点是第一层(level 1),以此类推 你可以使用having约束来获得特定的层级的节点

$result = Category::withDepth()->having('depth', '=', 1)->get();

注意 这在数据库严格模式下无效

默认排序

所有的节点都是在内部严格组织的,默认情况下没有顺序,所以节点是随机展现的,这部影响展现,你可以按字母和其他的顺序对节点排序。

但是在一些情况下按层级展示是必要的,它对获取祖先和用于菜单顺序有用。

使用deaultOrder运用树的排序: $result = Category::defaultOrder()->get();

你也可以使用倒序排序: $result = Category::reversed()->get();

让节点在父级内部上下移动来改变默认排序:

$bool = $node->down();
$bool = $node->up(); // 向下移动3个兄弟节点
$bool = $node->down(3);

操作返回根据操作的节点的位置是否改变的布尔值

约束

很多约束条件可以被用到这些查询构造器上:

  • whereIsRoot() 仅获取根节点;
  • whereIsAfter($id) 获取特定id的节点后面的所有节点(不仅是兄弟节点)。
  • whereIsBefore($id) 获取特定id的节点前面的所有节点(不仅是兄弟节点)。

祖先约束

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

$node 可以为模型的主键或者模型实例

后代约束

$result = Category::whereDescendantOf($node)->get();
$result = Category::whereNotDescendantOf($node)->get();
$result = Category::orWhereDescendantOf($node)->get();
$result = Category::orWhereNotDescendantOf($node)->get();
$result = Category::whereDescendantAndSelf($id)->get(); //结果集合中包含目标node自身
$result = Category::whereDescendantOrSelf($node)->get();

构建树

在获取了node的结果集合后,我们就可以将它转化为树,例如: $tree = Category::get()->toTree();

这将在每个node上添加parent 和 children 关系,且你可以使用递归算法来渲染树:

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

$traverse = function ($categories, $prefix = '-') use (&$traverse) {
foreach ($categories as $category) {
echo PHP_EOL.$prefix.' '.$category->name; $traverse($category->children, $prefix.'-');
}
}; $traverse($nodes);

这将像下面类似的输出:

- Root
-- Child 1
--- Sub child 1
-- Child 2
- Another root

构建一个扁平树

你也可以构建一个扁平树:将子节点直接放于父节点后面。当你获取自定义排序的节点和不想使用递归来循环你的节点时很有用。 $nodes = Category::get()->toFlatTree();

之前的例子将向下面这样输出:

Root
Child 1
Sub child 1
Child 2
Another root

构建一个子树

有时你并不需要加载整个树而是只需要一些特定的子树: $root = Category::descendantsAndSelf($rootId)->toTree()->first(); 通过一个简单的查询我们就可以获得子树的根节点和使用children关系获取它所有的后代

如果你不需要$root节点本身,你可以这样: $tree = Category::descendantsOf($rootId)->toTree($rootId);

删除节点

删掉一个节点:

$node->delete();

**注意!**节点的所有后代将一并删除 注意! 节点需要向模型一样删除,不能使用下面的语句来删除节点:

Category::where('id', '=', $id)->delete();

这将破坏树结构 支持SoftDeletestrait,且在模型层

helper 方法

检查节点是否为其他节点的子节点 $bool = $node->isDescendantOf($parent);

检查是否为根节点 $bool = $node->isRoot();

其他的检查

  • $node->isChildOf($other);
  • $node->isAncestorOf($other);
  • $node->isSiblingOf($other);
  • $node->isLeaf()

检查一致性

你可以检查树是否被破环 $bool = Category::isBroken();

获取错误统计: $data = Category::countErrors();

它将返回含有一下键的数组

  • oddness -- lft 和 rgt 值错误的节点的数量
  • duplicates -- lft 或者 rgt 值重复的节点的数量
  • wrong_parent -- left 和 rgt 值 与parent_id 不对应的造成无效parent_id 的节点的数量
  • missing_parent -- 含有parent_id对应的父节点不存在的节点的数量

修复树

从v3.1往后支持修复树,通过parent_id字段的继承信息,给每个node设置合适的lft 和 rgt值 Node::fixTree();

作用域(scope)

假设你有个Memu模型和MenuItems.他们之间是one-to-many 关系。MenuItems有menu_id属性并实现nested sets模型。显然你想基于menu_id属性来单独处理每个树,为了实现这样的功能,我们需要指定这个menu_id属性为scope属性。

protected function getScopeAttributes()
{
return [ 'menu_id' ];
}

现在我们为了实现自定义的查询,我们需要提供需要限制作用域的属性。

MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK
MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scope
MenuItem::scoped([ 'menu_id' => 5 ])->fixTree();

但使用model实例查询node,scope自动基于设置的限制作用域属性来删选node。例如:

$node = MenuItem::findOrFail($id);

$node->siblings()->withDepth()->get(); // OK

使用实例来获取删选的查询: $node->newScopedQuery();

注意,当通过主键获取模型时不需要使用scope

$node = MenuItem::findOrFail($id); // OK
$node = MenuItem::scoped([ 'menu_id' => 5 ])->findOrFail(); // OK, 但是多余
编辑于 2018-02-25
Laravel
PHP 框架

laravel-nestedset:多级无限分类正确姿势的更多相关文章

  1. jquery 多级无限分类

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/ ...

  2. PHP在无限分类时注意的一些问题(不保证代码完全正确哦)

    转自:PHP在无限分类时注意的一些问题(http://lxiaoke.cn) (注意:代码使用的是原生PHP,旨在提供解决思路)1 无限分类的查找(获取所有节点) 代码: /** * 无限分类查询,默 ...

  3. MySql无限分类数据结构--预排序遍历树算法

    MySql无限分类数据结构--预排序遍历树算法 无限分类是我们开发中非常常见的应用,像论坛的的版块,CMS的类别,应用的地方特别多. 我们最常见最简单的方法就是在MySql里ID ,parentID, ...

  4. 使用 win10 的正确姿势

    17年9月初,写了第一篇<使用 win10 的正确姿势>,而现在半年多过去,觉得文章得更新一些了,索性直接来个第二版吧. -----2018.3.24 写 一. 重新定义桌面 我的桌面: ...

  5. 使用 win10 的正确姿势 (第二版)

    文章为本人原创,转载请注明出处,谢谢. 17年9月初,写了第一篇<使用 win10 的正确姿势>,而现在半年多过去,文章更新了一些,主要是桌面的变化. 一. 重新定义桌面 我的桌面: 将桌 ...

  6. 使用 Java8 Optional 的正确姿势(转)

    我们知道 Java 8 增加了一些很有用的 API, 其中一个就是 Optional. 如果对它不稍假探索, 只是轻描淡写的认为它可以优雅的解决 NullPointException 的问题, 于是代 ...

  7. Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)

    笔者所在的趣店 FED 早在去年 10 月份就已全面使用 Taro 框架开发小程序(当时版本为 1.1.0-beta.4),至今也上线了 2 个微信小程序.2 个支付宝小程序. 之所以选用 Taro, ...

  8. [转] 使用 Java8 Optional 的正确姿势

    [From] https://unmi.cc/proper-ways-of-using-java8-optional/ 我们知道 Java 8 增加了一些很有用的 API, 其中一个就是 Option ...

  9. GitHub 热点速览 Vol.18:刷 LeetCode 的正确姿势

    作者:HelloGitHub-小鱼干 摘要:找对路子,事半功倍,正如本周 GitHub Trending #刷 LeetCode# 主题想表达的那般,正确的学习姿势方能让人走得更远,走进大厂

随机推荐

  1. python 之 Django框架(模板系统、过滤器、simple_tag、inclusion_tag、Tags、母版、组件)

    12.35 Django模板系统 {{ }}和 {% %},变量相关的用{{}},逻辑相关的用{%%} app02/views: # 模板语言测试函数 def template_test(reques ...

  2. 微信小程序的登入与授权

    官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 小程序登录 小程序可以通 ...

  3. IOC+EF+Core项目搭建EF封装(一)

    添加应用Microsoft.EntityFrameworkCore:Microsoft.EntityFrameworkCore.Design:Microsoft.EntityFrameworkCore ...

  4. js校验规则--去空格、加空格

    为了更加直观,有些号码需要加空格: // 拼接空格,每4位加一个空格 let bankAccount = '6228888888888888888'; let blank_value = bankAc ...

  5. HTML学习摘要2

    DAY 2 HTML 标签可以拥有属性.属性提供了有关 HTML 元素的更多的信息. 属性总是以名称/值对的形式出现,比如:name="value". 属性总是在 HTML 元素的 ...

  6. unity4.3.4firedrillonline项目首次整合问题总结

    零.资源导入后把所有资源模型拖到场景中去,并reset Transform,使场景展现原有样子. 一.资源导入之后发现项目场景是黑的,添加灯光之后场景中大部分仍然是黑的(并没有光照的效果) 可能原因: ...

  7. Python 源码剖析 目录

    Python 源码剖析 作者: 陈儒 阅读者:春生 版本:python2.5 版本 本博客园的博客记录我会适当改成Python3版本 阅读 Python 源码剖析 对读者知识储备 1.C语言基础知识, ...

  8. redis实现的简单令牌桶

    这里给出的令牌桶是以redis单节点或者集群为中间件. 不过, 这里的实现比较简单, 主要提供两个函数, 一个用于消费令牌, 一个用于添加令牌. 这里, 消费令牌和添加令牌都是通过lua来保证原子性. ...

  9. Python日记(二):Python之禅

    The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Si ...

  10. Flink 使用(一)——从kafka中读取数据写入到HBASE中

    1.前言 本文是在<如何计算实时热门商品>[1]一文上做的扩展,仅在功能上验证了利用Flink消费Kafka数据,把处理后的数据写入到HBase的流程,其具体性能未做调优.此外,文中并未就 ...