一、序言

(一)背景内容

软件应用技术架构中DAO层最常见的选型组件为MyBatis,熟悉MyBatis的朋友都清楚,曾几何时MyBatis是多么的风光,使用XML文件解决了复杂的数据库访问的难题。时至今日,曾经的屠龙者终成恶龙,以XML文件为基础的数据库访问技术变得臃肿、复杂,维护难度直线上升。

MybatisPlus对常见的数据库访问进行了封装,访问数据库大大减少了XML文件的依赖,开发者从臃肿的XML文件中获得了较大限度的解脱。

MybatisPlus官方并没有提供多表连接查询的通用解决方案,然而连接查询是相当普遍的需求。解决连接查询有两种需求,一种是继续使用MyBatis提供XML文件解决方式;另一种本文提供的解决方案。

事实上笔者强烈推荐彻底告别通过XML访问数据库,并不断探索新式更加友好、更加自然的解决方式,现分享最新的MybatisPlus技术的研究成果。

(二)场景说明

为了说明连接查询的关系,这里以学生、课程及其关系为示例。

(三)前期准备

此部分需要读者掌握以下内容:Lambda 表达式、特别是方法引用;函数式接口;流式运算等等,否则理解起来会有些吃力。

实体类与 Vo 的映射关系,作者创造性的引入特别构造器,合理利用继承关系,极大的方便了开发者完成实体类向 Vo 的转换。

空指针异常忽略不处理,借助Optional类实现,详情移步Java8 新特性查看。

二、一对一查询

一对一查询最典型的应用场景是将id替换成name,比如将userId替换成userName

(一)查询单条记录

查询单条记录是指返回值仅有一条记录,通常是以唯一索引作为条件的返回查询结果。

1、示例代码
/**
* 查询单个学生信息(一个学生对应一个部门)
*/
public UserVo getOneUser(Integer userId) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getUserId, userId);
// 先查询用户信息
User user = userMapper.selectOne(wrapper);
// 转化为Vo
UserVo userVo = Optional.ofNullable(user).map(UserVo::new).orElse(null);
// 从其它表查询信息再封装到Vo
Optional.ofNullable(userVo).ifPresent(this::addDetpNameInfo);
return userVo;
}

附属表信息补充

/**
* 补充部门名称信息
*/
private void addDetpNameInfo(UserVo userVo) {
LambdaQueryWrapper<Dept> wrapper = Wrappers.lambdaQuery(Dept.class)
.eq(Dept::getDeptId, userVo.getDeptId());
Dept dept = deptMapper.selectOne(wrapper);
Optional.ofNullable(dept).ifPresent(e -> userVo.setDeptName(e.getDeptName()));
}
2、理论分析

查询单个实体共分为两个步骤:根据条件查询主表数据(需处理空指针异常);封装 Vo 并查询附属表数据。

查询结果(VO)只有一条记录,需要查询两次数据库,时间复杂度为O(1)

(二)查询多条记录

查询多条记录是指查询结果为列表,通常是指以普通索引为条件的查询结果。

1、示例代码
/**
* 批量查询学生信息(一个学生对应一个部门)
*/
public List<UserVo> getUserByList() {
// 先查询用户信息(表现形式为列表)
List<User> user = userMapper.selectList(Wrappers.emptyWrapper());
List<UserVo> userVos = user.stream().map(UserVo::new).collect(toList());
// 此步骤可以有多个
addDeptNameInfo(userVos);
return userVos;
}

附属信息补充

private void addDeptNameInfo(List<UserVo> userVos) {
// 提取用户userId,方便批量查询
Set<Integer> deptIds = userVos.stream().map(User::getDeptId).collect(toSet());
// 根据deptId查询deptName(查询前,先做非空判断)
List<Dept> dept = deptMapper.selectList(Wrappers.lambdaQuery(Dept.class).in(Dept::getDeptId, deptIds));
// 构造映射关系,方便匹配deptId与deptName
Map<Integer, String> hashMap = dept.stream().collect(toMap(Dept::getDeptId, Dept::getDeptName));
// 封装Vo,并添加到集合中(关键内容)
userVos.forEach(e -> e.setDeptName(hashMap.get(e.getDeptId())));
}
2、理论分析

先查询包含id的列表记录,从结果集中析出id并转化成批查询语句再访问数据库,从第二次调用结果集中解析出name

查询结果(VO)有多条记录,但仅调用两次数据库,时间复杂度为O(1)

(三)查询多条记录(分页)

