一、序言

(一)背景内容

软件应用技术架构中DAO层最常见的选型组件为MyBatis,相熟MyBatis的敌人都分明,曾几何时MyBatis是如许的景色,应用XML文件解决了简单的数据库拜访的难题。时至今日,已经的屠龙者终成恶龙,以XML文件为根底的数据库拜访技术变得臃肿、简单,保护难度直线回升。

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

MybatisPlus官网并没有提供多表连贯查问的通用解决方案,然而连贯查问是相当广泛的需要。解决连贯查问有两种需要,一种是持续应用MyBatis提供XML文件解决形式;另一种本文提供的解决方案。

事实上笔者强烈推荐彻底辞别通过XML拜访数据库,并一直摸索旧式更加敌对、更加天然的解决形式,现分享最新的MybatisPlus技术的研究成果。

<img src="https://www.altitude.xin/typora/image-20211021114957682.png" alt="image-20211021114957682" style="zoom:50%;" />

(二)场景阐明

为了阐明连贯查问的关系,这里以学生、课程及其关系为示例。

<img src="https://www.altitude.xin/typora/image-20211020194255298.png" alt="image-20211020194255298" style="zoom:50%;" />

(三)后期筹备

此局部须要读者把握以下内容:Lambda 表达式、特地是办法援用;函数式接口;流式运算等等,否则了解起来会有些吃力。

<img src="https://www.altitude.xin/typora/image-20211021135113431.png" alt="image-20211021135113431" style="zoom:50%;" />

实体类与 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 取值等内容。

<img src="https://www.altitude.xin/typora/image-20211024115903848.png" alt="image-20211024115903848" style="zoom:50%;" />

(一)查问单条记录

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

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 = EntityUtils.toList(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 = EntityUtils.toList(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 = EntityUtils.toList(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 = EntityUtils.toPage(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 = EntityUtils.toList(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)

须要应用的依赖,用于解决实体类与VO等转换。

<dependency>    <groupId>xin.altitude.cms.common</groupId>    <artifactId>ucode-cms-common</artifactId>    <version>1.3.3</version></dependency>

五、总结与拓展

(一)总结

通过上述剖析,可能用 MybatisPlus 解决多表连贯查问中的一对一一对多多对多查问。

  • 上述代码行文紧凑,充分利用 IDE 对 Lambda 表达式的反对,在编译期间实现对代码的查看。
  • 业务逻辑清晰,可维护性、可修改性劣势显著。
  • 一次查问须要拜访至少两次数据库,工夫复杂度为o(1),主键查问或者索引查问,查问效率高。

(二)拓展

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

本计划不仅解决了连贯查问问题,同时具备如下内容拓展:

  • 当数据量较大时,依然具备稳固的查问效率

当数据量达到百万级别时,传统的单表通过索引查问曾经面临挑战,一般的多表连贯查问性能随着数据量的递增出现指数级降落。

本计划通过将连贯查问转化为主键(索引)查问,查问性能等效于单表查问。

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

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


喜爱本文点个♥️赞♥️反对一下,关注我下期再见,相干源码在GitHub,视频解说在B站,本文珍藏在专题博客。