# 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`以在不修改已有代码的基础上支持上述功能。 ![maven-central](https://img.shields.io/badge/maven--central-2.2.2--alpha1-green) ## 二、快速开始 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 部分的表导致查询数据行数与实际待分页数据行数不一致。