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方式效率高一些

总结

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

您可能感兴趣的文章:

相关文章

  • SpringBoot使用@Cacheable出现预览工具乱码的解决方法

    SpringBoot使用@Cacheable出现预览工具乱码的解决方法

    直接使用注解进行缓存数据,我们再使用工具去预览存储的数据时发现是乱码,这是由于默认序列化的问题,所以接下来将给大家介绍一下SpringBoot使用@Cacheable出现预览工具乱码的解决方法,需要的朋友可以参考下
    2023-10-10
  • Java 的 Condition 接口与等待通知机制详解

    Java 的 Condition 接口与等待通知机制详解

    在 Java 并发编程里,实现线程间的协作与同步是极为关键的任务,本文将深入探究Condition接口及其背后的等待通知机制,感兴趣的朋友一起看看吧
    2025-05-05
  • java中volatile关键字解释说明

    java中volatile关键字解释说明

    volatile是Java中的一个关键字,用于修饰变量,保证其可见性和禁止指令重排序,但不保证原子性,下面我们从原理层面详细解析volatile 的工作机制,感兴趣的朋友跟随小编一起看看吧
    2026-01-01
  • Mybatis分页插件Pagehelper的PageInfo字段属性使用及解释

    Mybatis分页插件Pagehelper的PageInfo字段属性使用及解释

    这篇文章主要介绍了Mybatis分页插件Pagehelper的PageInfo字段属性使用及解释,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • ZIP4j 压缩与解压的实例详解

    ZIP4j 压缩与解压的实例详解

    这篇文章主要介绍了ZIP4j 压缩与解压的实例详解的相关资料,希望通过本文能帮助到大家,需要的朋友可以参考下
    2017-10-10
  • Java读取Map的两种方法与对比

    Java读取Map的两种方法与对比

    相信大家都知道在Java中Map的使用非常频繁,我们经常会需要对Map进行遍历和读取,那么下面这篇文章将展示两种遍历的方法以及简要分析。有需要的可以参考借鉴,下面来一起看看吧。
    2016-11-11
  • 一文详解如何从零构建Spring Boot Starter并实现整合

    一文详解如何从零构建Spring Boot Starter并实现整合

    Spring Boot是一个开源的Java基础框架,用于创建独立、生产级的基于Spring框架的应用程序,这篇文章主要介绍了如何从零构建Spring Boot Starter并实现整合的相关资料,需要的朋友可以参考下
    2025-03-03
  • java 将字符串、list 写入到文件,并读取内容的案例

    java 将字符串、list 写入到文件,并读取内容的案例

    这篇文章主要介绍了java 将字符串、list 写入到文件,并读取内容的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • Java读取数据库表的示例代码

    Java读取数据库表的示例代码

    这篇文章主要介绍了Java读取数据库表,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • SpringBoot引入SPEL模板字符串替换的两种方式

    SpringBoot引入SPEL模板字符串替换的两种方式

    在 Spring Boot 中,我们可以使用字符串替换工具类来实现这些功能,本文主要介绍了SpringBoot引入SPEL模板字符串替换的两种方式,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03

最新评论