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


Posted in PHP onJuly 15, 2014

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

1. 领接表的方式;

2. 预排序遍历树方式;

假设树状结构如下图:

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

领接表方式

主要依赖于一个 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。

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

排序遍历树方式

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

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

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

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

接下来看看显示树/子树是多么简单,只需要一条 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+Mysql树型结构(无限分类)数据库设计的2种方式实例

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。

PHP 相关文章推荐
Windows IIS PHP 5.2 安装与配置方法
Jun 08 PHP
php HandlerSocket的使用
May 02 PHP
利用PHP生成静态HTML文档的原理
Oct 29 PHP
PHP结合JQueryJcrop实现图片裁切实例详解
Jul 24 PHP
PHP中Fatal error session_start()错误解决步骤
Aug 05 PHP
thinkphp浏览历史功能实现方法
Oct 29 PHP
PHP ajax 异步执行不等待执行结果的处理方法
May 27 PHP
php实现的验证码文件类实例
Jun 18 PHP
详解WordPress中创建和添加过滤器的相关PHP函数
Dec 29 PHP
php中json_encode不兼容JSON_UNESCAPED_UNICODE的解决方案
May 31 PHP
PHP使用DOM和simplexml读取xml文档的方法示例
Feb 08 PHP
基于CI(CodeIgniter)框架实现购物车功能的方法
Apr 09 PHP
PHP ignore_user_abort函数详细介绍和使用实例
Jul 15 #PHP
Linux下手动编译安装PHP扩展的例子分享
Jul 15 #PHP
可以保证单词完整性的PHP英文字符串截取代码分享
Jul 15 #PHP
PHP魔术引号所带来的安全问题分析
Jul 15 #PHP
PH P5.2至5.5、5.6的新增功能详解
Jul 14 #PHP
PHP文件锁定写入实例解析
Jul 14 #PHP
ThinkPHP惯例配置文件详解
Jul 14 #PHP
You might like
C#静态方法与非静态方法实例分析
2014/09/22 PHP
PHP输出两个数字中间有多少个回文数的方法
2015/03/23 PHP
WIFI万能钥匙密码查询接口实例
2015/09/28 PHP
thinkphp实现163、QQ邮箱收发邮件的方法
2015/12/18 PHP
探究Laravel使用env函数读取环境变量为null的问题
2016/12/06 PHP
javascript打印大全(打印页面设置/打印预览代码)
2013/03/29 Javascript
js实现将选中内容分享到新浪或腾讯微博
2015/12/16 Javascript
详解javascript跨浏览器事件处理程序
2016/03/27 Javascript
仅9张思维导图帮你轻松学习Javascript 就这么简单
2016/06/01 Javascript
仿百度换肤功能的简单实例代码
2016/07/11 Javascript
js动态生成form 并用ajax方式提交的实现方法
2016/09/09 Javascript
JS编写函数实现对身份证号码最后一位的验证功能
2016/12/29 Javascript
微信小程序开发之选项卡(窗口底部TabBar)页面切换
2017/04/12 Javascript
JavaScript实现的浏览器下载文件的方法
2017/08/09 Javascript
js数组实现权重概率分配
2017/09/12 Javascript
详解React 在服务端渲染的实现
2017/11/16 Javascript
r.js来合并压缩css文件的示例
2018/04/26 Javascript
微信小程序实现收藏与取消收藏切换图片功能
2018/08/03 Javascript
[01:21:58]守擂赛DOTA2第一周决赛
2020/04/22 DOTA
Python迭代器和生成器介绍
2015/03/06 Python
Python复制Word内容并使用格式设字体与大小实例代码
2018/01/22 Python
python:print格式化输出到文件的实例
2018/05/14 Python
flask-restful使用总结
2018/12/04 Python
Python中按键来获取指定的值
2019/03/02 Python
python Tensor和Array对比分析
2020/01/08 Python
Django单元测试中Fixtures的使用方法
2020/02/26 Python
Burberry英国官网:英国标志性奢侈品牌
2017/03/29 全球购物
Born鞋子官网:Born Shoes
2017/04/06 全球购物
匡威爱尔兰官网:Converse爱尔兰
2019/06/09 全球购物
Linux不知道文件后缀名怎么判断文件类型
2012/04/26 面试题
公司周年庆典邀请函
2014/01/12 职场文书
计算机学生的自我评价分享
2014/02/18 职场文书
致地震灾区的慰问信
2015/03/23 职场文书
《这片土地是神圣的》教学反思
2016/02/16 职场文书
又涨知识了,自律到底多重要?
2019/06/27 职场文书
浅谈Java父子类加载顺序
2021/08/04 Java/Android