mybatisplus树形分页查询
时间: 2025-06-09 14:06:36 浏览: 14
<think>我们正在使用MyBatis-Plus实现树形结构的分页查询。结合引用[4]中提到的项目实战,我们可以参考其思路:通过一次查询获取所有数据,然后在服务层将数据组装成树形结构。同时,我们需要支持分页功能。思路:1.首先进行分页查询(查询所有节点数据,不区分层级,因为后续在内存中构建树)2.在服务层将平铺的数据构建成树形结构3.分页处理:因为树形结构的分页通常是在根节点(顶级节点)上进行分页,所以我们需要先筛选出顶级节点,对顶级节点进行分页,然后为每个顶级节点挂载子节点。步骤:1.查询所有数据(不分页),然后构建树。但这样如果数据量大会有内存问题。因此我们改进:先分页查询顶级节点(parent_id=0或者为null),然后根据顶级节点去查询其所有子节点(包括多级),然后组装树。但是这样可能会有N+1查询问题。2.另一种方案(推荐):-第一步:分页查询顶级节点(Page对象,得到当前页的顶级节点列表)-第二步:根据这些顶级节点的id列表,递归查询出所有子节点(可以通过一个SQL查询出所有子节点,然后通过内存构建子树)-第三步:将每个顶级节点和其子树组合,最终返回一个顶级节点的列表(分页信息只针对顶级节点)。具体实现:假设我们有一个部门表(dept),字段包括id,name,parent_id(父部门id,顶级部门parent_id=0)。步骤1:使用MyBatis-Plus的分页查询,查询顶级部门(parent_id=0)并分页。步骤2:获取这些顶级部门的id列表,然后查询所有parent_id在这些顶级部门及其子部门id列表中的节点(即整个树的数据)。注意:这里我们可以一次性查询出整个树所需的所有节点(通过一次SQL查询),然后根据父子关系构建树。步骤3:将步骤2查询到的所有节点数据,在内存中构建成树(顶级节点就是步骤1分页得到的那一页节点,然后为每个顶级节点挂载其子节点树)。但是步骤2中如何一次性查询出所有子节点?我们可以使用递归查询,但数据库递归查询可能性能不好,并且不通用。另一种方法是在设计表时使用类似“路径枚举”或“闭包表”等方式存储层级关系,这样可以直接查询。但这里我们假设表结构是常见的parent_id方式。我们可以使用一个递归函数来获取某个节点的所有后代节点id,但是这样每个顶级节点都要递归查询一次,性能差。因此,我们可以考虑在应用层构建整个树结构(在内存中构建),但数据量大时可能性能不佳。折中方案:先分页查询顶级节点,然后查询所有非顶级的节点(即parent_id不为0的节点),再在内存中构建这些顶级节点及其子树。但是这样非顶级的节点可能非常多。优化:只查询属于当前页顶级节点的子树节点。即先得到当前页的顶级节点id列表,然后查询所有parent_id等于这些顶级节点id,或者parent_id在顶级节点id的子树中(这就需要我们有一个字段记录层级路径,如path字段,例如:顶级节点路径为/1/,其子节点为/1/2/,那么我们可以通过路径前缀匹配来查询)。这种设计需要数据库中有路径字段。我们假设表中有一个`path`字段,存储从根节点到当前节点的路径(以分隔符分隔)。例如:根节点id=1,path='/1/';其子节点id=2,path='/1/2/';id=3的父节点是2,则path='/1/2/3/'。那么,如果我们要查询顶级节点id=1的所有后代,可以通过:`wherepathlike'/1/%'`。有了这个前提,我们的步骤为:1.分页查询顶级节点(parent_id=0)的列表,得到Page<Dept>topPage。2.获取这些顶级节点的id列表:topIds3.查询所有属于这些顶级节点及其后代的节点:`select*fromdeptwhereparent_id=0andidin(topIds)ORpathlikeconcat('/','顶级节点id','/%')`。注意:这里我们使用OR条件,但需要为每个顶级节点生成like条件。可以改为:`wherepathlike'/1/%'orpathlike'/2/%'...`。但是这样如果顶级节点很多,条件会很长。改进:我们可以将多个like条件合并为一个正则表达式?但是这样可能索引失效。所以我们还是选择在内存中构建树。另一种思路(不使用path字段):1.查询所有数据(不分层级)2.在内存中构建完整的树结构(所有节点)3.筛选出顶级节点,并对顶级节点进行分页(注意:分页是在顶级节点上进行的)4.为分页后的每个顶级节点添加子树(子树已经通过完整数据构建好了)但是步骤1查询所有数据,如果数据量很大,内存可能不够。因此,如果数据量很大,建议在表设计时加入path字段,并建立索引,这样通过path的like查询(使用索引)可以快速获取一个节点的所有后代节点。下面我们使用带path字段的解决方案:实体类(Dept):```@DatapublicclassDept{privateLongid;privateStringname;privateLongparentId;privateStringpath;//路径,如顶级节点为"/1/",子节点为"/1/2/"}```Mapper接口(继承BaseMapper):```publicinterfaceDeptMapperextendsBaseMapper<Dept>{}```服务层代码:1.分页查询顶级节点(parent_id=0)2.得到顶级节点列表,收集这些顶级节点的id3.构建查询条件:查询path以顶级节点id开头的所有节点(即这些顶级节点及其后代)4.将查询到的所有节点构建成树形结构(此时顶级节点已经在步骤1中查询到,但是步骤3查询的结果包含了顶级节点和它们的后代)5.构建树:由于步骤1查询的顶级节点是分页的,我们只处理这一页的顶级节点。然后对于每个顶级节点,从步骤3查询的所有节点中提取出属于该顶级节点及其后代的节点,再构建树(可以通过父节点id递归构建)。但是步骤3中已经包含了所有需要的节点,我们可以直接对整个列表构建树(顶级节点parent_id=0),然后从中取出当前页的顶级节点(即步骤1查询出来的那一页的顶级节点)?不行,因为步骤3查出来的节点包括所有顶级节点(分页的顶级节点只是顶级节点中的一部分)和它们的后代,所以我们构建树时只针对这些节点,但顶级节点并不是全部顶级节点(而只是分页的这一页)。具体构建树方法:1.将从步骤3查询到的所有节点(包括顶级节点和后代)放到一个列表allNodes中。2.然后创建一个新列表,用于存放当前页的顶级节点(topPage中的记录)以及它们的子树。但是注意,我们分页的顶级节点已经查询出来了,现在要构建树,我们只要把这些顶级节点加上它们的子树即可。3.创建树:遍历allNodes,将每个节点按父节点id分组(构建一个Map<Long,List<Dept>>,key为父节点id)。然后从顶级节点开始(顶级节点的parent_id=0),递归地设置其children。注意:这里我们只需要当前页的顶级节点的树,所以顶级节点列表就是topPage记录(注意:这些顶级节点在allNodes中也存在)。我们遍历这个顶级节点列表,然后为每个顶级节点设置children(从分组Map中获取其子节点,然后递归设置子节点的children)。步骤:1.分页查询顶级节点:Page<Dept>topPage=deptMapper.selectPage(newPage<>(current,size),newQueryWrapper<Dept>().eq("parent_id",0));2.获取顶级节点的id列表:List<Long>topIds=topPage.getRecords().stream().map(Dept::getId).collect(Collectors.toList());3.如果topIds为空,则直接返回topPage(没有数据)4.否则,查询所有属于这些顶级节点及其后代的节点:select*fromdeptwhereparent_id=0andidin(topIds)OR(pathin(selectconcat(path,'%')fromdeptwhereidin(topIds)))这样写不对。更改为:使用条件构造器,构建多个like条件,或者使用OR条件连接多个like。示例:QueryWrapper<Dept>nodeWrapper=newQueryWrapper<>();nodeWrapper.eq("parent_id",0).in("id",topIds).or();for(LongtopId:topIds){nodeWrapper.or().likeRight("path","/"+topId+"/");//注意:使用or连接多个like}但是这样会产生很多个OR条件,如果topIds很大,SQL可能会很长。另一种写法(使用一条like条件,但需要将多个顶级节点path合并为一个正则)?不支持。我们可以写一个SQL函数来实现,但不通用。或者,在步骤1查询顶级节点时,同时查询出每个顶级节点的path。然后我们可以生成一个正则表达式,但是MySQL的like不支持正则?我们可以使用正则表达式匹配(使用REGEXP),但性能不如like。我们使用or连接多个like,并限定只查我们需要的子树:QueryWrapper<Dept>nodeWrapper=newQueryWrapper<>();nodeWrapper.or(wrapper->{wrapper.eq("parent_id",0).in("id",topIds);//顶级节点});nodeWrapper.or(wrapper->{for(Longid:topIds){wrapper.or().likeRight("path","/"+id+"/");//后代节点}});但是这样写,每个顶级节点都会产生一个OR条件,当topIds很大的时候,SQL语句会很长。所以,如果分页每页10条,这样写是可以接受的。如果每页1000条,就不合适了。为了避免这种情况,我们可以使用数据库的递归查询(CTE,公共表表达式),但MySQL8.0以上才支持,并且不同数据库语法不同,这里假设我们使用MySQL8.0。使用递归查询可以获取所有子节点的id,然后进行查询。但是这里我们不再深入。鉴于实际情况,我们假设分页的顶级节点数量不大(每页10到100个)。因此,我们采用多个OR条件。步骤4:将查询到的节点(包括顶级节点和后代)全部取出来。步骤5:构建树形结构(在内存中构建):-将步骤4查询到的节点按照父节点id分组:Map<Long,List<Dept>>childrenMap=allNodes.stream().collect(Collectors.groupingBy(Dept::getParentId));-但是注意,顶级节点的parent_id=0,所以我们可以直接为顶级节点设置children。-然后递归为每个节点设置children。但是这里有一个问题:步骤4查询出来的节点包括顶级节点(parent_id=0)和非顶级节点,分组后,顶级节点在childrenMap中对应的key为0。但是我们只需要当前页的顶级节点(它们的id在topIds中),我们只需要处理这些顶级节点。代码实现:List<Dept>topNodes=topPage.getRecords();//当前页的顶级节点for(Deptnode:topNodes){//设置节点的子节点setChildren(node,childrenMap);}其中setChildren方法是一个递归方法:privatevoidsetChildren(Deptnode,Map<Long,List<Dept>>childrenMap){Longid=node.getId();List<Dept>children=childrenMap.get(id);//取得该节点作为父节点的所有直接子节点if(children!=null){//为当前节点设置子节点node.setChildren(children);//递归设置每个子节点的子节点for(Deptchild:children){setChildren(child,childrenMap);}}}步骤6:现在topNodes中的每个顶级节点都已经构建好了完整的子树。然后我们将这个topNodes设置到topPage对象中(因为之前分页查询的顶级节点记录并没有children,现在构建好了)?注意:topPage的records就是topNodes,但是我们在构建树时修改了这些对象,所以不需要重新设置。步骤7:返回topPage,此时topPage中的records已经是树形结构了。注意:上面的childrenMap包含的键是所有在allNodes中出现过的parent_id,而allNodes中只包含我们需要的顶级节点及其后代。所以分组时只会包含这些节点的关系。完整服务层代码示例(关键部分):```publicPage<Dept>treePage(Page<Dept>pageQuery){//1.查询顶级节点(分页)QueryWrapper<Dept>topWrapper=newQueryWrapper<>();topWrapper.eq("parent_id",0);Page<Dept>topPage=baseMapper.selectPage(pageQuery,topWrapper);List<Dept>topNodes=topPage.getRecords();if(topNodes.isEmpty()){returntopPage;//没有数据,直接返回}List<Long>topIds=topNodes.stream().map(Dept::getId).collect(Collectors.toList());//2.查询所有相关节点(顶级节点及其后代)QueryWrapper<Dept>nodeWrapper=newQueryWrapper<>();//顶级节点(避免漏掉,但顶级节点已经查出来了,这里其实可以只查非顶级节点?但是后面构建树方便,全查出来)//构建OR条件:多个顶级节点的后代nodeWrapper.eq("parent_id",0).in("id",topIds)//顶级节点(我们已经有了,这里为了完整,也查出来。也可以不查,因为后面构建树会用到所有节点).or();//遍历每个顶级id,构造后代节点的查询条件(使用like匹配路径)for(Longid:topIds){nodeWrapper.or().likeRight("path","/"+id+"/");}List<Dept>allNodes=baseMapper.selectList(nodeWrapper);//3.构建树形结构所需的映射表:按父节点id分组Map<Long,List<Dept>>childrenMap=allNodes.stream().collect(Collectors.groupingBy(Dept::getParentId));//4.为顶级节点设置子节点(递归)//注意:我们的顶级节点在childrenMap中会出现在key=0的项中,但是我们不需要,因为我们要设置的是顶级节点的子节点,而顶级节点的父节点id为0,所以从childrenMap中取key=顶级节点id的子节点。//但是这里我们重新构建顶级节点树:我们构建的树中顶级节点的直接子节点应该在childrenMap中以顶级节点id为key的列表中。//但是注意:顶级节点在allNodes中也会出现,它们被分组到key=0的列表中?我们构建childrenMap时是以parentId分组的,顶级节点的parentId=0,所以会出现在0对应的列表中。但是我们构建树的时候,顶级节点已经作为根节点,我们只需要为它们设置子节点(即childrenMap中键为顶级节点id的列表)。//但是我们如何避免顶级节点被作为子节点?因为我们的allNodes中已经包含了顶级节点,且顶级节点的parent_id=0,所以childrenMap中0对应的列表就是所有顶级节点(包括不在当前页的顶级节点?)不,我们构建条件时,只查了当前页顶级节点及其后代。所以childrenMap中0对应的列表就是当前页的顶级节点(因为其他顶级节点不在allNodes中)。但是这样我们在构建树时,如果从顶级节点开始设置children,我们只需要为当前页的顶级节点设置。//所以,我们现在只需要为当前页的顶级节点设置子节点(注意:顶级节点本身已经在topNodes中,我们现在只需要设置它们的子节点)//递归设置for(Deptnode:topNodes){setChildren(node,childrenMap);}//5.返回分页对象,其记录已经是树形结构returntopPage;}privatevoidsetChildren(Deptnode,Map<Long,List<Dept>>childrenMap){Longid=node.getId();//从childrenMap中取出当前节点的子节点列表List<Dept>children=childrenMap.get(id);if(children!=null){//排序?如果有排序需求,可以在这里进行node.setChildren(children);//递归设置子节点的子节点for(Deptchild:children){setChildren(child,childrenMap);}}}```注意:上述代码中,我们为每个顶级节点设置子节点时,递归设置。这样构建的树形结构会嵌套多层。另外,childrenMap中可能包含其他节点的子节点(所有在allNodes中的节点),所以递归时会为每个节点设置其子节点。但是,返回的分页对象(topPage)的records(即顶级节点列表)已经具有完整的子树结构。存在的问题:1.分页是在顶级节点上进行的,但是分页信息(如总数、当前页等)都是顶级节点的信息,不包括子节点。2.当顶级节点很多时,构建OR条件查询后代节点会生成很长的SQL,所以分页页大小不宜设置太大。另外,如果我们的树层级很固定(例如只有固定几级),我们可以通过连接查询来构建树,这样可以避免递归查询。例如引用[4]中的做法:一次查询并映射为多层嵌套的结构。但是那种方式对每层都要求有固定的类型,并且需要自定义结果映射(MyBatis的resultMap实现嵌套),同时分页也是按顶级节点分页。如果层级不固定(无限级),那么上述方法(使用path字段)是一个可行的方案。如果不想使用path字段,可以改用递归查询获取每个顶级节点的后代节点id,然后合并id列表再进行查询。但是这样会有多次查询(递归查询每个顶级节点的子树id),可以通过存储过程或者数据库的递归查询(WITHRECURSIVE)实现。但为了简单和通用性,我们使用path字段。以上,我们实现了树形结构的分页查询(基于MyBatis-Plus)。实际应用中,还需要考虑分页查询的优化,比如对path字段建立索引等。引用:引用[4]中的项目实战使用的是多级嵌套的固定层级,而我们的方法支持无限级。§§相关问题§§1.如果树形结构数据量非常大(超过100万条),这种实现方式可能有什么问题?如何优化?2.如果不想在表中添加path字段,如何实现树形结构的分页查询?3.在构建树形结构时,如何避免循环引用(例如,节点A的父节点是B,节点B的父节点是A)?</think>
### MyBatis-Plus实现树形结构分页查询示例
下面基于引用[4]的多级树形结构拼接思路,结合MyBatis-Plus的分页功能,实现树形结构的分页查询:
#### 实体类设计(参考引用[4])
```java
@Data
public class TreeNode {
private Long id;
private String name;
private Long parentId;
private List<TreeNode> children;
@TableField(exist = false)
private Integer level; // 层级(非数据库字段)
}
```
#### Mapper接口(使用MyBatis-Plus)
```java
@Mapper
public interface TreeNodeMapper extends BaseMapper<TreeNode> {
// 查询所有节点(一次性加载)
@Select("SELECT id, name, parent_id FROM tree_node")
List<TreeNode> selectAllNodes();
}
```
#### 服务层实现(树形结构+分页)
```java
@Service
public class TreeService {
@Autowired
private TreeNodeMapper treeNodeMapper;
public Page<TreeNode> getTreePage(Page<TreeNode> page) {
// 1. 分页查询顶层节点(parent_id为0)
QueryWrapper<TreeNode> wrapper = new QueryWrapper<>();
wrapper.isNull("parent_id"); // 查询根节点
Page<TreeNode> rootPage = treeNodeMapper.selectPage(page, wrapper);
// 2. 获取所有非顶层节点数据
List<TreeNode> allNodes = treeNodeMapper.selectAllNodes();
// 3. 构建树形结构(参考引用[4])
rootPage.getRecords().forEach(root ->
buildTree(root, allNodes)
);
return rootPage;
}
private void buildTree(TreeNode parent, List<TreeNode> allNodes) {
List<TreeNode> children = allNodes.stream()
.filter(node -> parent.getId().equals(node.getParentId()))
.collect(Collectors.toList());
parent.setChildren(children);
children.forEach(child ->
buildTree(child, allNodes) // 递归构建子树
);
}
}
```
#### 控制器层
```java
@RestController
@RequestMapping("/tree")
public class TreeController {
@Autowired
private TreeService treeService;
@GetMapping("/page")
public Page<TreeNode> getTreePage(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size) {
return treeService.getTreePage(new Page<>(current, size));
}
}
```
#### 执行流程说明
1. **分页查询顶层节点**
利用MyBatis-Plus的`selectPage()`查询根节点(`parent_id IS NULL`)
2. **加载所有节点数据**
一次性查询全量数据(适合数据量不大的场景)
3. **构建树形结构**(参考引用[4])
```mermaid
graph TD
A[根节点分页查询] --> B[获取根节点列表]
B --> C[加载所有节点数据]
C --> D[递归构建子树]
D --> E[返回分页结果]
```
#### 性能优化建议(大数据量场景)
1. **按需加载子树**(参考引用[1][3])
```java
// 在mapper中添加方法
@Select("SELECT * FROM tree_node WHERE parent_id = #{parentId}")
List<TreeNode> selectChildren(@Param("parentId") Long parentId);
// 服务层改为懒加载
private void lazyLoadChildren(TreeNode node) {
List<TreeNode> children = treeNodeMapper.selectChildren(node.getId());
node.setChildren(children);
}
```
2. **层级字段优化**(添加`level`字段)
```sql
ALTER TABLE tree_node ADD COLUMN level TINYINT DEFAULT 0;
```
3. **路径枚举法**(参考引用[4])
添加`path`字段存储节点路径(如:/1/3/5/),可快速查询子树
#### 查询结果示例
```json
{
"records": [
{
"id": 1,
"name": "根节点",
"children": [
{
"id": 2,
"name": "子节点1",
"children": [...]
}
]
}
],
"total": 50,
"size": 10,
"current": 1,
"pages": 5
}
```
### 关键要点说明
1. **分页对象处理**:`Page.getRecords()`仅包含当前页的根节点
2. **子树数据加载**:通过一次性或懒加载方式获取子树
3. **递归构建**:基于父节点ID递归构建树形结构
4. **避免N+1查询**:通过全量加载或路径优化减少SQL查询次数
> 对于大数据量($10万+$)场景,建议添加`path`字段(存储节点路径),通过前缀匹配实现高效子树查询:
> ```sql
> SELECT * FROM tree_node WHERE path LIKE '1/%'
> ```
阅读全文
相关推荐












