MySQL遞歸查詢的幾種實(shí)現(xiàn)方法
背景
相信大家在平時(shí)開發(fā)的時(shí)候都遇見過以下這種樹形數(shù)據(jù)

這種樹形數(shù)據(jù)如何落庫應(yīng)該這里就不贅述了
核心就是使用額外一個(gè)字段parent_id保存父親節(jié)點(diǎn)的id,如下圖所示

這里的classpath指的是當(dāng)前節(jié)點(diǎn)的路徑,后續(xù)說明其作用
現(xiàn)有需求如下:
1、查詢指定id的分類節(jié)點(diǎn)的所有子節(jié)點(diǎn)2、查詢指定id的分類節(jié)點(diǎn)的所有父節(jié)點(diǎn)3、查詢整棵分類樹,可指定最大層級(jí)
常規(guī)操作
常規(guī)操作就是直接在程序?qū)用婵刂七f歸,下面根據(jù)需求一 一演示代碼。
PS:基礎(chǔ)工程代碼就不演示了,工程項(xiàng)目代碼在評(píng)論區(qū)鏈接中獲取
查詢指定id的分類節(jié)點(diǎn)的所有子節(jié)點(diǎn)
NormalController
/**
* 返回指定nodeId的節(jié)點(diǎn)信息,包括所有孩子節(jié)點(diǎn)
* @param nodeId
* @return
*/
@GetMapping("/childNodes/{nodeId}")
public CategoryVO childNodes(@PathVariable("nodeId") Integer nodeId){
return categoryService.normalChildNodes(nodeId);
}
CategoryServiceImpl
@Override
public CategoryVO normalChildNodes(Integer nodeId) {
// 查詢當(dāng)前節(jié)點(diǎn)信息
Category category = getById(nodeId);
return assembleChildren(category);
}
private CategoryVO assembleChildren(Category category) {
// 組裝vo信息
CategoryVO categoryVO = BeanUtil.copyProperties(category, CategoryVO.class);
// 如果沒有子節(jié)點(diǎn)了,則退出遞歸
List<Category> children = getChildren(category.getId());
if (children == null || children.isEmpty()) {
return categoryVO;
}
List<CategoryVO> childrenVOs = new ArrayList<>();
for (Category child : children) {
// 組裝每一個(gè)孩子節(jié)點(diǎn)
CategoryVO cv = assembleChildren(child);
// 將其加入到當(dāng)前層的孩子節(jié)點(diǎn)集合中
childrenVOs.add(cv);
}
categoryVO.setChildren(childrenVOs);
return categoryVO;
}
private List<Category> getChildren(int nodeId) {
// 如果不存在父親節(jié)點(diǎn)為nodeId的,則說明nodeId并不存在子節(jié)點(diǎn)
return lambdaQuery().eq(Category::getParentId,nodeId).list();
}
查詢id為6的分類信息

查詢指定id的分類節(jié)點(diǎn)的所有父節(jié)點(diǎn)
NormalController
/**
* 返回指定nodeId的節(jié)點(diǎn)父級(jí)集合,按照從下到上的順序
* @param nodeId
* @return
*/
@GetMapping("/parentNodes/{nodeId}")
public List<Category> parentNodes(@PathVariable("nodeId") Integer nodeId){
return categoryService.normalParentNodes(nodeId);
}
CategoryServiceImpl
@Override
public List<Category> normalParentNodes(Integer nodeId) {
Category category = getById(nodeId);
// 找到其所有的父親節(jié)點(diǎn)信息,即根據(jù)category的parentId一直查,直到查不到
List<Category> parentCategories = new ArrayList<>();
Category current = category;
while (true) {
Category parent = lambdaQuery().eq(Category::getId, current.getParentId()).one();
if (parent == null) {
break;
}
parentCategories.add(parent);
current = parent;
}
return parentCategories;
}
查詢id為12的父級(jí)分類信息