分页查询实体的思路与查询列表的思路相似,额外多处一步分页泛型转换。

1、示例代码
/**
* 分页查询学生信息(一个学生对应一个部门)
*/
public IPage<UserVo> getUserByPage(Page<User> page) {
// 先查询用户信息
IPage<User> xUserPage = userMapper.selectPage(page, Wrappers.emptyWrapper());
// 初始化Vo
IPage<UserVo> userVoPage = xUserPage.convert(UserVo::new);
if (userVoPage.getRecords().size() > 0) {
addDeptNameInfo(userVoPage);
}
return userVoPage;
}

查询补充信息

private void addDeptNameInfo(IPage<UserVo> userVoPage) {
// 提取用户userId,方便批量查询
Set<Integer> deptIds = userVoPage.getRecords().stream().map(User::getDeptId).collect(toSet());
// 根据deptId查询deptName
List<Dept> dept = deptMapper.selectList(Wrappers.lambdaQuery(Dept.class).in(Dept::getDeptId, deptIds));
// 构造映射关系,方便匹配deptId与deptName
Map<Integer, String> hashMap = dept.stream().collect(toMap(Dept::getDeptId, Dept::getDeptName));
// 将查询补充的信息添加到Vo中
userVoPage.convert(e -> e.setDeptName(hashMap.get(e.getDeptId())));
}

IPage接口中convert方法,能够实现在原实例上修改。

2、理论分析

先查询包含id的列表记录,从结果集中析出id并转化成批查询语句再访问数据库,从第二次调用结果集中解析出name

查询结果(VO)有多条记录,但仅调用两次数据库,时间复杂度为O(1)

三、一对多查询

一对多查询最常见的场景是查询部门所包含的学生信息,由于一个部门对应多个学生,每个学生对应一个部门,因此称为一对多查询。

(一)查询单条记录

1、示例代码
/**
* 查询单个部门(其中一个部门有多个用户)
*/
public DeptVo getOneDept(Integer deptId) {
// 查询部门基础信息
LambdaQueryWrapper<Dept> wrapper = Wrappers.lambdaQuery(Dept.class).eq(Dept::getDeptId, deptId);
DeptVo deptVo = Optional.ofNullable(deptMapper.selectOne(wrapper)).map(DeptVo::new).orElse(null);
Optional.ofNullable(deptVo).ifPresent(this::addUserInfo);
return deptVo;
}

补充附加信息

private void addUserInfo(DeptVo deptVo) {
// 根据部门deptId查询学生列表
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class).eq(User::getDeptId, deptVo.getDeptId());
List<User> users = userMapper.selectList(wrapper);
deptVo.setUsers(users);
}
2、理论分析

整个过程共分为两个阶段:通过部门表中主键查询指定部门信息,通过学生表中部门ID外键查询学生信息,将结果合并,形成返回值(Vo)。

一对多查询单条记录整个过程至多需要调用2次数据库查询,查询次数为常数,查询时间复杂度为O(1)

(二)查询多条记录

1、示例代码
/**
* 查询多个部门(其中一个部门有多个用户)
*/
public List<DeptVo> getDeptByList() {
// 按条件查询部门信息
List<Dept> deptList = deptMapper.selectList(Wrappers.emptyWrapper());
List<DeptVo> deptVos = deptList.stream().map(DeptVo::new).collect(toList());
if (deptVos.size() > 0) {
addUserInfo(deptVos);
}
return deptVos;
}

补充附加信息

private void addUserInfo(List<DeptVo> deptVos) {
// 准备deptId方便批量查询用户信息
Set<Integer> deptIds = deptVos.stream().map(Dept::getDeptId).collect(toSet());
// 用批量deptId查询用户信息
List<User> users = userMapper.selectList(Wrappers.lambdaQuery(User.class).in(User::getDeptId, deptIds));
// 重点:将用户按照deptId分组
Map<Integer, List<User>> hashMap = users.stream().collect(groupingBy(User::getDeptId));
// 合并结果,构造Vo,添加集合列表
deptVos.forEach(e -> e.setUsers(hashMap.get(e.getDeptId())));
}
2、理论分析

整个过程共分为三个阶段:通过普通索引从部门表中查询若干条记录;将部门ID转化为批查询从学生表中查询学生记录;将学生记录以部门ID为单位进行分组,合并结果,转化为Vo。

一对多查询多条记录需要调用2次数据库查询,查询次数为常数,查询时间复杂度为O(1)

(三)查询多条记录(分页)

