baril/bonsai

Eloquent 的闭包表模式的实现。

v3.2.0 2024-05-21 16:59 UTC

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 包)。

⚠️ ancestorsdescendants 关系为只读!尝试在这些关系上使用 attachdetach 方法将抛出异常。

ancestorsdescendants 关系有以下方法

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

当然,这也适用于 ancestorsparent 关系。

方法

特性定义了以下方法

  • isRoot(): 如果项目的 parent_idnull,则返回 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);