最近在作團隊的排期系統改版時涉及到數據庫的遞歸查詢問題,有一個需求數據表,表中的需求數據以parentId爲外鍵定義數據的繼承關係,需求之間的關係呈現樹狀關係。需求數據表以下:html
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)
複製代碼
目前有這樣的需求須要根據某個根需求,查找出所有的層級的子需求。mysql
例如A需求的樹狀結構以下:sql
數據以下:數據庫
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)
複製代碼
實現思路:首先根據子級parenId
等於父級id
的關係循環找出全部的層級關係數據的id,再拉出全部這些id的數據。express
(1)函數聲明bash
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的分隔符爲//
,默認分隔符是;
,爲了防止函數內使用;
中斷函數async
CREATE FUNCTION 函數名(參數) RETURNS 返回值類型
:自定義函數ide
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
中以,
分割各項
(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)
複製代碼
Sequelize是Node.js的ORM框架,可以把關係數據庫的表結構映射到對象上,支持數據庫Postgres、MySQL、 MariaDB、 SQLite and Microsoft SQL Server。在此次的排期系統後臺開發中,我選擇了該框架來操做數據庫,能夠更方便地處理數據。
更多Sequelize介紹能夠查看官方文檔:Sequelize官方文檔。
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查詢能夠更好的給層級數據劃分層級存儲。
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 : {
id
},
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