给Mybatis-Plus插上小翅膀,支持多表查询

之前一直使用Mybatis-Plus,说实话,个人还是比较喜欢Mybatis-Plus

ORM框架用的比较多的就两个,JPAMybatis。据说国内用Mybatis比较多,国外用JPA比较多。

Mybatis-Plus是在Mybatis的基础上,增加了很多牛🍺的功能。

再粘一下官网介绍的特性,又啰嗦了:

  1. 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  2. 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  3. 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  4. 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  5. 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  6. 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  7. 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  8. 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  9. 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  10. 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  11. 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  12. 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

详细的可以去官网看:mybatis.plus/
官网新域名也是牛🍺。反正用过的都说好。

至于JPA,虽然个人觉得有点太死板,不过也有值得学习的地方。

很早以前,用Mybatis-Plus的时候,有一个比较麻烦的问题,就是如果一组数据存在多张表中,这些表之间可能是一对一,一对多或者多对一,那我要想全部查出来就要调好几个Mapper的查询方法。代码行数一下就增加了很多。

之前也看过Mybatis-Plus的源码,想过如何使Mybatis-Plus支持多表联接查询。可是发现难度不小。因为Mybatis-Plus底层就只支持单表。

最近看到JPA@OneToOne@OneToMany@ManyToMany这些注解,忽然一个想法就在我的脑海里闪现出来,如果像JPA那样使用注解的方式,是不是简单很多呢?

事先声明,全是自己想的,没有看JPA源码, 所以实现方式可能和JPA不一样。

说干就干

  1. 添加注解
  2. 处理注解
  3. 打包发布

可能有人不知道,其实Mybatis也是支持拦截器的,既然如此,用拦截器处理注解就可以啦。

注解 One2One

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Inherited
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface One2One {

/**
* 本类主键列名
*/
String self() default "id";

/**
* 本类主键在关联表中的列名
*/
String as();

/**
* 关联的 mapper
*/
Class<? extends BaseMapper> mapper();

}

说一下,假如有两张表,AB是一对一的关系,AidB表中是a_id,用这样的方式关联的。
A的实体类中使用这个注解,self就是id,而as就是a_id,意思就是Aid作为a_id来查询,而mapper就是BMapper,下面是例子A就是UserAccountB就是UserAddress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "UserAccount对象", description = "用户相关")
public class UserAccount extends Model<UserAccount> {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Long id;

@ApiModelProperty(value = "昵称")
private String nickName;

@TableField(exist = false)
//把id的值 作为userId 在 UserAddressMapper中 查询
@One2One(self = "id", as = "user_id", mapper = UserAddressMapper.class)
private UserAddress address;

@Override
protected Serializable pkVal() {
return this.id;
}
}

Mybatis拦截器 One2OneInterceptor

这里不再详细介绍拦截器了,之前也写了几篇关于Mybatis拦截器的,有兴趣的可以去看看。

Mybatis拦截器实现Geometry类型数据存储与查询

Mybatis拦截器打印完整SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
public class One2OneInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (result == null) {
return null;
}
if (result instanceof ArrayList) {
ArrayList list = (ArrayList) result;
for (Object o : list) {
handleOne2OneAnnotation(o);
}
} else {
handleOne2OneAnnotation(result);
}
return result;
}

@SneakyThrows
private void handleOne2OneAnnotation(Object o) {
Class<?> aClass = o.getClass();
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
One2One one2One = field.getAnnotation(One2One.class);
if (one2One != null) {
String self = one2One.self();
Object value = MpExtraUtil.getValue(o, self);
String as = one2One.as();
Class<? extends BaseMapper> mapper = one2One.mapper();
BaseMapper baseMapper = SpringBeanFactoryUtils.getApplicationContext().getBean(mapper);
QueryWrapper<Object> eq = Condition.create().eq(as, value);
Object one = baseMapper.selectOne(eq);
field.set(o, one);
}
}
}

@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}

@Override
public void setProperties(Properties properties) {
}
}

Mybatis拦截器可以针对不同的场景进行拦截,比如:

  1. Executor:拦截执行器的方法。
  2. ParameterHandler:拦截参数的处理。
  3. ResultHandler:拦截结果集的处理。
  4. StatementHandler:拦截Sql语法构建的处理。

这里是通过拦截结果集的方式,在返回的对象上查找这个注解,找到注解后,再根据注解的配置,自动去数据库查询,查到结果后把数据封装到返回的结果集中。这样就避免了自己去多次调Mapper的查询方法。

难点:虽然注解上标明了是什么Mapper,可是在拦截器中取到的还是BaseMapper,而用BaseMapper实在不好查询,我试了很多方法,不过还好Mybatis-Plus支持使用Condition.create().eq(as, value);拼接条件SQL,然后可以使用baseMapper.selectOne(eq);去查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class MpExtraUtil {

@SneakyThrows
public static Object getValue(Object o, String name) {
Class<?> aClass = o.getClass();
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.getName().equals(name)) {
return field.get(o);
}
}
throw new IllegalArgumentException("未查询到名称为:" + name + " 的字段");
}
}

MpExtraUtil就是使用反射的方式,获取id的值。

再讲一个多对多的注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码@Inherited
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Many2Many {

/**
* 本类主键列名
*/
String self() default "id";

/**
* 本类主键在中间表的列名
*/
String leftMid();

/**
* 另一个多方在中间表中的列名
*/
String rightMid();

/**
* 另一个多方在本表中的列名
*/
String origin();

/**
* 关联的 mapper
*/
Class<? extends BaseMapper> midMapper();

/**
* 关联的 mapper
*/
Class<? extends BaseMapper> mapper();

}

