MybatisPlus一对多联表查询及分页的解决过程

 更新时间:2025年12月24日 15:35:28   作者:沙糖橘  
文章主要介绍了在使用MyBatis-Plus进行一对多查询时遇到的分页问题及解决方案,在一对多场景下,直接映射会导致分页数据数量不正确,而使用子查询映射则可以解决这个问题,此外,通过在Java代码中使用Stream进行关联处理,可以进一步优化查询性能,避免多次子查询带来的效率问题

需求

查询用户信息列表,其中包含用户对应角色信息,页面检索条件有根据角色名称查询用户列表;

需求分析

一个用户对应多个角色,用户信息和角色信息分表根据用户id关联存储,用户和角色一对多进行表连接查询,

创建对应表:

CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  `age` int DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4  COMMENT='用户信息表';

CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(30) NOT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4  COMMENT='角色信息表';


CREATE TABLE `sys_user_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4  COMMENT='用户和角色关联表';


INSERT INTO tsq.sys_user (name,age) VALUES
	 ('张三',18),
	 ('王二',19);

INSERT INTO tsq.sys_role (role_name) VALUES
	 ('角色1'),
	 ('角色2'),
	 ('角色3'),
	 ('角色4');

INSERT INTO tsq.sys_user_role (user_id,role_id) VALUES
	 (1,1),
	 (1,2),
	 (1,3),
	 (2,4);


对应实体类:

@Data
@ApiModel("用户信息表")
@TableName("sys_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    
    @ApiModelProperty("用户id")
    private Long id;
    @ApiModelProperty("姓名")
    private String name;
    @ApiModelProperty("年龄")
    private Integer age;
}


@Data
@ApiModel("角色信息表")
@TableName("sys_role")
public class Role implements Serializable {

    private static final long serialVersionUID = 1L;
    
    @ApiModelProperty("角色id")
    private Long id;
    @ApiModelProperty("角色名称")
    private String roleName;
}



@Data
@ApiModel("用户信息表")
public class UserVo implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty("用户id")
    private Long id;
    @ApiModelProperty("姓名")
    private String name;
    @ApiModelProperty("年龄")
    private Integer age;
    
    private List<Role> roleList;

}


分页问题说明

在使用一对多连接查询并且分页时,发现返回的分页列表数据数量不对

比如这里查询用户对应角色列表,如果使用直接映射,那么 roleList 的每个 Role 对象都会算一条数据;比如查第一页,一个用户有三个角色每页三条数据,就会出现查出一个 User ,三个 Role 的这些情况,这它也算每页三条(其实就只查到一个用户)

分页问题原因

mybatis-plus一对多分页时,应该使用子查询的映射方式,使用直接映射就会出错

所以直接映射适用于一对一,子查询映射使用于一对多;

一对多场景一

查询用户表的内容,角色表不参与条件查询,用懒加载形式

// controller
  @GetMapping("/pageList")
	public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int offset,
	                                    @RequestParam(required = false, defaultValue = "10") int pagesize) {
		return userService.pageList(offset, pagesize);
	}
  
  
// serviceimpl
  @Override
	public Map<String, Object> pageList(int offset, int pagesize) {
      List<UserVo> pageList = userMapper.pageList(offset, pagesize);
      int totalCount = userMapper.pageListCount();
      Map<String, Object> result = new HashMap<String, Object>();
      result.put("pageList", pageList);
      result.put("totalCount", totalCount);

      return result;
	}
  
// mapper.xml

   <resultMap id="getUserInfo" type="com.tsq.democase.onetomany.domain.vo.UserVo" >
        <result column="id" property="id" />
        <result column="name" property="name" />
        <result column="age" property="age" />
        <collection property="roleList" javaType="ArrayList" ofType="com.tsq.democase.onetomany.domain.Role"
                    select="getRolesByUserId" column="{userId = id}"/>
    </resultMap>

    <select id="getRolesByUserId" resultType="com.tsq.democase.onetomany.domain.Role">
        SELECT *
        FROM sys_user_role ur
                 inner join sys_role r on ur.role_id = r.id
        where ur.user_id = #{userId}
    </select>
    
    <select id="pageList"  resultMap="getUserInfo">
        SELECT *
        FROM sys_user
        LIMIT #{offset}, #{pageSize}
    </select>
    
    <select id="pageListCount" resultType="java.lang.Integer">
        SELECT count(1)
        FROM sys_user
    </select>

查询结果

一对多场景二

查询用户表的内容,角色表要作为查询条件参与查询,例如要根据角色名称查询出用户列表

// controller
	@GetMapping("/pageListByRoleName")
	public Map<String, Object> pageListByRoleName(@RequestParam(required = false, defaultValue = "0") int offset,
	                                    @RequestParam(required = false, defaultValue = "10") int pagesize,
	                                    @RequestParam String roleName) {
		return userService.pageListByRoleName(offset, pagesize, roleName);
	}
  
  
// serviceimpl
	@Override
	public Map<String, Object> pageListByRoleName(int offset, int pagesize,String roleName) {
      List<UserVo> pageList = userMapper.pageListByRoleName(offset, pagesize, roleName);
      int totalCount = userMapper.pageListCount();
      Map<String, Object> result = new HashMap<String, Object>();
      result.put("pageList", pageList);
      result.put("totalCount", totalCount);

      return result;
	}
  
// mapper.xml
   <resultMap id="getUserInfoByRoleName" type="com.tsq.democase.onetomany.domain.vo.UserVo" >
        <result column="id" property="id" />
        <result column="name" property="name" />
        <result column="age" property="age" />
        <collection property="roleList" javaType="ArrayList" ofType="com.tsq.democase.onetomany.domain.Role"
                    select="getRolesByUserIdAndRoleName" column="{userId = id,roleName = roleName}"/>
    </resultMap>

    <select id="getRolesByUserIdAndRoleName" resultType="com.tsq.democase.onetomany.domain.Role">
        SELECT *
        FROM sys_user_role ur
        inner join sys_role r on ur.role_id = r.id
        where ur.user_id = #{userId}
        <if test="roleName != null and roleName != ''" >
            and r.role_name LIKE concat('%', #{roleName}, '%')
        </if>
    </select>

    <select id="pageListByRoleName"  resultMap="getUserInfoByRoleName">
        SELECT temp.* FROM (
        SELECT distinct u.*,#{roleName} as roleName
        FROM sys_user u
        left join sys_user_role ur on u.id = ur.user_id
        left join sys_role r on r.id = ur.role_id
        <where>
            <if test="roleName != null and roleName != ''" >
                r.role_name LIKE concat('%', #{roleName}, '%')
            </if>
        </where>
        ) temp
        LIMIT #{offset}, #{pageSize}
    </select>

查询结果

性能优化

原因:

场景一二中使用 select方式会触发多次子查询(SELECT *FROM sys_user_role ur inner join sys_role …),当数据量大时会使查询速度很慢。

场景二中查询时产生的sql日志如下:

--  ==>  
SELECT
    temp.* 
FROM
    ( SELECT
        distinct u.*,
        '角色' as roleName 
    FROM
        sys_user u 
    left join
        sys_user_role ur 
            on u.id = ur.user_id 
    left join
        sys_role r 
            on r.id = ur.role_id 
    WHERE
        r.role_name LIKE concat('%', '角色', '%') ) temp LIMIT 0,
    10 
 --  ====>  
SELECT
    * 
FROM
    sys_user_role ur 
inner join
    sys_role r 
        on ur.role_id = r.id 
where
    ur.user_id = 1 
    and r.role_name LIKE concat('%', '角色', '%') 
 --  ====>  
SELECT
    * 
FROM
    sys_user_role ur 
inner join
    sys_role r 
        on ur.role_id = r.id 
where
    ur.user_id = 2 
    and r.role_name LIKE concat('%', '角色', '%') 
 --  ==>  
SELECT
    count(1) 
FROM
    sys_user 

sql可见如果有100各用户就要执行一百次子查询,效率极低。

优化解决方案

sql中只查询sys_user相关信息并且做roleName 过滤,roleList在java代码中用stream关联role并赋值roleList;

// serviceimpl
  @Override
  public Map<String, Object> pageListByRoleName(int offset, int pagesize,String roleName) {
     // List<UserVo> pageList = userMapper.pageListByRoleName(offset, pagesize, roleName);
     List<UserVo> pageList = userMapper.pageListByRoleName2(offset, pagesize, roleName);
     List<Long> userIds = pageList.stream().map(UserVo::getId).collect(Collectors.toList());
     List<UserRoleVo> userRoleVos =  userMapper.getUserRoleByUserIds(userIds);
     Map<Long, List<UserRoleVo>> userRoleMap = userRoleVos.stream().collect(Collectors.groupingBy(UserRoleVo::getUserId, Collectors.toList()));
     pageList.forEach(u -> {
        List<UserRoleVo> roleVos = userRoleMap.get(u.getId());
        List<RoleVo> roles = BeanUtils.listCopy(roleVos, CopyOptions.create(), RoleVo.class);
        u.setRoleList(roles);
     });
     int totalCount = userMapper.pageListCount();
     Map<String, Object> result = new HashMap<String, Object>();
     result.put("pageList", pageList);
     result.put("totalCount", totalCount);

     return result;
  }

// mapper.xml
<select id="pageListByRoleName2"  resultType="com.tsq.democase.onetomany.domain.vo.UserVo">
    SELECT distinct u.*
    FROM sys_user u
    left join sys_user_role ur on u.id = ur.user_id
    left join sys_role r on r.id = ur.role_id
    <where>
        <if test="roleName != null and roleName != ''" >
            r.role_name LIKE concat('%', #{roleName}, '%')
        </if>
    </where>
    LIMIT #{offset}, #{pageSize}
</select>  

查询结果

同场景二。

查询时产生的sql如下:

  --  ==>  
  SELECT
      distinct u.* 
  FROM
      sys_user u 
  left join
      sys_user_role ur 
          on u.id = ur.user_id 
  left join
      sys_role r 
          on r.id = ur.role_id 
  WHERE
      r.role_name LIKE concat('%', '角色', '%') LIMIT 0, 10 
   --  ==>  
  SELECT
      ur.user_id ,
      r.id roleId,
      r.role_name 
  FROM
      sys_user_role ur 
  inner join
      sys_role r 
          on ur.role_id = r.id 
   --  ==>  
  SELECT
      count(1) 
  FROM
      sys_user 
 

由sql日志可见这种方式比纯sql方式效率高一些

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:

相关文章

  • java跳出for循环的三种常见方法

    java跳出for循环的三种常见方法

    这篇文章主要给大家介绍了关于java跳出for循环的三种常见方法,需要的朋友可以参考下
    2023-07-07
  • Java线程间通讯的几种方法小结

    Java线程间通讯的几种方法小结

    线程通信可以用于控制并发线程的数量,本文主要介绍了Java线程间通讯的几种方法小结,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2024-01-01
  • SpringBoot + SpringSecurity 环境搭建的步骤

    SpringBoot + SpringSecurity 环境搭建的步骤

    这篇文章主要介绍了SpringBoot + SpringSecurity 环境搭建的步骤,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-05-05
  • Java中的常用时间日期类总结(Date、DateFormat)

    Java中的常用时间日期类总结(Date、DateFormat)

    在Java开发中处理时间和日期是相当常见的任务,无论是计算日期差异、格式化日期显示、解析日期字符串还是进行日期计算,都需要一些时间和日期处理的技巧,这篇文章主要给大家介绍了关于Java中常用时间日期类(Date、DateFormat)的相关资料,需要的朋友可以参考下
    2024-08-08
  • Netty实现简易版的RPC框架过程详解

    Netty实现简易版的RPC框架过程详解

    这篇文章主要为大家介绍了Netty实现简易版的RPC框架过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • MyBatis框架简介

    MyBatis框架简介

    本文主要介绍了MyBatis框架的基础知识。具有很好的参考价值。下面跟着小编一起来看下吧
    2017-03-03
  • Kotlin教程之函数和包的定义和流程控制

    Kotlin教程之函数和包的定义和流程控制

    这篇文章主要介绍了Kotlin教程之函数和包的定义和流程控制的相关资料,需要的朋友可以参考下
    2017-05-05
  • Hibernate+JDBC实现批量插入、更新及删除的方法详解

    Hibernate+JDBC实现批量插入、更新及删除的方法详解

    这篇文章主要介绍了Hibernate+JDBC实现批量插入、更新及删除的方法,结合实例形式较为详细的分析了Hibernate与JDBC针对数据库的批量操作相关实现技巧,需要的朋友可以参考下
    2017-11-11
  • Java Thread之Sleep()使用方法总结

    Java Thread之Sleep()使用方法总结

    这篇文章主要介绍了Java Thread之Sleep()使用方法总结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • Java异常--常见方法--自定义异常--增强try(try-with-resources)详解

    Java异常--常见方法--自定义异常--增强try(try-with-resources)详解

    这篇文章主要介绍了Java异常--常见方法--自定义异常--增强try(try-with-resources)的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03

最新评论