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