假设有AA_BB三张表,在A的实体类中使用这个注解, self就是A表主键idleftMid就是A表的id在中间表中的名字,也就是a_id,而rightMidB表主键在中间表的名字,就是b_id, origin就是B表自己主键原来的名字,即idmidMapper是中间表的Mapper,也就是A_B对应的MappermapperB表的Mapper

这个确实有点绕。

还有一个@One2Many就不说了,和@One2One一样,至于Many2One,从另一个角度看就是@One2One

使用方法

先把表创建好,然后代码生成器一键生成代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sql复制代码CREATE TABLE `user_account` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`nick_name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户相关';

CREATE TABLE `user_address` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '地址id',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`address` varchar(200) DEFAULT NULL COMMENT '详细地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


CREATE TABLE `user_class` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '课程id',
`class_name` varchar(20) DEFAULT NULL COMMENT '课程名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


CREATE TABLE `user_hobby` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '爱好id',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`hobby` varchar(40) DEFAULT NULL COMMENT '爱好名字',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


CREATE TABLE `user_mid_class` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '中间表id',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`class_id` bigint(20) DEFAULT NULL COMMENT '课程id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

添加依赖,已经发布到中央仓库了,可以直接使用:

1
2
3
4
5
xml复制代码<dependency>
<groupId>top.lww0511</groupId>
<artifactId>mp-extra</artifactId>
<version>1.0.1</version>
</dependency>

在启动类上添加注解

@EnableMpExtra

配置拦截器

因为一般项目都会配置自己的MybatisConfiguration,我在这里配置后,打包,然后被引入,是无法生效的。

所以就想了一种折中的方法。

以前MybatisConfiguration是通过new出来的,现在通过MybatisExtraConfig.getMPConfig();来获取,这样获取到的MybatisConfiguration就已经添加好了拦截器。

完整Mybatis-Plus配置类例子,注意第43行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码@Slf4j
@Configuration
@MapperScan(basePackages = "com.ler.demo.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfig {

private static final String BASE_PACKAGE = "com.ler.demo.";

@Bean("dataSource")
public DataSource dataSource() {
try {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost/mp-extra?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
dataSource.setUsername("root");
dataSource.setPassword("adminadmin");

dataSource.setInitialSize(1);
dataSource.setMaxActive(20);
dataSource.setMinIdle(1);
dataSource.setMaxWait(60_000);
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
dataSource.setTimeBetweenEvictionRunsMillis(60_000);
dataSource.setMinEvictableIdleTimeMillis(300_000);
dataSource.setValidationQuery("SELECT 1");
return dataSource;
} catch (Throwable throwable) {
log.error("ex caught", throwable);
throw new RuntimeException();
}
}

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setVfs(SpringBootVFS.class);
factoryBean.setTypeAliasesPackage(BASE_PACKAGE + "entity");

Resource[] mapperResources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml");
factoryBean.setMapperLocations(mapperResources);
// 43行 获取配置
MybatisConfiguration configuration = MybatisExtraConfig.getMPConfig();
configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.addInterceptor(new SqlExplainInterceptor());
configuration.setUseGeneratedKeys(true);
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}

@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}

@Bean(name = "transactionManager")
public PlatformTransactionManager platformTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Bean(name = "transactionTemplate")
public TransactionTemplate transactionTemplate(@Qualifier("transactionManager") PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}

}

在实体类上建立关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "UserAccount对象", description = "用户相关")
public class UserAccount extends Model<UserAccount> {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Long id;

@ApiModelProperty(value = "昵称")
private String nickName;

@TableField(exist = false)
//把id的值 作为userId 在 UserAddressMapper中 查询
@One2One(self = "id", as = "user_id", mapper = UserAddressMapper.class)
private UserAddress address;

@TableField(exist = false)
@One2Many(self = "id", as = "user_id", mapper = UserHobbyMapper.class)
private List<UserHobby> hobbies;

@TableField(exist = false)
@Many2Many(self = "id", leftMid = "user_id", rightMid = "class_id", origin = "id"
, midMapper = UserMidClassMapper.class, mapper = UserClassMapper.class)
private List<UserClass> classes;

@Override
protected Serializable pkVal() {
return this.id;
}

}

主要是那几个注解。对了还要加@TableField(exist = false),不然会报错。

测试接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@Slf4j
@RestController
@RequestMapping("/user")
@Api(value = "/user", description = "用户")
public class UserAccountController {

@Resource
private UserAccountService userAccountService;

@Resource
private UserAccountMapper userAccountMapper;

@ApiOperation("查询一个")
@ApiImplicitParams({
@ApiImplicitParam(name = "", value = "", required = true),
})
@GetMapping(value = "/one", name = "查询一个")
public HttpResult one() {
//service
UserAccount account = userAccountService.getById(1L);
//mapper
// UserAccount account = userAccountMapper.selectById(1L);
//AR模式
// UserAccount account = new UserAccount();
// account.setId(1L);
// account = account.selectById();
return HttpResult.success(account);
}
}

接口非常简单,调用内置的getById,可是却查出了所有相关的数据,这都是因为配置的那些注解。


可以看到其实发送了好几条SQL。第一条是userAccountService.getById(1L),后面几条都是自动发送的。


源码 gitee.com/github-2635…

GitHub: github.com/LerDer/mp-e…

示例 gitee.com/github-2635…

总结

实在不想贴太多代码,其实还是挺简单的,源码地址还有示例地址都贴出来啦,有兴趣的可以去看一下。觉得好用可以点个Star。欢迎大家一起来贡献。

最后欢迎大家关注我的公众号,共同学习,一起进步。加油🤣

搜索 南诏Blog 关注

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%