norbertogomez/doctrine2-nestedset

此Doctrine2扩展实现了嵌套集模型(修改过的先序遍历算法),用于Doctrine2。

1.0 2021-03-07 09:13 UTC

This package is auto-updated.

Last update: 2024-09-07 17:02:16 UTC


README

此Doctrine2扩展实现了嵌套集模型(修改过的先序遍历算法)用于Doctrine2。这允许在关系数据库的扁平表中存储层次结构数据,这是一种数据集合,其中每个项目都有一个父项和零个或多个子项。有关嵌套集模型的更多信息,请参阅

https://en.wikipedia.org/wiki/Nested_set_model

简介

嵌套集是一种存储层次结构数据的解决方案,它提供了非常快的读取访问。然而,更新嵌套集树的成本更高。因此,此解决方案最适合那些读得比写得多得多的层次结构。而且由于网络的本质,这是大多数网络应用程序的情况。

设置

要将您的模型设置为嵌套集,您的实体类必须实现DoctrineExtensions\NestedSet\Node接口。每个实体类必须包含用于存储嵌套集左值和右值的映射字段。

以下是一个使用注解映射的示例

namespace Entity;

use DoctrineExtensions\NestedSet\Node;

/**
 * @Entity
 */
class Category implements Node
{
    /**
     * @Id @Column(type="integer")
     * @GeneratedValue
     */
    private $id;

    /**
     * @Column(type="integer")
     */
    private $lft;

    /**
     * @Column(type="integer")
     */
    private $rgt;

    /**
     * @Column(type="string", length="16")
     */
    private $name;


    public function getId() { return $this->id; }

    public function getLeftValue() { return $this->lft; }
    public function setLeftValue($lft) { $this->lft = $lft; }

    public function getRightValue() { return $this->rgt; }
    public function setRightValue($rgt) { $this->rgt = $rgt; }

    public function getName() { return $this->name; }
    public function setName($name) { $this->name = $name; }

    public function __toString() { return $this->name; }
}

通常您不需要,也不应该与左值和右值交互。这些字段用于内部管理树结构。

多个树

嵌套集实现可以配置为允许您的表具有多个根节点,因此同一个表内可以有多个树。这是通过实现DoctrineExtensions\NestedSet\MultipleRootNode接口(而不是DoctrineExtensions\NestedSet\Node)并映射一个根字段来完成的。

扩展我们的注解示例

/**
 * @Column(type="integer")
 */
private $root;

public function getRootValue() { return $this->root; }
public function setRootValue($root) { $this->root = $root; }

与左值和右值一样,您通常不需要与根值交互。

处理树

在您成功设置嵌套集模型之后,您可以开始使用它。使用Doctrine2的嵌套集实现主要涉及两个类:Manager和NodeWrapper。NodeWrapper包装您的实体类,使您能够访问底层树结构。Manager提供创建新树和获取现有树的方法。

要从数据库中获取整个树

$config = new Config($em, 'Entity\Category');
$nsm = new Manager($config);
$rootNode = $nsm->fetchTree(1);

在此示例中,$rootNode是包装您的模型根节点的一个NodeWrapper实例。要访问您的模型对象

$modelObject = $rootNode->getNode();

创建根节点

$config = new Config($em, 'Entity\Category');
$nsm = new Manager($config);

$category = new Category();
$category->setName('Root Category 1');

$rootNode = $nsm->createRoot($category);

插入节点

$child1 = new Category();
$child1->setName('Child Category 1');

$child2 = new Category();
$child2->setName('Child Category 2');

$rootNode->addChild($child1);
$rootNode->addChild($child2);

删除节点

您必须始终使用NodeWrapper::delete()方法而不是EntityManager的delete方法来删除节点。NodeWrapper::delete()负责在删除节点时更新树

$category = $em->getRepository('Entity\Category')->findOneByName('Child Category 1');
$node = $nsm->wrapNode($category);
$node->delete();

删除节点也将删除该节点的所有后代。所以如果您不想删除它们,请确保在删除节点之前将它们移动到其他地方。

移动节点

移动节点很简单。NodeWrapper提供了几种方法来在树之间移动节点

  • moveAsLastChildOf($other)
  • moveAsFirstChildOf($other)
  • moveAsPrevSiblingOf($other)
  • moveAsNextSiblingOf($other)

检查节点

您可以通过使用以下一些函数来检查节点及其类型

$isLeaf = $node->isLeaf();
$isRoot = $node->isRoot();

检查和检索兄弟节点

