软件开发过程中我们经常会遇到各种各样多级的数据结构需求。刚开始与产品沟通的最多只支持3级。后期根据需求的迭代3级变成多级再变成无限级。有时候产品为了减少麻烦产品初级就会定义支持无限的分类结构。分类的产品需求非常简单可能就一句话支持无限级的分类数据结构。但分类的代码开发繁琐麻烦有时候还有点复杂。

今天为大家介绍两种无限级分类设计方案。大家可以根据自身的业务需求合理使用解决方案。

第一种方案:父ID方式

1、表结构设计

CREATE TABLE IF NOT EXISTS `classify`(
   `class_id` INT UNSIGNED AUTO_INCREMENT COMMENT '自增分类id',
   `class_name` VARCHAR(128) NOT NULL COMMENT '分类名称',
   `type` SMALLINT(1) NOT NULL DEFAULT 1 COMMENT '分类类型',
   `parent_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '分类父级id',
   `status` SMALLINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '1 正常 2 删除',
   `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间',
   `ext_data` VARCHAR(1024) NOT NULL DEFAULT '[]' COMMENT '扩展信息json',
   PRIMARY KEY ( `class_id` )
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '综合业务分类表';

#批量插入测试数据
INSERT INTO classify(`class_id`, `class_name`, `parent_id`, `status`) values(1, 'a', 0, 1),(2,'a1',1,1),(3,'b',0,1),(4,'a11',2,1),(5,'a12',2,1)

2、代码开发

#查询所有分类数据(执行db一次)
$conn = new mysqli('127.0.0.1', 'root', '', 'test');
if ($conn->connect_error) {
    die("连接失败: " . $conn->connect_error);
}
$sql = "SELECT class_id, class_name, parent_id FROM classify WHERE `type` = 1";
$result = $conn->query($sql);
$arrList = array();
while($row = $result->fetch_assoc()) {
	$arrList[] = $row;
}

#递归函数组合分类数据结构
function build_classify_tree($arrList,$intClassId) { 
  $arrTree = array(); 
  foreach($arrList as $arrItem) {
    if($arrItem['parent_id'] == $intClassId) { 
      $arrItem['child'] = build_classify_tree($arrList, $arrItem['class_id']); 
      $arrTree[] = $arrItem;
    } 
  }
  return $arrTree; 
}

$arrTree = build_classify_tree($arrList, 0);
$conn->close();

3、执行结果

array(2) {
  [0]=>
  array(4) {
    ["class_id"]=>
    string(1) "1"
    ["class_name"]=>
    string(1) "a"
    ["parent_id"]=>
    string(1) "0"
    ["child"]=>
    array(1) {
      [0]=>
      array(4) {
        ["class_id"]=>
        string(1) "2"
        ["class_name"]=>
        string(2) "a1"
        ["parent_id"]=>
        string(1) "1"
        ["child"]=>
        array(2) {
          [0]=>
          array(4) {
            ["class_id"]=>
            string(1) "4"
            ["class_name"]=>
            string(3) "a11"
            ["parent_id"]=>
            string(1) "2"
            ["child"]=>
            array(0) {
            }
          }
          [1]=>
          array(4) {
            ["class_id"]=>
            string(1) "5"
            ["class_name"]=>
            string(3) "a12"
            ["parent_id"]=>
            string(1) "2"
            ["child"]=>
            array(0) {
            }
          }
        }
      }
    }
  }
  [1]=>
  array(4) {
    ["class_id"]=>
    string(1) "3"
    ["class_name"]=>
    string(1) "b"
    ["parent_id"]=>
    string(1) "0"
    ["child"]=>
    array(0) {
    }
  }
}

第二种方案:父路径方式

1、表结构设计

CREATE TABLE IF NOT EXISTS `category`(
   `class_id` INT UNSIGNED AUTO_INCREMENT COMMENT '自增分类id',
   `class_name` VARCHAR(128) NOT NULL COMMENT '分类名称',
   `type` SMALLINT(1) NOT NULL DEFAULT 1 COMMENT '分类类型',
   `parent_path` VARCHAR(2048) NOT NULL DEFAULT '' COMMENT '分类父级路径',
   `status` SMALLINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '1 正常 2 删除',
   `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间',
   `ext_data` VARCHAR(1024) NOT NULL DEFAULT '[]' COMMENT '扩展信息json',
   PRIMARY KEY ( `class_id` )
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#批量插入测试数据
INSERT INTO category(`class_id`, `class_name`, `parent_path`, `status`) values(1, 'a', '/', 1),(2,'a1','/a/',1),(3,'b','/',1),(4,'a11','/a/a1/',1),(5,'a12', '/a/a1/', 1)

2、代码设计

$conn = new mysqli('127.0.0.1', 'root', '', 'test');
if ($conn->connect_error) {
    die("连接失败: " . $conn->connect_error);
}
$sql = "SELECT class_id, class_name, parent_path FROM category WHERE `type` = 1";
$result = $conn->query($sql);
$arrList = array();
while($row = $result->fetch_assoc()) {
	$arrList[] = $row;
}
function build_classify_tree($arrList,$strParentPath) { 
  $arrTree = array(); 
  foreach($arrList as $arrItem) {
    if($arrItem['parent_path'] == $strParentPath) {
      $strChildParentPath = sprintf('%s%s', $strParentPath, $arrItem['class_name'] . '/');
      $arrItem['child'] = build_classify_tree($arrList, $strChildParentPath); 
      $arrTree[] = $arrItem;
    } 
  }
  return $arrTree; 
}

$arrTree = build_classify_tree($arrList, '/');
var_dump($arrTree);
$conn->close();

3、执行结果

array(2) {
  [0]=>
  array(4) {
    ["class_id"]=>
    string(1) "1"
    ["class_name"]=>
    string(1) "a"
    ["parent_path"]=>
    string(1) "/"
    ["child"]=>
    array(1) {
      [0]=>
      array(4) {
        ["class_id"]=>
        string(1) "2"
        ["class_name"]=>
        string(2) "a1"
        ["parent_path"]=>
        string(3) "/a/"
        ["child"]=>
        array(2) {
          [0]=>
          array(4) {
            ["class_id"]=>
            string(1) "4"
            ["class_name"]=>
            string(3) "a11"
            ["parent_path"]=>
            string(6) "/a/a1/"
            ["child"]=>
            NULL
          }
          [1]=>
          array(4) {
            ["class_id"]=>
            string(1) "5"
            ["class_name"]=>
            string(3) "a12"
            ["parent_path"]=>
            string(6) "/a/a1/"
            ["child"]=>
            NULL
          }
        }
      }
    }
  }
  [1]=>
  array(4) {
    ["class_id"]=>
    string(1) "3"
    ["class_name"]=>
    string(1) "b"
    ["parent_path"]=>
    string(1) "/"
    ["child"]=>
    NULL
  }
}

大家倾向于哪种方案呢?我个人比较喜欢第二种方案因为分类结构在表里一眼就可以看出来。