添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

最近在做团队的排期系统改版时涉及到数据库的递归查询问题,有一个需求数据表,表中的需求数据以parentId为外键定义数据的继承关系,需求之间的关系呈现树状关系。需求数据表如下:

mysql> desc needs;
+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int(11)     | NO   | PRI | NULL    | auto_increment |
| name     | varchar(45) | YES  |     | NULL    |                |
| parentId | int(11)     | YES  |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)
复制代码

目前有这样的需求需要根据某个根需求,查找出全部的层级的子需求。

例如A需求的树状结构如下:

数据如下:

mysql> select * from needs;
+----+------+----------+
| id | name | parentId |
+----+------+----------+
|  1 | A    |     NULL |
|  2 | B    |        1 |
|  3 | C    |        1 |
|  4 | D    |        2 |
|  5 | E    |        2 |
|  6 | F    |        3 |
|  7 | G    |        3 |
|  8 | H    |        5 |
|  9 | I    |        5 |
| 10 | J    |        8 |
+----+------+----------+
10 rows in set (0.00 sec)
复制代码

二、MySQL实现

1.自定义函数实现

实现思路:首先根据子级 parenId 等于父级 id 的关系循环找出所有的层级关系数据的id,再拉出所有这些id的数据。

(1)函数声明

DELIMITER // 
CREATE FUNCTION `getParentList`(rootId INT)
    RETURNS char(400)
    BEGIN
      DECLARE fid int default 1;
      DECLARE str char(44) default rootId;
      WHILE rootId > 0 DO
      SET fid=(SELECT parentId FROM needs WHERE id=rootId);
     IF fid > 0 THEN
     SET str=CONCAT(str , ',' , fid);
     SET rootId=fid;
     ELSE SET rootId=fid;
     END IF;
     END WHILE;
  return  str;
  END //
复制代码

语法解释:

DELIMITER :定义MySQL的分隔符为 // ,默认分隔符是 ; ,为了防止函数内使用 ; 中断函数

CREATE FUNCTION 函数名(参数) RETURNS 返回值类型 :自定义函数

DECLARE :声明变量

WHILE 条件 DO 循环体 :while循环

IF 条件 THEN 内容体 ELSE 内容体 :if判断

SET 变量=值 :存储值

CONCAT(str1,str2,...) :函数,用于将多个字符串连接成一个字符串

(2)函数调用

mysql> DELIMITER;
    -> SELECT getParentList(1);
+-----------------------+
| getParentList(1)       |
+-----------------------+
| ,1,2,3,4,5,6,7,8,9,10 |
+-----------------------+
1 row in set (0.01 sec)
复制代码

语法解释:

DELIMITER; :由于之前执行了 DELIMITER // 修改了分隔符,因此需要重新调用修改分隔符为 ;

SELECT 函数() :调用函数并搜索出结果

(3)结合FIND_IN_SET,拉取出所有的子需求

mysql> SELECT * FROM needs WHERE FIND_IN_SET(ID , getParentList(1));
+----+------+----------+
| id | name | parentId |
+----+------+----------+
|  1 | A    |     NULL |
|  2 | B    |        1 |
|  3 | C    |        1 |
|  4 | D    |        2 |
|  5 | E    |        2 |
|  6 | F    |        3 |
|  7 | G    |        3 |
|  8 | H    |        5 |
|  9 | I    |        5 |
| 10 | J    |        8 |
+----+------+----------+
10 rows in set (0.03 sec)
复制代码

FIND_IN_SET(str,strlist) :函数,查询字段 strlist 中包含 str 的结果, strlist 中以 , 分割各项

2.递归CTE实现

(1)递归CTE介绍

CTE(common table expression)为公共表表达式,可以对定义的表达式进行自引用查询。在MySQL 8.0版以上才支持。

递归CTE由三个部分组成:初始查询部分、递归查询部分、终止递归条件。

语法如下:

