本文目录导读:

- 邻接表模型(Adjacency List)
- 嵌套集模型(Nested Set)
- JSON存储
- 内存树结构(PHP数组)
- 使用第三方库:Staudenmeir/ Laravel-adjacency-list
- 选择建议
- 最佳实践建议
在PHP中存储树形结构有多种方法,以下是几种常见的方案:
邻接表模型(Adjacency List)
最常用的方式,每个节点存储父节点的ID。
数据库表结构:
CREATE TABLE categories (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
parent_id INT DEFAULT NULL,
sort_order INT DEFAULT 0,
FOREIGN KEY (parent_id) REFERENCES categories(id)
);
PHP操作示例:
class CategoryTree {
private $db;
public function __construct($pdo) {
$this->db = $pdo;
}
// 添加节点
public function addNode($name, $parentId = null) {
$sql = "INSERT INTO categories (name, parent_id) VALUES (?, ?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$name, $parentId]);
}
// 获取所有节点并构建树
public function getTree() {
$sql = "SELECT * FROM categories ORDER BY parent_id, sort_order";
$stmt = $this->db->query($sql);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 构建树结构
$tree = [];
$map = [];
foreach ($items as $item) {
$map[$item['id']] = array_merge($item, ['children' => []]);
}
foreach ($map as $id => &$node) {
if ($node['parent_id'] && isset($map[$node['parent_id']])) {
$map[$node['parent_id']]['children'][] = &$node;
} else {
$tree[] = &$node;
}
}
unset($node);
return $tree;
}
// 递归获取子树
public function getSubtree($parentId) {
$sql = "SELECT * FROM categories WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$parentId]);
$node = $stmt->fetch(PDO::FETCH_ASSOC);
if ($node) {
$node['children'] = $this->getChildren($parentId);
}
return $node;
}
private function getChildren($parentId) {
$sql = "SELECT * FROM categories WHERE parent_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$parentId]);
$children = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($children as &$child) {
$child['children'] = $this->getChildren($child['id']);
}
return $children;
}
}
嵌套集模型(Nested Set)
适合频繁查询子树和层级关系的场景。
数据库表结构:
CREATE TABLE categories_nested (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL,
level INT NOT NULL
);
PHP操作示例:
class NestedSet {
private $db;
public function __construct($pdo) {
$this->db = $pdo;
}
// 添加根节点
public function addRoot($name) {
$sql = "SELECT COALESCE(MAX(rgt), 0) + 1 FROM categories_nested";
$lft = $this->db->query($sql)->fetchColumn();
$sql = "INSERT INTO categories_nested (name, lft, rgt, level) VALUES (?, ?, ?, 0)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$name, $lft, $lft + 1]);
}
// 添加子节点
public function addNode($name, $parentId) {
$sql = "SELECT lft, rgt, level FROM categories_nested WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$parentId]);
$parent = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$parent) return false;
// 更新所有受影响节点的左右值
$sql = "UPDATE categories_nested SET rgt = rgt + 2 WHERE rgt > ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$parent['lft']]);
$sql = "UPDATE categories_nested SET lft = lft + 2 WHERE lft > ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$parent['lft']]);
// 插入新节点
$sql = "INSERT INTO categories_nested (name, lft, rgt, level) VALUES (?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$name, $parent['lft'] + 1, $parent['lft'] + 2, $parent['level'] + 1]);
}
// 获取整个树
public function getTree() {
$sql = "SELECT id, name, level FROM categories_nested ORDER BY lft";
return $this->db->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
// 获取某节点的所有后代
public function getDescendants($nodeId) {
$sql = "SELECT child.*
FROM categories_nested AS child,
categories_nested AS parent
WHERE parent.id = ?
AND child.lft BETWEEN parent.lft AND parent.rgt
ORDER BY child.lft";
$stmt = $this->db->prepare($sql);
$stmt->execute([$nodeId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
JSON存储
适合数据量小,需要灵活性高的场景。
// 数据库表结构
/*
CREATE TABLE tree_data (
id INT PRIMARY KEY AUTO_INCREMENT,
data JSON NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
*/
class JsonTree {
private $db;
public function __construct($pdo) {
$this->db = $pdo;
}
public function saveTree($treeData) {
$json = json_encode($treeData, JSON_UNESCAPED_UNICODE);
$sql = "INSERT INTO tree_data (data) VALUES (?)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$json]);
}
public function getTree($id) {
$sql = "SELECT data FROM tree_data WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$id]);
$data = $stmt->fetchColumn();
return json_decode($data, true);
}
// 在内存中操作树
public function addNode(&$tree, $parentId, $node) {
foreach ($tree as &$item) {
if ($item['id'] === $parentId) {
$item['children'][] = $node;
return true;
}
if (isset($item['children'])) {
if ($this->addNode($item['children'], $parentId, $node)) {
return true;
}
}
}
return false;
}
}
内存树结构(PHP数组)
适合数据量较小,不需要持久化的场景。
class MemoryTree {
private $tree = [];
// 初始化树
public function buildTree($data, $parentId = 0) {
$branch = [];
foreach ($data as $item) {
if ($item['parent_id'] === $parentId) {
$children = $this->buildTree($data, $item['id']);
if ($children) {
$item['children'] = $children;
}
$branch[] = $item;
}
}
return $branch;
}
// 遍历树
public function traverseTree($tree, &$result = []) {
foreach ($tree as $node) {
$result[] = $node['id'];
if (isset($node['children'])) {
$this->traverseTree($node['children'], $result);
}
}
return $result;
}
// 查找节点
public function findNode($tree, $id) {
foreach ($tree as $node) {
if ($node['id'] == $id) {
return $node;
}
if (isset($node['children'])) {
$found = $this->findNode($node['children'], $id);
if ($found) {
return $found;
}
}
}
return null;
}
}
使用第三方库:Staudenmeir/ Laravel-adjacency-list
如果你使用Laravel框架,可以使用成熟的第三方包。
// composer require staudenmeir/laravel-adjacency-list
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
class Category extends Model {
use HasRecursiveRelationships;
// 获取子树
public function getTree() {
return $this->descendants()->get();
}
// 获取整个树
public static function allTree() {
return static::tree()->get();
}
// 找祖先
public function getAncestors($id) {
return static::find($id)->ancestors()->get();
}
}
选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 邻接表 | 简单易懂,插入删除容易 | 查询子树需要递归 | 大多数场景 |
| 嵌套集 | 查询效率高,无需递归 | 插入删除复杂,维护成本高 | 频繁查询、较少修改 |
| JSON | 灵活,结构自由 | 查询、更新困难 | 配置类数据 |
| 内存数组 | 速度快,简单 | 不持久化 | 临时数据、测试 |
最佳实践建议
- 数据量小(<1000条):使用邻接表
- 数据量大且查询频繁:使用嵌套集
- 需要灵活结构:使用JSON
- 使用Laravel:建议使用专业库(如laravel-adjacency-list)
选择存储方案时,要综合考虑:
- 数据量大小
- 查询模式(读取多还是写入多)
- 树深度(浅树还是深树)
- 是否需要跨层查询
- 维护成本
希望这些方案能帮助你选择最适合的树形结构存储方式!