数据库树状结构的设计和处理以及Spring Boot中MyBatis对树状结构的级联操作

1,084 阅读4分钟

在实际开发中,我们发现,其实很多数据结构是树状的,每个节点下面有不确定数量的节点,并且深度也不确定。这种数据结构应当如何进行处理呢?

其实我们的文件目录,就是这样的数据结构:

image.png

那么今天就以一个设计和查询一个简单的目录为例来进行讲解。

1,设计数据库表和类

我们首先还是设计数据库和类。

简单起见,我们这里的目录类只有id和名字这两个基本信息。我们先设计数据库表如下:

image.png

可见除了基本信息以外,我还加了parent_id字段表示某个目录的上一级目录,即为它的父目录。借助这个字段,我们是不是就可以查到一个目录之下所有的子目录了呢?

好的,我们继续设计类图,构建的目录类如下:

image.png

在Java的类里面,我们就没有设定parent_id这个字段了,而是换成了子目录的集合。这样我们下面使用MyBatis进行级联查询到一个目录时,就直接把子目录放在这个集合即可。

这里给出目录表sql文件和类代码:

Java类:

package com.example.treehandle.dataobject;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;
import java.util.List;

/**
 * 菜单类
 */
@Getter
@Setter
@NoArgsConstructor
public class Menu implements Serializable {

   /**
    * 主键id
    */
   private int id;

   /**
    * 目录名
    */
   private String name;

   /**
    * 该目录下的子目录
    */
   private List<Menu> childMenus;

}

sql文件:

-- 初始化表
drop table if exists `menu`;
create table `menu`
(
   `id`        int unsigned auto_increment,
   `name`      varchar(16) not null,
   `parent_id` int         not null, -- 这个目录的父目录id,如果这个目录是根目录那么这个字段值为0
   primary key (`id`)
) engine = InnoDB
  default charset = utf8mb4;

-- 初始化测试数据
insert into `menu` (`name`, `parent_id`)
values ('音乐', 0),
      ('视频', 0),
      ('图片', 0),
      ('中文歌', 1),   -- “中文歌”目录是“音乐”的子目录
      ('英文歌', 1),   -- “英文歌”目录是“音乐”的子目录
      ('动物', 3),    -- “动物”目录是“图片”的子目录
      ('二次元', 3),   -- “二次元”目录是“图片”的子目录
      ('动漫', 7),    -- “动漫”目录是“二次元”的子目录
      ('二次元游戏', 7), -- “二次元游戏”目录是“二次元”的子目录
      ('公主连结', 9),  -- “公主连结”目录是“二次元游戏”的子目录
      ('碧蓝航线', 9),  -- “碧蓝航线”目录是“二次元游戏”的子目录
      ('原神', 9); -- “原神”目录是“二次元游戏”的子目录

那么重要的问题来了:怎么进行树状结构的级联查询呢?我们往下看。

2,MyBatis级联查询树状结构

首先还是定义DAO层,这里只写两个查询方法:

package com.example.treehandle.dao;

import com.example.treehandle.dataobject.Menu;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface MenuDAO {

   /**
    * 根据id查找目录
    */
   Menu getById(int id);

   /**
    * 根据父目录id查找目录(查找某一目录下的所有子目录)
    */
   List<Menu> getByParentId(int parentId);

}

这里除了定义getById方法,我们还定义了getByParentId方法,这个就是用于帮助我们查到一个目录的子目录的方法。

然后就是编写MyBatis Mapper XML文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.treehandle.dao.MenuDAO">
   <resultMap id="menuResultMap" type="com.example.treehandle.dataobject.Menu">
      <id column="id" property="id"/>
      <result column="name" property="name"/>
      <!-- 查询以自身id为父节点的节点,实现往下递归查询 -->
      <collection property="childMenus" select="com.example.treehandle.dao.MenuDAO.getByParentId" column="id" fetchType="eager"/>
   </resultMap>

   <select id="getById" resultMap="menuResultMap">
      select *
      from `menu`
      where id = #{id}
   </select>

   <select id="getByParentId" resultMap="menuResultMap">
      select *
      from `menu`
      where parent_id = #{parentId}
   </select>
</mapper>

这里是重点,在上面resultMap节点中,我们定义了collection节点表示我们的childMenus属性是个集合,这个collection节点中各个属性如下:

  • property 表示这个节点数据对应目录类中的哪个属性
  • select 指定执行一个DAO查询方法并将其结果放在该节点
  • column 用于执行select属性中查询方法的参数
  • fetchType 指定是懒获取还是全部获取

那么可见,我们在执行getById查询方法时,遇到这个collection节点,就会以查到的自己的id字段值作为参数去执行getByParentId查询方法,即查询以自身id为父目录id的目录(说白了就是查询自己的所有子目录),这样就实现了往下递归查询,这样就能查询到一个目录以及其所有子目录的信息了。

这里,我们就完成了对树状结构的设计和查询了!

编写一个接口并查询一下试试,结果如下:

image.png

完整结果:

{
    "id": 3,
    "name": "图片",
    "childMenus": [
        {
            "id": 6,
            "name": "动物",
            "childMenus": []
        },
        {
            "id": 7,
            "name": "二次元",
            "childMenus": [
                {
                    "id": 8,
                    "name": "动漫",
                    "childMenus": []
                },
                {
                    "id": 9,
                    "name": "二次元游戏",
                    "childMenus": [
                        {
                            "id": 10,
                            "name": "公主连结",
                            "childMenus": []
                        },
                        {
                            "id": 11,
                            "name": "碧蓝航线",
                            "childMenus": []
                        },
                        {
                            "id": 12,
                            "name": "原神",
                            "childMenus": []
                        }
                    ]
                }
            ]
        }
    ]
}

可见,一个清晰明了的树状结构就查出来了。

示例仓库地址