dogado / baum
Baum 是为 Eloquent 模型实现嵌套集模式的实现。
Requires
- php: >=5.6.3
- illuminate/console: 5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*
- illuminate/database: 5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*
- illuminate/events: 5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*
- illuminate/filesystem: 5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*
- illuminate/support: 5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*
Requires (Dev)
- fabpot/php-cs-fixer: ^1.11
- mockery/mockery: ~0.9
- phpunit/phpunit: 4.*
- satooshi/php-coveralls: dev-master
This package is auto-updated.
Last update: 2020-04-09 11:54:41 UTC
README
已废弃 - 请不要再使用!将不会再有更新。
Baum

从 gazsp/baum 分支而来 - 修复了数据库事务错误的严重问题。
修复了在同时运行多个 INSERT
或 DELETE
操作时破坏嵌套集的漏洞。
描述和复现都非常困难,但当同时进行重建树的操作时可能会遇到错误。
在修复错误之前,只有树的重建在事务中。实际操作,例如 INSERT
或 DELETE
节点,不在事务中。
假设一个节点被删除,正在进行的重建... 然后,几乎在同一时间,第二个节点也被删除,第二个重建由于表锁定而停止。之后树被破坏。
错误
应用程序日志显示以下错误。
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction
SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active. Consider using PDOStatement::fetchAll(). Alternatively, if your code is only ever going to run against mysql, you may enable query buffering by setting the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute.
解决方案
我们在第一次写入数据库之前启动了新的进一步的事务。我们还需要在稍后提交这些事务。因此,我们在 Node
类中重写了 delete
和 finishSave
方法。
从 etrepat/baum 分支而来 - 继续开发并修复 Laravel 5.x 上的失败的单元测试
如果您发现了一个错误,请提交问题并提交一个包含失败的单元测试的 pull 请求。
Baum 是为 Laravel 5 的 Eloquent ORM 实现的 嵌套集模式。
为了与 Laravel 4.2.x 兼容,请查看 1.0.x 分支 或使用最新的 1.0.x 标签版本。
文档
关于嵌套集
嵌套集是一种智能的方式来实现一个有序树,它允许快速的非递归查询。例如,你可以通过单个查询获取一个节点所有后代,无论树有多深。缺点是插入/移动/删除需要复杂的SQL语句,但这个包会背后处理这些操作!
嵌套集适用于有序树(例如,菜单、商业类别)和需要高效查询的大树(例如,线程帖子)。
有关嵌套集的更多信息,请参阅维基百科条目。此外,这是一篇很好的入门教程:http://www.evanpetersen.com/item/nested-sets.html
背后的理论,简而言之
可视化嵌套集工作方式的一个简单方法是想象一个父实体包围其所有子实体,父实体的父实体包围它,依此类推。因此,这棵树
root
|_ Child 1
|_ Child 1.1
|_ Child 1.2
|_ Child 2
|_ Child 2.1
|_ Child 2.2
可以这样可视化
___________________________________________________________________
| Root |
| ____________________________ ____________________________ |
| | Child 1 | | Child 2 | |
| | __________ _________ | | __________ _________ | |
| | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
| |___________________________| |___________________________| |
|___________________________________________________________________|
数字代表左右边界。然后表格可能看起来像这样
id | parent_id | lft | rgt | depth | data
1 | | 1 | 14 | 0 | root
2 | 1 | 2 | 7 | 1 | Child 1
3 | 2 | 3 | 4 | 2 | Child 1.1
4 | 2 | 5 | 6 | 2 | Child 1.2
5 | 1 | 8 | 13 | 1 | Child 2
6 | 5 | 9 | 10 | 2 | Child 2.1
7 | 5 | 11 | 12 | 2 | Child 2.2
要获取一个父节点的所有子节点,你可以
SELECT * WHERE lft IS BETWEEN parent.lft AND parent.rgt
要获取子节点的数量,它是
(right - left - 1)/2
要获取一个节点及其所有祖先节点(追溯到根节点),你可以
SELECT * WHERE node.lft IS BETWEEN lft AND rgt
如你所见,这些在普通树中会递归且非常慢的查询突然变得非常快。不错吧?
安装
Baum从Laravel 5开始工作。你可以在composer.json
文件中添加它
"dogado/baum": ">=1.3.0"
运行composer install
来安装它。
与大多数Laravel 5包一样,你接下来需要注册Baum的服务提供者。为此,转到你的config/app.php
文件,并在providers
数组中添加以下行
'Baum\Providers\BaumServiceProvider',
入门
在包正确安装后,开始使用提供的生成器是最简单的方法
php artisan baum:install MODEL
用你计划用于你的嵌套集模型类的名称替换模型
生成器将安装一个迁移和一个模型文件到你的应用程序中,配置为使用Baum提供的嵌套集行为。你应该查看这些文件,因为它们中的每一个都描述了如何进行自定义。
接下来,你可能会运行artisan migrate
来应用迁移。
模型配置
为了与Baum一起工作,你必须确保你的模型类扩展Baum\Node
。
这是最简单的方式
class Category extends Baum\Node { }
这是一个稍微复杂一些的例子,其中我们自定义了列名
class Dictionary extends Baum\Node { protected $table = 'dictionary'; // 'parent_id' column name protected $parentColumn = 'parent_id'; // 'lft' column name protected $leftColumn = 'lidx'; // 'rgt' column name protected $rightColumn = 'ridx'; // 'depth' column name protected $depthColumn = 'nesting'; // guard attributes from mass-assignment protected $guarded = array('id', 'parent_id', 'lidx', 'ridx', 'nesting'); }
请记住,显然,列名必须与数据库表中的列名匹配。
迁移配置
你必须确保支持你的Baum模型的数据库表具有以下列
parent_id
:对父实体的引用(int)lft
:左索引边界(int)rgt
:右索引边界(int)depth
:深度或嵌套级别(int)
这是一个示例迁移文件
class Category extends Migration { public function up() { Schema::create('categories', function(Blueprint $table) { $table->increments('id'); $table->integer('parent_id')->nullable(); $table->integer('lft')->nullable(); $table->integer('rgt')->nullable(); $table->integer('depth')->nullable(); $table->string('name', 255); $table->timestamps(); }); } public function down() { Schema::drop('categories'); } }
你可以自由修改列名,前提是在迁移和模型中都进行更改。
用法
配置好模型并运行迁移后,你现在可以使用Baum与你的模型一起使用。以下是一些示例。
- 创建根节点
- 插入节点
- 删除节点
- 获取节点的嵌套级别
- 移动节点
- 向你的节点提问
- 关系
- 根和叶范围
- 访问祖先/后代链
- 限制返回的子级级别
- 自定义排序列
- 导出层次树
- 模型事件:
moving
和moved
- 范围支持
- 验证
- 树重建
- 软删除
- 播种/批量赋值
- 其他/实用函数
创建根节点
默认情况下,所有节点都被创建为根节点
$root = Category::create(['name' => 'Root category']);
或者,你可能需要将现有的节点转换为 根节点
$node->makeRoot();
你也可以将它的 parent_id
列设置为 null 以实现相同的行为
// This works the same as makeRoot() $node->parent_id = null; $node->save();
插入节点
// Directly with a relation $child1 = $root->children()->create(['name' => 'Child 1']); // with the `makeChildOf` method $child2 = Category::create(['name' => 'Child 2']); $child2->makeChildOf($root);
删除节点
$child1->delete();
被删除节点的后代也将被删除,并且所有的 lft
和 rgt
边界都将被重新计算。请注意,目前,对于后代,不会触发 deleting
和 deleted
模型事件。
获取节点的嵌套级别
getLevel()
方法将返回节点的当前嵌套级别或深度。
$node->getLevel() // 0 when root
移动节点
Baum 提供了多种移动节点的方法
moveLeft()
:找到左兄弟并移动到它的左边。moveRight()
:找到右兄弟并移动到它的右边。moveToLeftOf($otherNode)
:移动到 ... 的左边节点moveToRightOf($otherNode)
:移动到 ... 的右边节点makeNextSiblingOf($otherNode)
:moveToRightOf
的别名。makeSiblingOf($otherNode)
:makeNextSiblingOf
的别名。makePreviousSiblingOf($otherNode)
:moveToLeftOf
的别名。makeChildOf($otherNode)
:将节点变为 ... 的子节点。makeFirstChildOf($otherNode)
:将节点变为 ... 的第一个子节点。makeLastChildOf($otherNode)
:makeChildOf
的别名。makeRoot()
:将当前节点变为根节点。
例如
$root = Creatures::create(['name' => 'The Root of All Evil']); $dragons = Creatures::create(['name' => 'Here Be Dragons']); $dragons->makeChildOf($root); $monsters = new Creatures(['name' => 'Horrible Monsters']); $monsters->save(); $monsters->makeSiblingOf($dragons); $demons = Creatures::where('name', '=', 'demons')->first(); $demons->moveToLeftOf($dragons);
向节点提问
你可以向 Baum 节点提出一些问题
isRoot()
:如果是根节点,则返回 true。isLeaf()
:如果是叶节点(分支的末尾),则返回 true。isChild()
:如果是子节点,则返回 true。isDescendantOf($other)
:如果是其他节点的后代,则返回 true。isSelfOrDescendantOf($other)
:如果是自身或后代,则返回 true。isAncestorOf($other)
:如果是其他节点的祖先,则返回 true。isSelfOrAncestorOf($other)
:如果是自身或祖先,则返回 true。equals($node)
:当前节点实例等于其他节点。insideSubtree($node)
:检查给定的节点是否在由左和右索引定义的子树内。inSameScope($node)
:如果给定的节点与当前节点具有相同的范围,则返回 true。也就是说,如果scoped
属性中的每列在两个节点中都有相同的值。
使用前面的示例中的节点
$demons->isRoot(); // => false $demons->isDescendantOf($root) // => true
关系
Baum 为你的节点提供两个自引用的 Eloquent 关系:parent
和 children
。
$parent = $node->parent()->get(); $children = $node->children()->get();
根和叶作用域
Baum提供了一些基本的查询作用域,用于访问根节点和叶节点
// Query scope which targets all root nodes Category::roots() // All leaf nodes (nodes at the end of a branch) Category:leaves()
你可能只对第一个根节点感兴趣
$firstRootNode = Category::root();
访问祖先/后代链
Baum提供了几种方法来访问嵌套集树中节点的祖先/后代链。需要注意的是,它们以两种方式提供
首先作为查询作用域,返回一个Illuminate\Database\Eloquent\Builder
实例以继续查询。要从这些查询中获取实际结果,请记住调用get()
或first()
。
ancestorsAndSelf()
:包括当前节点在内的所有祖先链节点。ancestors()
:查询不包括当前节点的祖先链节点。siblingsAndSelf()
:实例作用域,针对父节点的所有子节点,包括自身。siblings()
:实例作用域,针对父节点的所有子节点,除了自身。leaves()
:实例作用域,针对所有没有子节点的嵌套子节点。descendantsAndSelf()
:作用域,针对自身及其所有嵌套子节点。descendants()
:所有子节点和嵌套子节点集。immediateDescendants()
:所有子节点集(非递归)。
其次,作为返回实际Baum\Node
实例(在适当的Collection
对象中)的方法
getRoot()
:从当前节点返回根节点。getAncestorsAndSelf()
:检索包括当前节点在内的所有祖先链。getAncestorsAndSelfWithoutRoot()
:所有祖先(包括当前节点),但不包括根节点。getAncestors()
:从数据库中获取不包括当前节点的所有祖先链。getAncestorsWithoutRoot()
:不包括当前节点和根节点的所有祖先。getSiblingsAndSelf()
:获取包括自身在内的所有父节点子节点。getSiblings()
:返回所有父节点子节点,除了自身。getLeaves()
:返回所有没有子节点的嵌套子节点。getDescendantsAndSelf()
:检索所有嵌套子节点和自身。getDescendants()
:检索所有子节点和嵌套子节点。getImmediateDescendants()
:检索所有子节点(非递归)。
以下是一个简单的示例,用于迭代一个节点的后代(假设有一个名称属性可用)
$node = Category::where('name', '=', 'Books')->first(); foreach($node->getDescendantsAndSelf() as $descendant) { echo "{$descendant->name}"; }
限制返回的子级级别
在某些情况下,如果层次结构的深度很大,可能需要限制返回的子级级别的数量(深度)。您可以在Baum中使用limitDepth
查询作用域来实现这一点。
以下代码片段将获取当前节点的后代,最多5个深度级别以下
$node->descendants()->limitDepth(5)->get();
同样,您可以通过提供所需的深度限制作为第一个参数,使用getDescendants
和getDescendantsAndSelf
方法来限制后代级别。
// This will work without depth limiting // 1. As usual $node->getDescendants(); // 2. Selecting only some attributes $other->getDescendants(array('id', 'parent_id', 'name')); ... // With depth limiting // 1. A maximum of 5 levels of children will be returned $node->getDescendants(5); // 2. A max. of 5 levels of children will be returned selecting only some attrs $other->getDescendants(5, array('id', 'parent_id', 'name'));
自定义排序列
默认情况下,在Baum中,所有结果都是根据一致性返回的lft
索引列值进行排序的。
如果您想更改此默认行为,您需要在模型中指定要用于排序结果的列名,如下所示
protected $orderColumn = 'name';
转储层次结构树
Baum 扩展了默认的 Eloquent\Collection
类,并为其提供了 toHierarchy
方法,该方法返回一个表示查询树的嵌套集合。
将完整的树形结构检索到一个常规的 Collection
对象中,并正确嵌套其子节点,操作非常简单
$tree = Category::where('name', '=', 'Books')->first()->getDescendantsAndSelf()->toHierarchy();
模型事件: moving
和 moved
Baum 模型在每次节点在嵌套集树中移动时都会触发以下事件: moving
和 moved
。这允许您在节点移动过程中钩入这些点。与正常的 Eloquent 模型事件一样,如果 moving
事件返回 false
,则移动操作将被取消。
建议通过使用模型的自定义启动方法来挂钩这些事件
class Category extends Baum\Node { public static function boot() { parent::boot(); static::moving(function($node) { // Before moving the node this function will be called. }); static::moved(function($node) { // After the move operation is processed this function will be // called. }); } }
范围支持
Baum 提供了一种简单的方法来实现嵌套集的“范围”,这限制了我们认为属于嵌套集树的哪些部分。这应该允许在同一数据库表中存在多个嵌套集树。
要使用范围功能,您可以在子类中重写 scoped
模型属性。此属性应包含一个数组,包含用于限制嵌套集查询的列名(数据库字段)
class Category extends Baum\Node { ... protected $scoped = array('company_id'); ... }
在先前的示例中,company_id
有效地限制了(或“范围”)一个嵌套集树。因此,对于该字段的每个值,我们都可以构建一个完全不同的树。
$root1 = Category::create(['name' => 'R1', 'company_id' => 1]); $root2 = Category::create(['name' => 'R2', 'company_id' => 2]); $child1 = Category::create(['name' => 'C1', 'company_id' => 1]); $child2 = Category::create(['name' => 'C2', 'company_id' => 2]); $child1->makeChildOf($root1); $child2->makeChildOf($root2); $root1->children()->get(); // <- returns $child1 $root2->children()->get(); // <- returns $child2
所有请求或遍历嵌套集树的方法都将使用 scoped
属性(如果提供)。
请注意,目前不支持在范围之间移动节点。
验证
::isValidNestedSet()
静态方法允许您检查您的基础树结构是否正确。它主要检查以下3件事
- 检查绑定的索引
lft
、rgt
是否不为空,rgt
值大于lft
且在父节点(如果设置)的范围内。 - 确保
lft
和rgt
列值的唯一性。 - 由于第一个检查实际上没有检查根节点,请检查每个根节点是否在其子节点的范围内具有
lft
和rgt
索引。
所有检查都是 范围感知的,并在需要的情况下将分别检查每个范围。
示例用法,给定一个 Category
节点类
Category::isValidNestedSet() => true
树重建
Baum 支持通过 ::rebuild()
静态方法进行完整的树结构重建(或重新索引)。
此方法将重新索引所有 lft
、rgt
和 depth
列值,仅从父 <-> 子关系的角度检查您的树。这意味着您只需要一个正确填充的 parent_id
列,Baum 将尽力重新计算其余部分。
当索引值出现严重错误或需要从另一个实现(可能具有 parent_id
列)进行转换时,这非常有用。
此操作也是 范围感知的,如果定义了范围,将分别重建所有范围。
简单示例用法,给定一个 Category
节点类
Category::rebuild()
不会检查树是否已有效,这意味着调用重建将始终重建树,无论其是否有效。如果您不希望这种行为,当 isValidNestedSet 返回 true 时,不要调用重建。
软删除
使用软删除 / restore()
不建议,如果树在软删除操作之后已修改,可能会导致问题。
播种/批量赋值
由于嵌套集结构通常涉及到多个方法调用以构建层次结构(这会导致多次数据库查询),Baum提供了两个方便的方法,可以将提供的节点属性数组映射到它们,从而创建层次树。
buildTree($nodeList)
:这是一个静态方法,将提供的节点属性数组映射到数据库中。makeTree($nodeList)
:这是一个实例方法,使用当前的节点实例作为子树的父节点,将提供的节点属性数组映射到数据库中。
这两个方法都会在主键未提供时创建新节点,如果提供了主键,则更新或创建节点,并删除在影响范围中不存在的所有节点。理解影响范围对于buildTree
静态方法是整个嵌套集树,对于makeTree
实例方法是当前节点的所有后代。
例如,假设我们想要将以下类别层次结构映射到我们的数据库中
- 电视 & 家庭影院
- 平板电脑 & 电子阅读器
- 计算机
- 笔记本电脑
- PC 笔记本
- Macbook(Air/Pro)
- 台式机
- 显示器
- 笔记本电脑
- 手机
这可以通过以下代码轻松实现
$categories = [ ['id' => 1, 'name' => 'TV & Home Theather'], ['id' => 2, 'name' => 'Tablets & E-Readers'], ['id' => 3, 'name' => 'Computers', 'children' => [ ['id' => 4, 'name' => 'Laptops', 'children' => [ ['id' => 5, 'name' => 'PC Laptops'], ['id' => 6, 'name' => 'Macbooks (Air/Pro)'] ]], ['id' => 7, 'name' => 'Desktops'], ['id' => 8, 'name' => 'Monitors'] ]], ['id' => 9, 'name' => 'Cell Phones'] ]; Category::buildTree($categories) // => true
之后,我们可以根据需要更新层次结构
$categories = [ ['id' => 1, 'name' => 'TV & Home Theather'], ['id' => 2, 'name' => 'Tablets & E-Readers'], ['id' => 3, 'name' => 'Computers', 'children' => [ ['id' => 4, 'name' => 'Laptops', 'children' => [ ['id' => 5, 'name' => 'PC Laptops'], ['id' => 6, 'name' => 'Macbooks (Air/Pro)'] ]], ['id' => 7, 'name' => 'Desktops', 'children' => [ // These will be created ['name' => 'Towers Only'], ['name' => 'Desktop Packages'], ['name' => 'All-in-One Computers'], ['name' => 'Gaming Desktops'] ]] // This one, as it's not present, will be deleted // ['id' => 8, 'name' => 'Monitors'], ]], ['id' => 9, 'name' => 'Cell Phones'] ]; Category::buildTree($categories); // => true
makeTree
实例方法的工作方式类似。唯一的不同是,它只会在调用节点实例的后代上执行操作。
现在假设我们已经在数据库中有了以下层次结构
- 电子产品
- 健康与美容
- 小型家电
- 大型家电
如果我们执行以下代码
$children = [ ['name' => 'TV & Home Theather'], ['name' => 'Tablets & E-Readers'], ['name' => 'Computers', 'children' => [ ['name' => 'Laptops', 'children' => [ ['name' => 'PC Laptops'], ['name' => 'Macbooks (Air/Pro)'] ]], ['name' => 'Desktops'], ['name' => 'Monitors'] ]], ['name' => 'Cell Phones'] ]; $electronics = Category::where('name', '=', 'Electronics')->first(); $electronics->makeTree($children); // => true
将得到以下结果
- 电子产品
- 电视 & 家庭影院
- 平板电脑 & 电子阅读器
- 计算机
- 笔记本电脑
- PC 笔记本
- Macbook(Air/Pro)
- 台式机
- 显示器
- 笔记本电脑
- 手机
- 健康与美容
- 小型家电
- 大型家电
更新和删除子树中的节点的工作方式相同。
其他/实用函数
节点提取查询范围
Baum提供了一些查询范围,可以用来从当前结果集中提取(移除)选定的节点。
withoutNode(node)
:从当前结果集中提取指定的节点。withoutSelf()
:从当前结果集中提取自身。withoutRoot()
:从结果集中提取当前根节点。
$node = Category::where('name', '=', 'Some category I do not want to see.')->first(); $root = Category::where('name', '=', 'Old boooks')->first(); var_dump($root->descendantsAndSelf()->withoutNode($node)->get()); ... // <- This result set will not contain $node
获取嵌套列表的列值
::getNestedList()
静态方法返回一个键值对数组,指示节点深度。这对于填充
它期望返回的列名,以及可选的:用作数组键的列(如果没有提供,将使用id
)和/或分隔符
public static function getNestedList($column, $key = null, $seperator = ' ', $symbol = '');
一个示例用法
$nestedList = Category::getNestedList('name'); // $nestedList will contain an array like the following: // array( // 1 => 'Root 1', // 2 => ' Child 1', // 3 => ' Child 2', // 4 => ' Child 2.1', // 5 => ' Child 3', // 6 => 'Root 2' // );
更多信息
您可以在wiki中找到关于Baum的更多信息、用法示例和/或常见问题。
完成本README后,请随意浏览wiki。
https://github.com/etrepat/baum/wiki
贡献
想要贡献?也许你发现了一些讨厌的bug?这是个好消息!
- 分叉并克隆项目:
git clone git@github.com:your-username/baum.git
。 - 运行测试并确保它们在你的设置下通过:
phpunit
。 - 创建你的bugfix/feature分支并开始编写代码。为你的更改添加测试。
- 确保所有测试仍然通过:
phpunit
。 - 推送到你的分叉并提交新的pull请求。
请参阅CONTRIBUTING.md文件以获取更详细的指南和建议。
授权
Baum遵循MIT许可证(详情请见LICENSE文件)。
由Estanislau Trepat (etrepat)编写。我还在twitter上@etrepat。