1、示例代码
/**
* 分页查询部门信息(其中一个部门有多个用户)
*/
public IPage<DeptVo> getDeptByPage(Page<Dept> page) {
// 按条件查询部门信息
IPage<Dept> xDeptPage = deptMapper.selectPage(page, Wrappers.emptyWrapper());
IPage<DeptVo> deptVoPage = xDeptPage.convert(DeptVo::new);
if (deptVoPage.getRecords().size() > 0) {
addUserInfo(deptVoPage);
}
return deptVoPage;
}

查询补充信息

private void addUserInfo(IPage<DeptVo> deptVoPage) {
// 准备deptId方便批量查询用户信息
Set<Integer> deptIds = deptVoPage.getRecords().stream().map(Dept::getDeptId).collect(toSet());
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class).in(User::getDeptId, deptIds);
// 用批量deptId查询用户信息
List<User> users = userMapper.selectList(wrapper);
// 重点:将用户按照deptId分组
Map<Integer, List<User>> hashMap = users.stream().collect(groupingBy(User::getDeptId));
// 合并结果,构造Vo,添加集合列表
deptVoPage.convert(e -> e.setUsers(hashMap.get(e.getDeptId())));
}
2、理论分析

整个过程共分为三个阶段:通过普通索引从部门表中查询若干条记录;将部门ID转化为批查询从学生表中查询学生记录;将学生记录以部门ID为单位进行分组,合并结果,转化为Vo。

一对多查询多条记录需要调用2次数据库查询,查询次数为常数,查询时间复杂度为O(1)

四、多对多查询

MybatisPlus 实现多对多查询是一件极富挑战性的任务,也是连接查询中最困难的部分。

以空间置换时间,借助于流式运算,解决多对多查询难题。

多对多查询相对于一对多查询,增加了流式分组运算、批量 HashMap 取值等内容。

(一)查询单条记录

查询单条记录一般是指通过两个查询条件查询出一条匹配表中的记录。

1、示例代码
public StudentVo getStudent(Integer stuId) {
// 通过主键查询学生信息
StudentVo studentVo = ConvertUtils.convertObj(getById(stuId), StudentVo::new);
LambdaQueryWrapper<StuSubRelation> wrapper = Wrappers.lambdaQuery(StuSubRelation.class).eq(StuSubRelation::getStuId, stuId);
// 查询匹配关系
List<StuSubRelation> stuSubRelations = stuSubRelationMapper.selectList(wrapper);
Set<Integer> subIds = stuSubRelations.stream().map(StuSubRelation::getSubId).collect(toSet());
if (studentVo != null && subIds.size() > 0) {
List<Subject> subList = subjectMapper.selectList(Wrappers.lambdaQuery(Subject.class).in(Subject::getId, subIds));
List<SubjectBo> subBoList = ConvertUtils.convertList(subList, SubjectBo::new);
HashBasedTable<Integer, Integer, Integer> table = getHashBasedTable(stuSubRelations);
subBoList.forEach(e -> e.setScore(table.get(stuId, e.getId())));
studentVo.setSubList(subBoList);
}
return studentVo;
}
2、理论分析

多对多单条记录查询最多访问数据库3次,先查询学生信息,然后查询学生与课程匹配信息,最后查询课程分数信息,查询时间复杂度为O(1)

(二)查询多条记录

1、示例代码
public List<StudentVo> getStudentList() {
// 通过主键查询学生信息
List<StudentVo> studentVoList = ConvertUtils.convertList(list(), StudentVo::new);
// 批量查询学生ID
Set<Integer> stuIds = studentVoList.stream().map(Student::getId).collect(toSet());
LambdaQueryWrapper<StuSubRelation> wrapper = Wrappers.lambdaQuery(StuSubRelation.class).in(StuSubRelation::getStuId, stuIds);
List<StuSubRelation> stuSubRelations = stuSubRelationMapper.selectList(wrapper);
// 批量查询课程ID
Set<Integer> subIds = stuSubRelations.stream().map(StuSubRelation::getSubId).collect(toSet());
if (stuIds.size() > 0 && subIds.size() > 0) {
HashBasedTable<Integer, Integer, Integer> table = getHashBasedTable(stuSubRelations);
List<Subject> subList = subjectMapper.selectList(Wrappers.lambdaQuery(Subject.class).in(Subject::getId, subIds));
List<SubjectBo> subjectBoList = ConvertUtils.convertList(subList, SubjectBo::new);
Map<Integer, List<Integer>> map = stuSubRelations.stream().collect(groupingBy(StuSubRelation::getStuId, mapping(StuSubRelation::getSubId, toList())));
for (StudentVo studentVo : studentVoList) {
// 获取课程列表
List<SubjectBo> list = ListUtils.select(subjectBoList, e -> emptyIfNull(map.get(studentVo.getId())).contains(e.getId()));
// 填充分数
list.forEach(e -> e.setScore(table.get(studentVo.getId(), e.getId())));
studentVo.setSubList(list);
}
}
return studentVoList;
}
2、理论分析