您可以使用以下方法轻松检查一个节点是否有任何下一个或上一个兄弟节点

$hasNextSib = $node->hasNextSibling();
$hasPrevSib = $node->hasPrevSibling();

如果您存在下一个或上一个兄弟节点,您可以使用以下方法检索它们

$nextSib = $node->getNextSibling();
$prevSib = $node->getPrevSibling();

如果您想检索所有兄弟节点的一个数组,您可以使用getSiblings()方法

$siblings = $node->getSiblings();

检查和检索后代

您可以使用以下方法检查一个节点是否有父节点或子节点。

$hasChildren = $node->hasChildren();
$hasParent = $node->hasParent();

您可以使用以下方法获取节点的第一个和最后一个子节点。

$firstChild = $node->getFirstChild();
$lastChild = $node->getLastChild();

或者,如果您想获取节点的父节点

$parent = $node->getParent();

您可以使用以下方法获取节点的子节点

$children = $node->getChildren();

getChildren() 方法仅返回直接子节点。如果您想获取所有子节点,请使用 getDescendants() 方法。

您可以使用以下方法获取节点的子节点或祖先节点。

$descendants = $node->getDescendants();
$ancestors = $node->getAncestors();

有时您可能只想获取子节点或子节点的数量。您可以使用以下方法来完成此操作

$numChildren = $node->getNumberChildren();
$numDescendants = $node->getNumberDescendants();

getDescendants() 方法接受一个参数,您可以使用该参数指定结果的分支深度。例如,getDescendants(1) 仅检索直接子节点(位于1级以下的子节点,与 getChildren() 相同)。

渲染简单的树

$tree = $nsm->fetchTreeAsArray(1);

foreach ($tree as $node) {
    echo str_repeat('&nbsp;&nbsp;', $node->getLevel()) . $node . "<br>";
}

高级用法

前几节已解释了Doctrine嵌套集实现的常用用法。本节将进一步介绍。

带关系的树检索

如果您是要求严格的软件开发人员,这个问题可能已经出现在您的脑海中:“我如何检索带相关数据的树/分支?”。简单的例子:您想显示一个分类树,但您还想显示每个分类的相关数据,比如该分类中最热产品的详细信息。如前几节所示检索树,并在遍历树时访问关系是可能的,但会产生很多不必要的数据库查询。幸运的是,Manager 和嵌套集实现的一些灵活性可以帮您解决这个问题。嵌套集实现使用 QueryBuilder 对象进行所有数据库操作。通过为您提供访问嵌套集实现的基础查询构建器的权限,您可以在使用嵌套集的同时释放 QueryBuilder 的全部功能。

$qb = $em->createQueryBuilder();
$qb->select('c.name, p.name, m.name')
    ->from('Category', 'c')
    ->leftJoin('c.HottestProduct', 'p')
    ->leftJoin('p.Manufacturer', 'm');

现在我们需要将上述查询设置为树的基查询

$nsm->getConfiguration()->setBaseQueryBuilder($qb);
$tree = $nsm->fetchTree(1);

这就是它了,包含您所需所有相关数据的树,全部在一个查询中。

如果您没有设置自己的基查询,那么系统会自动为您创建一个。

完成操作后,将基查询重置为正常是一个好主意

$nsm->getConfiguration()->resetBaseQueryBuilder();

事务

当使用来自 NodeWrapper 的方法修改树时,每个方法都会立即执行。这与使用正常Doctrine2实体进行操作不同,在正常情况下,更改会通过 EntityManager 排队,并在调用 flush 时执行。

如果您正在执行多个更改,建议将这些更改包装在事务中

$em->getConnection()->beginTransaction();
try {

    $root = $nsm->createRoot(new Category('Root'));
    $root->addChild(new Category('Child 1'));
    $root->addChild(new Category('Child 2'));

    $em->getConnection()->commit();
} catch (Exception $e) {
    $em->close();
    $em->getConnection()->rollback();
    throw $e;
}

自定义左、右和根字段

NestedSet 需要您在实体类中包含左、右和根字段。默认情况下,NestedSet 期望这些字段的名称分别为 lft、rgt 和 root。您可以通过管理器配置来自定义这些字段的名称

$config = new Config($em, 'Entity\Category');
$config->setLeftFieldName('nsLeft');
$config->setRightFieldName('nsRight');
$config->setRootFieldName('nsRoot');
$nsm = new Manager($config);

结论

NestedSet 使在 Doctrine2 中管理层次数据变得快速且简单。