一、序言
(一)背景内容
软件应用技术架构中 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 站,本文珍藏在专题博客。