多对多N条记录查询由于使用了批查询,因此最多访问数据库也是3次,先查询学生信息,然后查询学生与课程匹配信息,最后查询课程分数信息,查询时间复杂度为O(1)

(三)查询多条记录(分页)

1、示例代码
public IPage<StudentVo> getStudentPage(IPage<Student> page) {
// 通过主键查询学生信息
IPage<StudentVo> studentVoPage = ConvertUtils.convertPage(page(page), StudentVo::new);
// 批量查询学生ID
Set<Integer> stuIds = studentVoPage.getRecords().stream().map(Student::getId).collect(toSet());
LambdaQueryWrapper<StuSubRelation> wrapper = Wrappers.lambdaQuery(StuSubRelation.class).in(StuSubRelation::getStuId, stuIds);
// 通过学生ID查询课程分数
List<StuSubRelation> stuSubRelations = stuSubRelationMapper.selectList(wrapper);
// 批量查询课程ID
Set<Integer> subIds = stuSubRelations.stream().map(StuSubRelation::getSubId).collect(toSet());
if (stuIds.size() > 0 && subIds.size() > 0) {
HashBasedTable<Integer, Integer, Integer> table = getHashBasedTable(stuSubRelations);
// 学生ID查询课程ID组
Map<Integer, List<Integer>> map = stuSubRelations.stream().collect(groupingBy(StuSubRelation::getStuId, mapping(StuSubRelation::getSubId, toList()))); List<Subject> subList = subjectMapper.selectList(Wrappers.lambdaQuery(Subject.class).in(Subject::getId, subIds));
List<SubjectBo> subBoList = ConvertUtils.convertList(subList, SubjectBo::new);
for (StudentVo studentVo : studentVoPage.getRecords()) {
List<SubjectBo> list = ListUtils.select(subBoList, e -> emptyIfNull(map.get(studentVo.getId())).contains(e.getId()));
list.forEach(e -> e.setScore(table.get(studentVo.getId(), e.getId())));
studentVo.setSubList(list);
}
}
return studentVoPage;
}
2、理论分析

多对多N条记录分页查询由于使用了批查询,因此最多访问数据库也是3次,先查询学生信息,然后查询学生与课程匹配信息,最后查询课程分数信息,查询时间复杂度为O(1)

五、总结与拓展

(一)总结

通过上述分析,能够用 MybatisPlus 解决多表连接查询中的一对一一对多多对多查询。

  • 上述代码行文紧凑,充分利用 IDE 对 Lambda 表达式的支持,在编译期间完成对代码的检查。
  • 业务逻辑清晰,可维护性、可修改性优势明显。
  • 一次查询需要访问至多两次数据库,时间复杂度为o(1),主键查询或者索引查询,查询效率高。

(二)拓展

MybatisPlus能很好的解决单表查询问题,同时借助在单表查询的封装能很好地解决连接查询问题。

本方案不仅解决了连接查询问题,同时具备如下内容拓展:

  • 当数据量较大时,仍然具有稳定的查询效率

当数据量达到百万级别时,传统的单表通过索引查询已经面临挑战,普通的多表连接查询性能随着数据量的递增呈现指数级下降。

本方案通过将连接查询转化为主键(索引)查询,查询性能等效于单表查询。

  • 与二级缓存配合使用进一步提高查询效率

当所有的查询均转化为以单表为基础的查询后,方能安全的引入二级缓存。二级缓存的单表增删改查操作自适应联动,解决了二级缓存的脏数据问题。

原文地址