查詢整棵分類樹,可指定最大層級(jí)
NormalController
/**
* 返回整棵分類樹,可設(shè)置最大層級(jí)
* @param maxLevel
* @return
*/
@GetMapping("/treeCategory")
public List<CategoryVO> treeCategory(@RequestParam(value = "maxLevel",required = false) Integer maxLevel){
return categoryService.normalTreeCategory(maxLevel);
}
CategoryServiceImpl
@Override
public List<CategoryVO> normalTreeCategory(Integer maxLevel) {
// 虛擬根節(jié)點(diǎn)
CategoryVO root = new CategoryVO();
root.setId(-1);
root.setName("ROOT");
root.setClasspath("/");
// 隊(duì)列,為了控制層級(jí)的
Queue<CategoryVO> queue = new LinkedList<>();
queue.offer(root);
int level = 1;
while (!queue.isEmpty()) {
// 到達(dá)最大層級(jí)了
if (maxLevel != null && maxLevel == level) {
break;
}
int size = queue.size();
for (int i = 0; i < size; i++) {
CategoryVO poll = queue.poll();
if (poll == null) {
continue;
}
//得到當(dāng)前層級(jí)的所有孩子節(jié)點(diǎn)
List<Category> children = getChildren(poll.getId());
// 有孩子節(jié)點(diǎn)
if (children != null && !children.isEmpty()) {
List<CategoryVO> childrenVOs = new ArrayList<>();
// 構(gòu)建孩子節(jié)點(diǎn)
for (Category child : children) {
CategoryVO cv = BeanUtil.copyProperties(child, CategoryVO.class);
childrenVOs.add(cv);
queue.offer(cv);
}
// 設(shè)置孩子節(jié)點(diǎn)
poll.setChildren(childrenVOs);
}
}
// 層級(jí)自增
level++;
}
// 返回虛擬節(jié)點(diǎn)的孩子節(jié)點(diǎn)
return root.getChildren();
}
查詢整棵分類樹


MySQL8新特性
MySQL8有一個(gè)新特性就是with共用表表達(dá)式,使用這個(gè)特性就可以在MySQL層面實(shí)現(xiàn)遞歸查詢。
我們先來看看從上至下的遞歸查詢的SQL語句,查詢id為1的節(jié)點(diǎn)的所有子節(jié)點(diǎn)
WITH recursive r as ( -- 遞歸基:由此開始遞歸 select id,parent_id,name from category where id = 1 union ALL -- 遞歸步:關(guān)聯(lián)查詢 select c.id,c.parent_id,c.name from category c inner join r -- r作為父表,c作為子表,所以查詢條件是c的parent_id=r.id where r.id = c.parent_id ) select id,parent_id,name from r
查詢結(jié)果如下圖所示

舉一反三,則查詢id為12的所有父節(jié)點(diǎn)信息的就是從下至上的遞歸查詢,SQL如下所示
WITH recursive r as ( -- 遞歸基:從id為12的開始 select id,parent_id,name from category where id = 12 union ALL -- 遞歸步 select c.id,c.parent_id,c.name from category c inner join r -- 因?yàn)槭菑南轮辽系牟?,所以c作為子表,r作為父表 where r.parent_id = c.id ) select id,parent_id,name from r
結(jié)果如下圖所示

查詢指定id的分類節(jié)點(diǎn)的所有子節(jié)點(diǎn)
AdvancedController
/**
* 返回指定nodeId的節(jié)點(diǎn)信息,包括所有孩子節(jié)點(diǎn)
* @param nodeId
* @return
*/
@GetMapping("/childNodes/{nodeId}")
public CategoryVO childNodes(@PathVariable("nodeId") Integer nodeId){
return categoryService.advancedChildNodes(nodeId);
}
CategoryServiceImpl
@Override
public CategoryVO advancedChildNodes(Integer nodeId) {
List<Category> categories = categoryMapper.advancedChildNodes(nodeId);
List<CategoryVO> assemble = assemble(categories);
// 這里一定是第一個(gè),因?yàn)閏ategories集合中的是id為nodeId和其子分類的信息,結(jié)果assemble組裝后,只會(huì)存在一個(gè)根節(jié)點(diǎn)
return assemble.get(0);
}
// 組裝categories
private List<CategoryVO> assemble(List<Category> categories){
// 組裝categories
CategoryVO root = new CategoryVO();
root.setId(-1);
root.setChildren(new ArrayList<>());
Map<Integer,CategoryVO> categoryMap = new HashMap<>();
categoryMap.put(-1, root);
for (Category category : categories) {
CategoryVO categoryVO = BeanUtil.copyProperties(category, CategoryVO.class);
categoryVO.setChildren(new ArrayList<>());
categoryMap.put(category.getId(), categoryVO);
}
for (Category category : categories) {
// 得到自身節(jié)點(diǎn)
CategoryVO categoryVO = categoryMap.get(category.getId());
// 得到父親節(jié)點(diǎn)
CategoryVO parent = categoryMap.get(category.getParentId());
// 沒有父親節(jié)點(diǎn)(此情況只會(huì)在數(shù)據(jù)庫中最上層節(jié)點(diǎn)的父節(jié)點(diǎn)id不為-1的時(shí)候出現(xiàn))
if (parent == null) {
root.getChildren().add(categoryVO);
continue;
}
parent.getChildren().add(categoryVO);
}
return root.getChildren();
}
CategoryMapper
<select id="advancedChildNodes" resultType="com.example.mysql8recursive.entity.Category">
WITH recursive r as (select id, parent_id, name,classpath
from category
where id = #{nodeId}
union ALL
select c.id, c.parent_id, c.name,c.classpath
from category c
inner join r
where r.id = c.parent_id)
select id, parent_id, name, classpath
from r
</select>
查詢分類id為6的分類信息