WITH RECURSIVE cte_name AS(
    initial_query       -- 初始查询部分
    UNION ALL           -- 递归查询与初始查询部分连接查询
    recursive_query     -- 递归查询部分
SELECT * FROM cte_name
复制代码

更多CTE介绍可以查看文档: A Definitive Guide To MySQL Recursive CTE

(2)递归CTE实现

WITH RECURSIVE needsTree AS
( SELECT id,
         name,
         parentId,
         1 lvl
    FROM needs
    WHERE id = 1 
  UNION ALL
  SELECT nd.id,
         nd.name,
         nd.parentId,
         lvl+1
    FROM needs AS nd
    JOIN needsTree AS nt ON nt.id = nd.parentId 
SELECT * FROM needsTree ORDER BY lvl;
复制代码

实现解释:

初始查询部分:找出一级需求

递归查询部分:找出子级需求

终止递归条件:子级的 parentId 等于父级的 id

查询结果:

+------+------+----------+------+
| id   | name | parentId | lvl  |
+------+------+----------+------+
|    1 | A    | NULL     |    1 |
|    2 | B    | 1        |    2 |
|    3 | C    | 1        |    2 |
|    6 | F    | 3        |    3 |
|    7 | G    | 3        |    3 |
|    4 | D    | 2        |    3 |
|    5 | E    | 2        |    3 |
|    8 | H    | 5        |    4 |
|    9 | I    | 5        |    4 |
|   10 | J    | 8        |    5 |
+------+------+----------+------+
10 rows in set (0.00 sec)
复制代码

三、Sequqlize实现

1.Sequelize介绍

Sequelize是Node.js的ORM框架,能够把关系数据库的表结构映射到对象上,支持数据库Postgres、MySQL、 MariaDB、 SQLite and Microsoft SQL Server。在这次的排期系统后台开发中,我选择了该框架来操作数据库,可以更方便地处理数据。

更多Sequelize介绍可以查看官方文档: Sequelize官方文档

2.递归实现

1.连接mysql数据库

var Sequelize = require('sequelize');
const sequelize = new Sequelize('schedule' , 'root' , '12345678' , {
    host : '127.0.0.1',
    dialect : 'mysql',
    port : '3306',
module.exports = {
    sequelize
复制代码

语法解释:

new Sequelize(databse , username , password , options) :实例化Sequelize,连接数据库

options = {
    host,   //数据库主机
    dialect,    //数据库
    port        //数据库端口号,默认为3306
复制代码

2.定义数据表的schema模型表

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('needs', {
    id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    name: {
      type: DataTypes.STRING(45),
      allowNull: false
    parentId: {
      type: DataTypes.INTEGER(11),
      allowNull: true,
    tableName: 'needs',
    timestamps: false
复制代码

语法解释:

sequelize.define(modelName , attribute , options) :定义数据表的模型,相当于定义数据表。

attribute :一个对象,为数据表对应的列项, key 值为对应的列项名, value 为对应列项的定义,比如数据类型、是否主键、是否必需等

options :数据表的一些配置。比如对应的数据表名 tableName 、是否需要时间戳 timestamp

3.导入数据表模型

const { sequelize } = require('../config/db');
// 导入数据表模型
const Needs = sequelize.import('./needs.js');
复制代码

语法解释:

sequelize.import(path) :导入数据表模型

4.递归查询

实现思路:跟CTE实现思路相似,先找出找出一级需求,再递归找出子需求。

class NeedModule{
    constructor(id){
        this.id = id;
    async getNeedsTree(){
        let rootNeeds = await Needs.findAll({
            where : { 
                id : this.id 
        rootNeeds = await this.getChildNeeds(rootNeeds);
        return rootNeeds;
    async getChildNeeds(rootNeeds){
        let expendPromise = [];
        rootNeeds.forEach(item => {
            expendPromise.push(Needs.findAll({
                where : {
                    parentId : item.id
        let child = await Promise.all(expendPromise);
        for(let [idx , item] of child.entries()){
            if(item.length > 0){
                item = await getChildNeeds(item);
            rootNeeds[idx].child = item;
        return rootNeeds;
复制代码

语法解释:

findALL(options) :查询多条数据

options :查询配置

  • options.where :查询条件
  • 查询结果如下:

    从搜索结果可以看出,使用Sequelize查询可以更好的给层级数据划分层级存储。

    3.nested属性实现

    Sequelize的 findAll 方法中的 nested 属性可以根据连接关系找出继承关系的数据。

    1.定义表关系

    由于需要需求表进行自连接查询,因此需要先定义表关系。需求表自身关系以父需求为主查询是一对多关系,因此使用 hasMany 定义关系。

    Needs.hasMany(
        Needs, 
            as: 'child', 
            foreignKey: 'parentId'
    复制代码

    语法解释:

    sourceModel.hasMany(targetModel, options) :定义源模型和目标模型的表是一对多关系,外键会添加到目标模型中

    options :定义表关系的一些属性。如 as 定义连接查询时,目标模型的别名。 foreignKey 为外键名。

    2.自连接查询

    async getNeedTree(id){
        return await Needs.findAll({
            where : {
            include : {
                model: Needs,
                as:'child', 
                required : false,
                include : {
                    all : true,
                    nested : true,
    复制代码

    语法解释:

    include :连接查询列表

    include.model :连接查询的模型

    include.as :连接查询模型的别名

    include.requeired :如果为 true ,连接查询为内连接。 false 为左连接。如果有 where 默认为 true ,其他情况默认为 false

    include.all :嵌套查询所有的模型

    include.nested :嵌套查询

    使用此方法,查询最深的子级结果为三层。如果能保证数据继承关系最深为三层,可以使用此方法。

    在MySQL 8+可以使用CTE实现,相对于自定义函数实现可以使用更少的代码量实现,且使用 WITH...AS 可以优化递归查询。Sequelize目前支持CTE,但仅支持PostgreSQL、SQLite、MSSQL数据库,如果有更好的实现方式,可以分享下哦(≧▽≦)

    1.mysql 递归查询 http://www.cnblog…

    2.Managing Hierarchical Data in MySQL Using the Adjacency List Model

    3.A Definitive Guide To MySQL Recursive CTE