一、序言
(一)背景內容
軟件應用技術架構中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能很好的解決單表查詢問題,同時借助在單表查詢的封裝能很好地解決連接查詢問題。
本方案不僅解決了連接查詢問題,同時具備如下內容拓展:
- 當數據量較大時,仍然具有穩定的查詢效率
當數據量達到百萬級別時,傳統的單表通過索引查詢已經面臨挑戰,普通的多表連接查詢性能隨着數據量的遞增呈現指數級下降。
本方案通過將連接查詢轉化為主鍵(索引)查詢,查詢性能等效於單表查詢。
- 與二級緩存配合使用進一步提高查詢效率
當所有的查詢均轉化為以單表為基礎的查詢后,方能安全的引入二級緩存。二級緩存的單表增刪改查操作自適應聯動,解決了二級緩存的臟數據問題。