拓展
這里其實(shí)還有另一種利用mybatis的collection子查詢的寫法,一筆帶過
<resultMap id="BaseResultMap" type="com.example.mysql8recursive.entity.Category">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="parentId" column="parent_id"/>
<result property="classpath" column="classpath"/>
</resultMap>
<resultMap id="CategoryVOResultMap" type="com.example.mysql8recursive.vo.CategoryVO" extends="BaseResultMap">
<collection property="children"
column="id"
ofType="com.example.mysql8recursive.vo.CategoryVO"
javaType="java.util.ArrayList"
select="advancedChildNodes"
>
</collection>
</resultMap>
<select id="advancedChildNodes" resultMap="CategoryVOResultMap">
select * from category where parent_id = #{id}
</select>
查詢指定id的分類節(jié)點(diǎn)的所有父節(jié)點(diǎn)
AdvancedController
/**
* 返回指定nodeId的節(jié)點(diǎn)父級(jí)集合,按照從下到上的順序
* @param nodeId
* @return
*/
@GetMapping("/parentNodes/{nodeId}")
public List<Category> parentNodes(@PathVariable("nodeId") Integer nodeId){
return categoryService.advancedParentNodes(nodeId);
}
CategorySericeImpl
@Override
public List<Category> advancedParentNodes(Integer nodeId) {
return categoryMapper.advancedParentNodes(nodeId);
}
CategoryMapper
<select id="advancedParentNodes" resultType="com.example.mysql8recursive.entity.Category">
WITH recursive r as (select id, parent_id, name, classpath
from category
where id = #{nodeId}
union ALL
select c.id, c.parent_id, c.name, c.classpath
from category c
inner join r
where r.parent_id = c.id)
select id, parent_id, name, classpath
from r
</select>
查詢分類id為12的所有父級(jí)分類信息

查詢整棵分類樹
AdvancedController
/**
* 返回整棵分類樹
* @return
*/
@GetMapping("/treeCategory")
public List<CategoryVO> treeCategory(){
return categoryService.advancedTreeCategory();
}
CategoryServiceImpl
@Override
public List<CategoryVO> advancedTreeCategory() {
return assemble(list());
}
查詢整棵分類樹


到此這篇關(guān)于MySQL遞歸查詢的幾種實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)MySQL遞歸查詢內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mysql中LEFT JOIN和JOIN查詢區(qū)別及原理詳解
這篇文章主要介紹了Mysql中LEFT JOIN和JOIN查詢區(qū)別及原理詳解,Nested Loop Join 實(shí)際上就是通過驅(qū)動(dòng)表的結(jié)果集作為循環(huán)基礎(chǔ)數(shù)據(jù),然后一條一條的通過該結(jié)果集中的數(shù)據(jù)作為過濾條件到下一個(gè)表中查詢數(shù)據(jù),然后合并結(jié)果,需要的朋友可以參考下2023-08-08
MySQL定時(shí)任務(wù)EVENT事件的使用方法
本文主要介紹了MySQL定時(shí)任務(wù)EVENT事件的使用方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
MySQL中Next-Key Lock底層原理實(shí)現(xiàn)
Next-KeyLock是MySQL InnoDB存儲(chǔ)引擎中的一種鎖機(jī)制,結(jié)合記錄鎖和間隙鎖,用于高效并發(fā)控制并避免幻讀,本文主要介紹了MySQL中Next-Key Lock底層原理實(shí)現(xiàn),感興趣的可以了解一下2025-03-03
淺談MySQL InnoDB實(shí)現(xiàn)MVCC原理
本文主要介紹了MySQL InnoDB實(shí)現(xiàn)MVCC原理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2026-03-03
php 不能連接數(shù)據(jù)庫 php error Can''t connect to local MySQL server
php 不能連接數(shù)據(jù)庫 php error Can't connect to local MySQL server through socket '/tmp/mysql.sock'2011-05-05
MySQL授權(quán)命令grant的使用方法小結(jié)
這篇文章主要介紹了MySQL授權(quán)命令grant的使用方法,本文實(shí)例,運(yùn)行于?MySQL?5.0?及以上版本,介紹了MySQL?賦予用戶權(quán)限命令的簡單格式,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-12-12
MYSQL根據(jù)分組獲取組內(nèi)多條數(shù)據(jù)中符合條件的一條(實(shí)例詳解)
這篇文章主要介紹了MYSQL根據(jù)分組獲取組內(nèi)多條數(shù)據(jù)中符合條件的一條,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06

