草庐IT

Mybatis Plus一对多联表查询及分页解决方案

沙糖橘 2023-04-12 原文

文章目录

需求

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

需求分析

一个用户对应多个角色,用户信息和角色信息分表根据用户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方式效率高一些

有关Mybatis Plus一对多联表查询及分页解决方案的更多相关文章

  1. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

    我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.

  2. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  3. ruby-on-rails - 在 Rails 和 ActiveRecord 中查询时忽略某些字段 - 2

    我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr

  4. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  5. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  6. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

  7. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

  8. ruby-on-rails - solr 清理查询 - 2

    我在Rails上使用带有ruby​​的solr。一切正常,我只需要知道是否有任何现有代码来清理用户输入,比如以?开头的查询。或* 最佳答案 我不知道执行此操作的任何代码,但理论上可以通过查看parsingcodeinLucene来完成并搜索thrownewParseException(只有16个匹配!)。在实践中,我认为您最好只捕获代码中的任何solr异常并显示“无效查询”消息或类似信息。编辑:这里有几个“sanitizer”:http://pivotallabs.com/users/zach/blog/articles/937-s

  9. Ruby 守护进程和 JRuby - 备选方案 - 2

    我有一个应用程序正在从Ruby迁移到JRuby(由于需要通过Java提供更好的Web服务安全支持)。我使用的gem之一是daemons创建后台作业。问题在于它使用fork+exec来创建后台进程,但这对JRuby来说是禁忌。那么-是否有用于创建后台作业的替代gem/wrapper?我目前的想法是只从shell脚本调用rake并让rake任务永远运行......提前致谢,克里斯。更新我们目前正在使用几个与Java线程相关的包装器,即https://github.com/jmettraux/rufus-scheduler和https://github.com/philostler/acts

  10. ruby-on-rails - Rails 3 在一个查询中包含多个表 - 2

    我正在为锦标赛开发一个Rails应用程序。我在这个查询中使用了三个模型:classPlayertruehas_and_belongs_to_many:tournamentsclassTournament:destroyclassPlayerMatch"Player",:foreign_key=>"player_one"belongs_to:player_two,:class_name=>"Player",:foreign_key=>"player_two"在tournaments_controller的显示操作中,我调用以下查询:Tournament.where(:id=>params

随机推荐