nlzet / doctrine2-nestedset
这是一个 Doctrine2 嵌套集扩展。
Requires
- php: >=5.3.3
This package is not auto-updated.
Last update: 2024-10-01 06:54:25 UTC
README
此 Doctrine2 扩展实现了嵌套集模型(修改后的前序遍历算法),允许在关系数据库的平面表中存储层次数据,其中每个项目有一个父项和零个或多个子项。有关嵌套集模型的更多信息,请参阅
https://en.wikipedia.org/wiki/Nested_set_model
简介
嵌套集是一种用于存储层次数据的解决方案,它提供了非常快的读取访问。然而,更新嵌套集树的成本较高。因此,这种解决方案最适合那些读取频率远高于写入频率的层次结构。由于网络的本性,对于大多数Web应用来说,都是这种情况。
设置
要将您的模型设置为嵌套集,您的实体类必须实现 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(' ', $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 实体工作不同,在正常 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 中管理层次数据变得快速且简单。