baril / bonsai
Eloquent 的闭包表模式的实现。
Requires
- illuminate/console: ^8.0|^9.0|^10.0|^11.0
- illuminate/database: ^8.0|^9.0|^10.0|^11.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0
Requires (Dev)
- baril/orderly: ^3.0
- laravel/legacy-factories: ^1.3.2
- orchestra/testbench: ^6.23|^7.0|^8.0|^9.0
- squizlabs/php_codesniffer: ^2.8
Suggests
- baril/octopus: Required to use the $node->siblings() relation.
- baril/orderly: Required for ordered trees.
This package is auto-updated.
Last update: 2024-09-21 17:38:35 UTC
README
此包实现了 Laravel 和 MySQL 的“闭包表”设计模式。此模式允许对存储在关系型数据库中的树状结构进行更快地查询。它是嵌套集的替代方案。
版本兼容性
闭包表模式
假设你有一个包含标签分层列表的 tags
表。你可能有一个名为 parent_id
或类似的自引用外键。
闭包表模式表示你将创建一个辅助表(让我们称之为 tag_tree
),包含以下列
ancestor_id
:主表的外键,descendant_id
:主表的外键,depth
:无符号整数。
该表包含所有可能的祖先和后代组合。例如,以下树
1
├ 2
│ ├ 3
│ └ 4
└ 5
将生成以下闭包
设置
新安装
首先,你的主表需要一个 parent_id
列(名称可以配置)。这个列包含规范数据:闭包只是该信息的复制。
然后,让模型实现 Baril\Bonsai\Concerns\BelongsToTree
特性。
你可以使用以下属性来配置表和列名称
$parentForeignKey
:主表中自引用外键的名称(默认为parent_id
),$closureTable
:闭包表的名称(默认为模型名称的小写形式后缀为_tree
,例如tag_tree
)。
class Tag extends \Illuminate\Database\Eloquent\Model { use \Baril\Bonsai\Concerns\BelongsToTree; protected $parentForeignKey = 'parent_tag'; protected $closureTable = 'tag_closures'; }
bonsai:grow
命令将根据模型配置生成闭包表的迁移文件
php artisan bonsai:grow "App\\Models\\Tag"
如果你使用 --migrate
选项,该命令还将运行迁移。如果你的主表已包含数据,它还将插入现有数据的闭包。
php artisan bonsai:grow "App\\Models\\Tag" --migrate
⚠️ 如果你使用 --migrate
选项,任何其他挂起的迁移也将运行。
还有一些其他选项:使用 --help
了解更多信息。
Artisan 命令
除了上述的 bonsai:grow
命令外,此包还提供以下命令
如果你的数据以某种方式损坏,bonsai:fix
命令将截断闭包表并重新填充它(基于主表 parent_id
列中的数据)
php artisan bonsai:fix "App\\Models\\Tag"
bonsai:show
命令提供了一种快速简便的方法来输出树的内容。它接受一个 label
参数,用于定义使用哪个列(或访问器)作为标签。你可以可选地指定最大深度。
php artisan bonsai:show "App\\Models\\Tag" --label=name --depth=3
基本用法
只需填写模型的 parent_id
并保存模型:闭包表将相应地更新。
$tag = Tag::find($tagId); $tag->parent_id = $parentTagId; // or: $tag->parent()->associate($parentTag); $tag->save();
如果发生重复错误(即如果 parent_id
对应于模型本身或其子代之一),则 save
方法将抛出 \Baril\Bonsai\TreeException
。
当你删除一个模型时,其闭包将自动删除。如果模型有子代,则 delete
方法将抛出 TreeException
。你需要使用以下两种方法之一
deleteTree
将删除节点及其所有子代,deleteNode
将从树中删除节点,即将其子代附加到其父代(或将其子代作为根节点),然后删除该节点。
try { $tag->delete(); } catch (\Baril\Bonsai\TreeException $e) { // some specific treatment // ... $tag->deleteTree(); }
关系
该特性定义了以下关系
parent
: 与父级的关系为BelongsTo
,children
: 与子级的关系为HasMany
,ancestors
: 与祖先的关系为BelongsToMany
,descendants
: 与后代的关系为BelongsToMany
,siblings
: 与同父级其他子级的关系为HasMany
(需要安装baril/octopus
包)。
⚠️ ancestors
和 descendants
关系为只读!尝试在这些关系上使用 attach
或 detach
方法将抛出异常。
ancestors
和 descendants
关系有以下方法
includingSelf()
: 将项目本身包含在关系的结果中,orderByDepth($direction = 'asc')
,upToDepth($depth)
: 将检索到(包括)提供的$depth
的祖先/后代。
加载或预加载 descendants
关系将自动加载 children
关系(无需额外查询)。此外,它还将递归地加载所有预加载的后代 children
关系
$tags = Tag::with('descendants')->limit(10)->get(); // The following code won't execute any new query: foreach ($tags as $tag) { dump($tag->name); foreach ($tag->children as $child) { dump('-' . $child->name); foreach ($child->children as $grandchild) { dump('--' . $grandchild->name); } } }
当然,这也适用于 ancestors
和 parent
关系。
方法
特性定义了以下方法
isRoot()
: 如果项目的parent_id
为null
,则返回true
,isLeaf()
: 检查项目是否为叶节点(即没有子节点),hasChildren()
:$tag->hasChildren()
与!$tag->isLeaf()
类似,但更易读,isChildOf($item)
,isParentOf($item)
,isDescendantOf($item)
,isAncestorOf($item)
,isSiblingOf($item)
,findCommonAncestorWith($item)
: 返回两个项目之间的第一个共同祖先,如果没有共同祖先(可能发生在树有多个根的情况下),则返回null
,getDistanceTo($item)
: 返回两个项目之间的 "距离",getDepth()
: 返回项目在树中的深度(根元素的深度为 0),getSubtreeDepth()
: 返回以项目为根的子树的深度(如果是叶节点,则为 0)。
此外,可以使用 getTree
静态方法检索整个树
$tags = Tag::getTree();
它将返回一个根元素的集合,其中每个元素都预加载了 children
关系,直到叶子节点。
查询范围
withAncestors($depth = null, $constraints = null)
: 简写为with('ancestors')
,增加了指定$depth
限制的能力(例如,$query->withAncestors(1)
将仅加载直接父级)。可选地,您可以传递额外的$constraints
。withDescendants($depth = null, $constraints = null)
.withDepth($as = 'depth')
: 将在结果模型上添加一个depth
列(或您提供的任何别名)。whereIsRoot($bool = true)
: 限制查询为没有父级的项目(通过将$bool
参数设置为false
来反转范围的行为)。whereIsLeaf($bool = true)
.whereHasChildren($bool = true)
: 是whereIsLeaf
的反义词。whereIsDescendantOf($ancestor, $maxDepth = null, $includingSelf = false)
: 限制查询为$ancestor
的后代,可选地,您可以提供$maxDepth
。如果将$includingSelf
参数设置为true
,则祖先也将包含在查询结果中。($ancestor
参数可以是 id 或 Model 本身。)whereIsAncestorOf($descendant, $maxDepth = null, $includingSelf = false)
.
有序树
如果您需要明确地按顺序排列树的每一级,可以使用 Baril\Bonsai\Concerns\BelongsToOrderedTree
特性(而不是 BelongsToTree
)。为了使用此特性,除了 Bonsai 之外,还需要 Orderly 包。
composer require baril/orderly
您的主表(主表列名可以通过$orderColumn
属性进行配置)需要有一个position
列。
class Tag extends \Illuminate\Database\Eloquent\Model { use \Baril\Bonsai\Concerns\BelongsToOrderedTree; protected $orderColumn = 'order'; }
现在将对children
关系进行排序。如果您需要按其他字段排序,首先需要使用unordered
作用域。
$children = $this->children()->unordered()->orderBy('name');
此外,现在将提供在Orderly包文档中描述的由Orderable
特性定义的所有方法。
$lastChild->moveToPosition(1);