MybatisPlus多表连接查询的更多相关文章

  1. SQL多表连接查询(详细实例)

    转载博客:joeleo博客(http://www.xker.com/page/e2012/0708/117368.html) 本文主要列举两张和三张表来讲述多表连接查询. 新建两张表: 表1:stud ...

  2. SQL多表连接查询

    SQL多表连接查询 本文主要列举两张和三张表来讲述多表连接查询. 新建两张表: 表1:student  截图如下: 表2:course  截图如下: (此时这样建表只是为了演示连接SQL语句,当然实际 ...

  3. oracle(sql)基础篇系列(二)——多表连接查询、子查询、视图

        多表连接查询 内连接(inner join) 目的:将多张表中能通过链接谓词或者链接运算符连接起来的数据查询出来. 等值连接(join...on(...=...)) --选出雇员的名字和雇员所 ...

  4. Access数据库多表连接查询

    第一次在Access中写多表查询,就按照MS数据库中的写法,结果报语法错,原来Access的多表连接查询是不一样的 表A.B.C,A关联B,B关联C,均用ID键关联 一般写法:select * fro ...

  5. PostgreSQL-join多表连接查询和子查询

    一.多表连接查询 1.连接方式概览 [inner] join 内连接:表A和表B以元组为单位做一个笛卡尔积,记为表C,然后在C中挑选出满足符合on 语句后边的限制条件的内容. left [outer] ...

  6. SQL表连接查询(inner join、full join、left join、right join)

    SQL表连接查询(inner join.full join.left join.right join) 前提条件:假设有两个表,一个是学生表,一个是学生成绩表. 表的数据有: 一.内连接-inner ...

  7. SQL的多表连接查询

    SQL的多表连接查询 多表连接查询具有两种规范,SQL92和SQL99规范. SQL92规范支持下列多表连接查询: (1)等值连接: (2)非等值连接: (3)外连接: (4)广义笛卡尔积: SQL9 ...

  8. hibernate 实现多表连接查询(转载)

    http://www.cnblogs.com/lihuiyy/archive/2013/03/28/2987531.html 为了方便,直接粘过来,方便查看.不收藏了 Hibernate主要支持两种查 ...

  9. django ORM model filter 条件过滤,及多表连接查询、反向查询,某字段的distinct

    版权归作者所有,任何形式转载请联系作者.作者:petanne(来自豆瓣)来源:https://www.douban.com/note/301166150/ 1.多表连接查询:感觉django太NX了. ...

随机推荐

  1. VAE with a VampPrior

    目录 概 主要内容 分级的VAE 代码 Tomczak J. & Welling M. VAE with a VampPrior. In International Conference on ...

  2. idea解决springboot项目中log4j漏洞升级问题

    最近阿里云团队发现log4j漏洞,危险级别:严重,相关资讯 https://m.sohu.com/coo/hsdt/506958086_355140 https://www.sohu.com/a/50 ...

  3. 编写Java程序,创建一个 XML 文档,文档名为“hero.xml”,用于保存“王者荣耀”的英雄信息。

    查看本章节 查看作业目录 需求说明: 创建一个 XML 文档,文档名为"hero.xml",用于保存"王者荣耀"的英雄信息.英雄信息包括编号(id).姓名(na ...

  4. C#WPF数据绑定模板化操作四步走

    前言:WPF数据绑定对于WPF应用程序来说尤为重要,本文将讲述使用MVVM模式进行数据绑定的四步走用法: 具体实例代码如下: 以下代码仅供参考,如有问题请在评论区留言,谢谢 1 第一步:声明一个类用来 ...

  5. Linux查找class类所在jar包

    1.说明 写代码或者定位问题的时候, 经常发生只知道类名不知道其所在jar包的问题, 在Eclipse中可以使用Ctrl+Shift+T查找类, 但是如果类所在的jar包不在Build Path中, ...

  6. Ranger-Sqoop2插件安装

    Ranger-Sqoop2插件安装,基于Ranger版本1.0.0,支持Sqoop2版本1.99.7. 1.获取安装包 scp root@10.43.159.11:/home/compile/rang ...

  7. nginx rewrite 基础

    一.跳转到首页  如果请求的页面不存在的话就跳转到首页 location / {      if (!-e $request_filename){        rewrite ^/(.*) /ind ...

  8. centos6.5-nginx搭建

    一.安装nginx 1.安装相关组件 yum -y install pcre-devel zlib-devel 2.创建启动用户 useradd -M -s /sbin/nologin nginx t ...

  9. nginx worker_cpu_affinity使用方法

    Nginx默认没有开启利用多核CPU,我们可以通过增加worker_cpu_affinity配置参数来充分利用多核CPU.CPU是任务处理,计算最关键的资源,CPU核越多,性能就越好. 配置Nginx ...

  10. OpenIddict 登录及详细流程解析

    GitHub上实例都是集成了Identity来实现,我这里去掉了相关东西,实现自定义的登录满足自己的结构要求 服务端配置添加数据库服务以及定时任务服务 builder.Services.AddDbCo ...