# mybatis-plus-join
**Repository Path**: CreateSequence/mybatis-plus-join
## Basic Information
- **Project Name**: mybatis-plus-join
- **Description**: 基于myabtis-plus的连表查询扩展,基于自定义的条件构造器支持多级join、字段别名、数据库内置函数、对 lambda 表达式支持更友好的group查询以及预设条件参数
- **Primary Language**: Unknown
- **License**: Apache-2.0
- **Default Branch**: release
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 6
- **Forks**: 0
- **Created**: 2022-07-15
- **Last Updated**: 2024-02-04
## Categories & Tags
**Categories**: Uncategorized
**Tags**: MyBatis, mybatis-plus, mybatis-plus-join
## README
## 一、简介
本项目基于 mybatis-plus,提供通过条件构造器以代码方式构造 join 查询的相关功能。
开发的初衷是为了解决mp日常使用中感觉到的一些痛点的,比如条件构造器不支持join语法,lambda表达式版本的group...having支持不够、查询字段与条件字段都不支持数据库函数,不支持逻辑表,像in或eq这类的方法需要重复添加判空条件......等等。
本框架旨保留mp原功能的基础上,基于`Wrapper`类扩展一个新的`JoinWrapper`以在不修改已有代码的基础上支持上述功能。

## 二、快速开始
1. 引入 mybatis-plus-boot-starter 与 mybatis-plus-join 依赖:
~~~xml
top.xiajibagao
mybatis-plus-join
${version}
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
~~~
> 本项目需要自行引入依赖 `mybatis-plus-boot-starter`,此外其他依赖皆不向下传递
2. 将动态返回值插件`DynamicResultInterceptor`注册到 `com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean` 中 ,
2. 然后将扩展 SQL 注入器 `JoinMethodInjector`注入到 `com.baomidou.mybatisplus.core.config.GlobalConfig`中。
这里给出一个最简单配置:
~~~java
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(dataSource);
// 注册动态返回值插件
sqlSessionFactory.setPlugins(new DynamicResultInterceptor());
// 注册扩展sql注入器
MybatisConfiguration configuration = new MybatisConfiguration();
GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
globalConfig.setSqlInjector(new JoinMethodInjector());
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory.getObject();
}
~~~
3. 令 `mapper`接口从继承 mp 提供的 `BaseMapper`换为 `JoinMapper`:
~~~java
@Mapper
public interface StudentMapper extend JoinMapper {
// ... ...
}
~~~
5. 使用 `JoinWrapper` 构建查询条件,并使用 `JoinMapper` 提供的方法进行查询
~~~java
JoinWrapper wrapper = JoinWrapper.create(Student.class, StudentVO.class)
.selectAll() // 查询 Foo 的全部字段
.leftJoin(Score.class) // 关联查询 Score
.on(StudentDO::getId, Condition.EQ, Score::getStudentId)
.selectAll(); // 查询 Score 的全部字段
List StudentVOS = StudentMapper.selectListJoin(wrapper);
~~~
## 三、核心功能
分别创建学生表 student,课程表 course 与考试分数表 score 三张表,其对应数据库脚本如下:
~~~sql
-- 课程表
CREATE TABLE `course` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
)
-- 考试分数表
CREATE TABLE `score` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`student_id` int(0) NULL DEFAULT NULL,
`course_id` int(0) NULL DEFAULT NULL,
`score` int(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
)
-- 学生表
CREATE TABLE `student` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
)
~~~
以下示例皆基于上述三表及对应实体。
### 1、字段别名
条件构造器可以指定主表与可以指定对象类型,需要指定返回值对象类型,并指定别名:
```java
// 查询学生id,学生名称并指定别名,其余与默认字段相同
JoinWrapper wrapper = JoinWrapper.create(StudentDO.class, StudentDTO.class)
.select(StudentDO::getName, StudentDTO::getStudentName)
.select(StudentDO::getId, StudentDTO::getStudentId)
.selectAll();
List studentDTOS = studentMapper.selectListJoin(wrapper);
```
该条件构造器构造的 SQL 等同:
~~~sql
SELECT t1.*, t1.name AS student_name, t1.id AS student_id FROM student t1
~~~
### 2、扩展条件
`JoinWrapper`基于 mp 的条件构造器原有方法额外提供三个方向的扩展:
- 基于 Lambda 表达式的应用条件;
- 预设的应用条件:包括 `in/notInIfNotEmpty`,`eqIfNotNull`,`likeIfNotBank`,`between/notBetweenIfAllNotNull`等;
- 补充方法:包括 `notLikeRight`,`notLikeLeft`,`limit`等;
如:
~~~java
JoinWrapper wrapper = JoinWrapper.create(StudentDO.class, StudentDTO.class)
// 基于lambda表达式的应用条件
.eq(Objects::nonNull, StudentDO::getName, null)
.in(t -> !t.isEmpty(), StudentDO::getId, Arrays.asList(1, 2, 3))
// 预设应用条件
.eqIfNotNull(StudentDO::getName, null)
.inIfNotEmpty(StuedntDO::getId, Collections.emptyList())
// 补充方法
.notLikeRight(CharSequenceUtil::isNotBlank, StudentDO::getName, "小明")
.limit(true, 1);
List studentDTOS = studentMapper.selectListJoin(wrapper);
~~~
最终构造的 SQL同:
~~~sql
SELECT t1.* FROM student t1 WHERE (t1.name NOT LIKE '小明%' and t1.id in (1, 2, 3)) LIMIT 1
~~~
### 3、连表查询
`JoinWrapper`支持构造关联查询:
~~~java
// 查询学生成绩
JoinWrapper wrapper = JoinWrapper.create(StudentDO.class, StudentDTO.class)
.selectAll()
.leftJoin(ScoreDO.class, w -> w
.on(StudentDO::getId, Condition.EQ, ScoreDO::getStudentId)
.select(ScoreDO::getScore, StudentDTO::getScore)
.leftJoin(CourseDO.class)
.on(ScoreDO::getCourseId, Condition.EQ, CourseDO::getId)
.select(CourseDO::getName, StudentDTO::getCourseName)
);
// 该写法等同于
JoinWrapper wrapper = JoinWrapper.create(StudentDO.class, StudentDTO.class)
.selectAll()
.leftJoin(ScoreDO.class, w -> w
.on(StudentDO::getId, Condition.EQ, ScoreDO::getStudentId)
.select(ScoreDO::getScore, StudentDTO::getScore)
.leftJoin(CourseDO.class, w2 -> w2
.on(ScoreDO::getCourseId, Condition.EQ, CourseDO::getId)
.select(CourseDO::getName, StudentDTO::getCourseName)
)
);
~~~
该条件构造器构造的 SQL 等同:
~~~sql
SELECT t1.*, t2.score AS score, t3.name AS course_name
FROM student t1
LEFT JOIN score t2 ON (t1.id = t2.student_id)
LEFT JOIN course t3 ON (t2.course_id = t3.id)
~~~
支持的关联查询包括 `fulljoin`、`left join`、`right join`、`inner join` 四种,关联的每张表都可以添加复数的普通条件、关联条件(on)以及查询字段,比如:
~~~java
JoinWrapper wrapper = JoinWrapper.create(StudentDO.class, StudentDTO.class)
.selectAll()
.eqIfNotNull(StudentDO::getName, "小明")
.leftJoin(ScoreDO.class, w -> w
.on(StudentDO::getId, Condition.EQ, ScoreDO::getStudentId)
.select(ScoreDO::getScore, StudentDTO::getScore)
.le(ScoreDO::getScore, 60)
.leftJoin(CourseDO.class, w2 -> w2
.on(ScoreDO::getCourseId, Condition.EQ, CourseDO::getId)
.select(CourseDO::getName, StudentDTO::getCourseName)
.likeIfNotBank(CourseDO::getType, "文科")
)
);
~~~
该条件构造器构造的 SQL 如下:
~~~sql
SELECT t1.*, t2.score AS score, t3.name AS course_name
FROM student t1
LEFT JOIN score t2
ON (t1.id = t2.student_id)
LEFT JOIN course t3
ON (t2.course_id = t3.id)
WHERE (t1.name = '小明' AND t2.score <= 60 AND t3.type LIKE '%文科%');
~~~
### 4、数据库函数字段
`JoinWrapper`支持将数据库函数作为字段,可以有三种用法:
- 作为查询字段,如:`select ifNull(a.name, 'fack name')`;
- 作为查询条件,包括 where 与 having 条件;
- 用于函数嵌套,如 `concat('user: ', ifNull(a.name, 'fack name'))`;
由于在关联查询时必须指定表字段来源表的别名,因此创建表字段需要通过 `JoinWrapper.toTableColumn()`将字段与表进行绑定,然后可通过函数字段工厂类`top.xiajibagao.mybatis.plus.join.wrapper.column.Columns`对获取的字段进行函数化。
支持的函数:
- 日期类:now, currentTimestamp, currentDate, currentTime, dateFormat, day, month, year;
- 数学:abs, avg, max, min, sum, rand, count;
- 字符串:ifNull, concat, format, replace, upper, lower;
- 控制流:case..then...when...else;
> **注意**:部分函数可能不受某些数据库支持,请根据自己项目使用的数据库选择性使用
#### Select
如:
```java
// 查询分数,并根据分段给出评价
JoinWrapper wrapper = JoinWrapper.create(ScoreDO.class, StudentDTO.class);
wrapper.select(ScoreDO::getScore) // 查询 score 字段
.select(ScoreDO::getCourseId, StudentDTO::getCourseId) // 查询 courseId 字段, 并指定别名为 courseId
.selectAll() // 查询 ScoreDO 的全部字段, 效果同 select *
.selectAllColumns() // 查询 ScoreDO 的全部字段, 效果同 select 对象中全部被 @TableField 注解的可查询字段
.selectAllColumns(ScoreDO::getName) // 查询 ScoreDO 的全部字段, 但是并排除 name 字段
.caseByCondition(StudentDTO::getRemark)
.when(wrapper.toTableColumn(ScoreDO::getScore), Condition.GE, 90, "'优'")
.when(wrapper.toTableColumn(ScoreDO::getScore), Condition.GE, 60, "'及格'")
.el(() -> "'不及格'")
.end();
```
该条件构造器构造的 SQL 同:
~~~sql
SELECT t1.score, (
CASE t1.score
WHEN t1.score >= 90 THEN '优'
WHEN t1.score >= 60 THEN '及格'
ELSE '不及格' END
) AS remark
FROM score t1
~~~
#### Where
像函数这类特殊的字段需要依靠`where()`方法构建:
~~~java
JoinWrapper wrapper = JoinWrapper.create(ScoreDO.class, StudentDTO.class);
wrapper.selectAll()
.where(Columns.plus(wrapper.toTableColumn(ScoreDO::getScore), 5), Condition.EQ, 100);
~~~
构建的 SQL 同:
~~~sql
SELECT t1.* FROM score t1 WHERE ((t1.score + 5) = 100)
~~~
#### Having
Having 关键字需要配合 group by 使用:
~~~java
// 查询挂了不止1人的科目的挂科人数
JoinWrapper wrapper = JoinWrapper.create(ScoreDO.class, StudentDTO.class);
wrapper.select(ScoreDO::getCourseId, StudentDTO::getCourseId)
.select(Columns.count(), StudentDTO::getNum)
.where(ScoreDO::getScore, Condition.LT, 60)
.groupBy(ScoreDO::getCourseId)
.having(Columns.count(), Condition.GT, 1);
~~~
构建的 SQL 同:
~~~sql
SELECT t1.course_id AS course_id, COUNT(*) AS num
FROM score t1
WHERE (t1.score < 60)
GROUP BY t1.course_id
HAVING COUNT(*) > 1
~~~
### 5、子查询
JoinWrapper 允许将一个已经构造好的条件构造器转为一张逻辑表/临时表,并用于子查询。
#### JOIN
~~~java
// 查询挂科超过1人的科目的挂科人数
JoinWrapper logicTable = JoinWrapper.create(ScoreDO.class, StudentDTO.class);
logicTable.select(ScoreDO::getCourseId, StudentDTO::getCourseId)
.select(Columns.count(), StudentDTO::getNum)
.where(ScoreDO::getScore, Condition.LT, 60)
.groupBy(ScoreDO::getCourseId)
.having(Columns.count(), Condition.GT, "1");
// 查询挂科超过1人的科目的科目信息与挂科人数
JoinWrapper wrapper = JoinWrapper.create(CourseDO.class, StudentDTO.class);
wrapper.selectAll()
// 关联逻辑表
.innerJoin(logicTable)
.on(CourseDO::getId, Condition.EQ, StudentDTO::getCourseId)
.selectAll();
~~~
该条件构造器构造的 SQL 同:
~~~sql
SELECT t1.*, t2.*
FROM course t1
INNER JOIN (
SELECT t1.course_id AS course_id, COUNT(*) AS num
FROM score t1
WHERE (t1.score < 60) GROUP BY t1.course_id HAVING COUNT(*) > 1
) t2 ON (t1.id = t2.course_id);
~~~
#### FROM
~~~java
// 查询挂科超过1人的科目及挂科人数
JoinWrapper wrapper = JoinWrapper.create(ScoreDO.class, StudentDTO.class);
wrapper.select(ScoreDO::getCourseId, StudentDTO::getCourseId)
.select(Columns.count(), StudentDTO::getNum)
.where(ScoreDO::getScore, Condition.LT, 60)
.groupBy(ScoreDO::getCourseId)
.having(Columns.count(), Condition.GT, 1);
// 将上一查询转为逻辑表,然后查询该科目名称
JoinWrapper logicTable = wrapper.toLogicTable()
.selectAll()
.leftJoin(CourseDO.class, w -> w
.on(StudentDTO::getCourseId, Condition.EQ, CourseDO::getId)
.select(CourseDO::getName, StudentDTO::getCourseName)
);
~~~
该条件构造器构造的 SQL 同:
~~~sql
SELECT t1.*, t2.name AS course_name
FROM (
SELECT t1.course_id AS course_id, COUNT(*) AS num
FROM score t1
WHERE (t1.score < 60)
GROUP BY t1.course_id
HAVING COUNT(*) > 1
) t1
LEFT JOIN course t2 ON (t1.course_id = t2.id)
~~~
#### WHERE
~~~java
// 查询挂科人数超过1人的科目
JoinWrapper wrapper = JoinWrapper.create(CourseDO.class, StudentDTO.class);
wrapper.selectAll()
.where(wrapper.toTableColumn(CourseDO::getId), Condition.IN, Columns.subQuery(
JoinWrapper.create(ScoreDO.class, StudentDTO.class)
.select(ScoreDO::getCourseId, StudentDTO::getCourseId)
.where(ScoreDO::getScore, Condition.LT, 60)
.groupBy(ScoreDO::getCourseId)
.having(Columns.count(), Condition.GT, "1")
));
~~~
该条件构造器构造的 SQL 同:
~~~sql
SELECT t1.*
FROM course t1
WHERE (
t1.id IN (
SELECT t1.course_id AS course_id
FROM score t1
WHERE (t1.score < 60) GROUP BY t1.course_id HAVING COUNT(*) > 1
)
);
~~~
### 6、原生方法适配
#### 兼容BaseMapper方法
JoinWrapper 兼容 mp 原生 Wrapper 中**除`setEntity()`外**的全部查询方法,并且也可以直接作为参数传入 BaseMapper 的方法中:
~~~java
// BaseMapper.selectList(Wrapper wrapper)
JoinWrapper wrapper = JoinWrapper.create(StudentDO.class, StudentDTO.class);
List students = studentMapper.selectList(wrapper);
~~~
不过这样使用时,除 Join 条件将不生效外,其余扩展功能仍可以正常使用。
#### 逻辑删除
当配置了逻辑删除时(具体配置参见[mybaits plus逻辑删除](https://baomidou.com/pages/6b03c5/)),JoinWrapper 将在初始化时,自动逻辑删除字段作为查询条件添加到 Where 条件后,关联表亦同。
假设现在已有配置:
~~~yml
mybatis-plus:
global-config:
db-config:
logic-delete-field: isDelete
logic-delete-value: 1
logic-not-delete-value: 0
~~~
当我们使用条件构造成构建一个查询时,会自动从 `com.baomidou.mybatisplus.core.metadata.TableInfo`获取逻辑删除相关配置,并自动添加条件 `logic-delete-field = login-not-delete-value`,如:
~~~java
JoinWrapper wrapper = JoinWrapper.create(StudentDO.class, StudentDTO.class);
List students = studentMapper.selectList(wrapper);
~~~
若 `StudentDO`及对于表存在字段`is_delete`,且已有相关逻辑删除配置,则实际构造出的 SQL 为:
~~~sql
SELECT * FROM student t1 where t1.is_delete = 0
~~~
#### 分页
参见[mybtis-plus分页插件](https://baomidou.com/pages/97710a/#paginationinnerinterceptor),该插件基于 SQL 分析生效,因此不受影响。
但是要注意,与当使用`JoinWrapper`构建关联查询时,与原写法一样,若 join 的表没有 where 条件,则生成的 countSql 会忽略 join 部分的表导致查询数据行数与实际待分页数据行数不一致。