PHP+Mysql树型结构(无限分类)数据库设计的2种方式实例

        咱们常常须要在关系型数据库中保存一些树状结构数据,好比分类、菜单、论坛帖子树状回复等。经常使用的方法有两种:php

        1. 领接表的方式;node

        2. 预排序遍历树方式;mysql

        假设树状结构以下图:算法

        

领接表方式sql

        主要依赖于一个 parent 字段,用于指向上级节点,将相邻的上下级节点链接起来,id 为自动递增自动,parent_id 为上级节点的 id。一目了然,“Java”是“Language”的子节点。数据库

        咱们要显示树,PHP 代码也能够很直观,代码以下:数组

<?php
/**
 * 获取父节点下的全部子节点
 * @since 2011-05-18
 * @param $parent_id 父节点 id,0 则显示整个树结构。
 * @param $level 当前节点所处的层级,用于缩进显示节点。
 * @return void
 */
function show_children ($parent_id = 0, $level = 0){
    // 获取父节点下的全部子节点
    $result = mysql_query('SELECT id, name FROM tree WHERE parent_id=' . intval($parent_id));
    // 显示每一个子节点
    while ($row = mysql_fetch_array($result)) {
        // 缩进显示
        echo '<div style="margin-left:' . ($level * 12) . 'px">' . $row['name'] . '</div>';
        // 递归调用当前函数,显示再下一级的子节点
        show_children($row['id'], $level + 1);
    }
}
?>

        想要显示整个树结构,调用 show_children()。想要显示“Database”子树,则调用 show_children(2),由于“Database”的 id 是 2。函数

        还有一个常常用到的功能是获取节点路径,即给出一个节点,返回从根节点到当前节点的路径。用函数实现以下:性能

<?php
/**
 * @param $id 须要获取路径的当前节点的 id。
 * @return array
 */
function get_path($id) {
    // 获取当前节点的父节点 id 和当前节点名
    $result = mysql_query('SELECT parent_id, name FROM tree WHERE id='.intval($id));
    $row = mysql_fetch_array($result);
    // 使用此数组保存路径
    $path = array();
    // 将当前节点名保存进路径数组中
    $path[] = $row['name'];
    // 若是父节点非 0,即非根节点,则进行递归调用获取父节点的路径
    if ($row['parent_id']) {
        // 递归调用,获取父节点的路径,而且合并到当前路径数组的其它元素前边
        $path = array_merge(get_path($row['parent_id']), $path);
    }
    return $path;
}
?>

        想要获取“MySQL 5.0”的路径,调用 get_path(4),4 便是这个节点的 id。fetch

        领接表方式的优势在于容易理解,代码也比较简单明了。缺点则是递归中的 SQL 查询会致使负载变大,特别是须要处理比较大型的树状结构的时候,查询语句会随着层级的增长而增长,WEB 应用的瓶颈基本都在数据库方面,因此这是一个比较致命的缺点,直接致使树结构的扩展困难重重。

排序遍历树方式

        如今咱们来聊聊第二种方式─预排序遍历树方式(即一般所说的 MPTT,Modified Preorder Tree Traversal)。此算法是在第一种方式的基础之上,给每一个节点增长一个左、右数字,用于标识节点的遍历顺序,以下图所示:

        从根节点开始左边为 1,而后下一个节点的左边为 2,以此类推,到最低层节点以后,最低层节点的右边为其左边的数字加 1。顺着这些节点,咱们能够很容易地遍历完整个树。根据上图,咱们对数据表作一些改变,增长两个字段,lft 和 rgt 用于存储左右数字(因为 left 和 right 是 MySQL 的保留字,因此咱们改用简写)。表中各行的内容也就变成了:

       接下来看看显示树/子树是多么简单,只须要一条 SQL 语句便可,好比显示“Database”子树,则须要获取到“Database”的左右数字,左为 2,右为 11,那么 SQL 语句是:  

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;

        SQL 语句是简单了,但咱们所但愿的缩进显示倒是个问题。何时应该显示缩进?缩进多少单位?解决这个问题,须要使用堆栈,即后进先出(LIFO),每到一个节点,将其右边的数字压入堆栈中。咱们知道,全部节点右边的值都比其父节点右边的值小,那么将当前节点右边的值和堆栈最上边的右边值进行比较,若是当前节点比堆栈最上边的值小,表示当前堆栈里边剩下的都是父节点了,这时能够显示缩进,堆栈的元素数量便是缩进深度。PHP 代码实现以下:

<?php
/**
 * @param $root_id 须要显示的树/子树根节点 id
 */
function show_tree($root_id = 1) 
{
    // 获取当前根节点的左右数值
    $result = mysql_query('SELECT lft, rgt FROM tree WHERE id='.intval($root_id));
    $row = mysql_fetch_array($result);
    // 堆栈,存储节点右边的值,用于显示缩进
    $stack = array();
    // 获取 $root_id 节点的全部子孙节点
    $result = mysql_query('SELECT name, lft, rgt FROM tree WHERE lft BETWEEN '.$row['lft'].' AND '.$row['rgt'].' ORDER BY lft ASC');
    // 显示树的每一个节点
    while ($row = mysql_fetch_array($result)) {
        if (count($stack)>0) { //仅当堆栈非空的时候检测
            // 若是当前节点右边的值比堆栈最上边的值大,则移除堆栈最上边的值,由于这个值对应的节点不是当前节点的父节点
            while ($row['rgt'] > $stack[count($stack)-1]) {
                array_pop($stack);
            } //while 循环结束以后,堆栈里边只剩下当前节点的父节点了
        }
        // 如今能够显示缩进了
        echo '<div style="margin-left:'.(count($stack)*12).'px">'.$row['name'].'</div>';
        // 将当前的节点压入堆栈里边,为循环后边的节点缩进显示作好准备
        array_push($stack, $row['rgt']);
    }
}
?>

获取整个树调用 show_tree(),获取“Database”子树调用show_tree(2)。在这个函数中,咱们总算不须要用到递归了,呵呵。

接下来是显示从根节点到某节点的路径,这比起领接表方式来讲也简单了不少,只须要一句 SQL 就行,不用递归  好比获取“ORACLE”这个节点的路径,其左右值分别是 7 和 10,则 SQL 语句为:

SELECT name FROM tree WHERE lft <= 7 AND rgt >= 10 ORDER BY lft ASC;

PHP 函数实现以下:

<?php
/**
 * @param $node_id 须要获取路径的节点 id
 */
function get_path2($node_id) {
    // 获取当前节点的左右值
    $result = mysql_query('SELECT lft, rgt FROM tree WHERE id='.intval($node_id));
    $row = mysql_fetch_array($result);
    // 获取路径中的全部节点
    $result = mysql_query('SELECT name FROM tree WHERE lft <= '.$row['lft'].' AND rgt >= '.$row['rgt'].' ORDER BY lft ASC');
    $path = array();
    while ($row = mysql_fetch_array($result)) {
        $path[] = $row['name'];
    }
    return $path;
}
?>

        显示树和路径都没问题了,如今须要了解一下如何插入一个节点。插入新节点以前,首先要给这个节点腾出空位来,假设咱们如今要在“ORACLE 9i”这个节点右边增长一个“ORACLE 10”,则腾位的 SQL 语句以下(“ORACLE 9i”的右边值为 9):

UPDATE tree SET rgt=rgt+2 WHERE rgt>9;
UPDATE tree SET lft=lft+2 WHERE lft>9;

位置空出来了,开始插入新节点吧:

INSERT INTO tree SET lft=10, rgt=11, name='ORACLE 10';

调用 show_tree() 看看结果对不对  具体的 PHP 实现代码这里就不写了。

如今总结一下预排序遍历树方式的优缺点。缺点是算法比较抽象,不容易理解,增长节点的时候虽然只用了几条 SQL 语句,但可能会须要更新不少记录,从而形成阻塞。优势是树的构造,路径获取方面性能都比领接表方式好不少。也就是说,这个算法牺牲了一些写的性能来换取读的性能,在 WEB 应用中,读数据库的比例远大于写数据库的比例,因此预排序遍历树方式比领接表方式更加受欢迎,更加实用,不少应用中都能看到 MPTT 的影子,一般所用的表里都有字段 lft 和 rgt。

相关文章
相关标签/搜索