PHP怎么存储树形结构

wen PHP项目 1

本文目录导读:

PHP怎么存储树形结构

  1. 邻接表模型(Adjacency List)
  2. 嵌套集模型(Nested Set)
  3. JSON存储
  4. 内存树结构(PHP数组)
  5. 使用第三方库:Staudenmeir/ Laravel-adjacency-list
  6. 选择建议
  7. 最佳实践建议

在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 灵活,结构自由 查询、更新困难 配置类数据
内存数组 速度快,简单 不持久化 临时数据、测试

最佳实践建议

  1. 数据量小(<1000条):使用邻接表
  2. 数据量大且查询频繁:使用嵌套集
  3. 需要灵活结构:使用JSON
  4. 使用Laravel:建议使用专业库(如laravel-adjacency-list)

选择存储方案时,要综合考虑:

  • 数据量大小
  • 查询模式(读取多还是写入多)
  • 树深度(浅树还是深树)
  • 是否需要跨层查询
  • 维护成本

希望这些方案能帮助你选择最适合的树形结构存储方式!

抱歉,评论功能暂时关闭!