mybatis-plus
也是我在项目中发现的一个好用的框架,可能知道的人不多,所以很少看到关于它的介绍。正如官方介绍的:“我们的愿景是成为MyBatis
最好的搭档,就像魂斗罗中的1P
、2P
,基友搭配,效率翻倍”。目的是增强MyBatis
,简化开发、提高效率。
使用mybatis-plus
可以少写很多常用的SQL
,通过继承BaseMapper
使用,还可以动态拼接SQL
。第一眼看到我还以为是JPA
。对分页查询也做了很好的支持,实际上分页的实现还是基于mybatis
提供的插件(拦截器)特性去实现的。本篇将介绍如何快速上手mybatis-plus
,并重点从源码分析分页PaginationInterceptor
的性能。
在项目中添加依赖:
// mybatis and mybatis-plus
compile group: 'com.baomidou', name: 'mybatisplus-spring-boot-starter', version: '1.0.5'
compile group: 'com.baomidou', name: 'mybatis-plus', version: '2.2.0'
编写对应数据库表的实体类。使用@TableName
注解声明对应数据库中的哪个表,使用@TableId
注解注释的字段就是对应数据库表的主键列。如果不开启驼峰映射,还可以使用@TableField
注解将字段与表的字段名做映射。
@TableName("company")
public class Company {
@TableId
private Long id;
private String company;
private String email;
private String details;
// @TableField("create_tm")
private Date createTm;
// @TableField("modified_tm")
private Date modifiedTm;
}
如需开启驼峰映射,只需在yml
配置文件中将db-column-underline
改为true
。
mybatis-plus:
global-config:
# 配置开启驼峰映射
db-column-underline: true
编写Mapper
接口,需要继承BaseMapper
,泛型指定为对应表的实体类。无需编写mapper.xml
文件,就能使用一些常用的CRUD
功能,当然,也包括了分页查询。
/**
* 需要继承BaseMapper接口
*/
@Mapper
public interface CompanyMapper extends BaseMapper<Company> {
}
现在就可以使用CompanyMapper
了。
CompanyMapper companyMapper = applicationContext.getBean(CompanyMapper.class);
companyMapper.selectById(3);
companyMapper.selectBatchIds(Arrays.asList(3,4,5));
复杂一点的查询需要通过Wrapper
去构造查询条件,以分页查询为列。
Page<Company> pageResult = this.selectPage(
// 获取第一页,页大小为2
new Page(1,2),
// 查询id>=1 且 小于等于100的记录
new EntityWrapper<Company>().between("id", "1", "100"));
// 分页信息
System.out.println("current page:"+pageResult.getCurrent());
System.out.println("page size:"+pageResult.getSize());
System.out.println("total records:"+pageResult.getTotal());
System.out.println("page count:"+pageResult.getPages());
// 查询结果
pageResult.getRecords().stream().forEach(System.out::println);
EntityWrapper
是Wrapper
的一个子类。Wrapper
封装了where
、group by
、order by
、having
部分sql
的生成逻辑。提供between
、notIn
、in
、eq
、or
、and
、le
、lt
、gt
、notExists
、exists
、isNull
、isNotNull
、link
等方法指定查询条件,并支持链式调用。
Wrapper
非常适用于动态编写一些简单的sql
,省去在mapper
接口添加方法、再去xml
文件编写sql
的工作量,而且使用这种方式的好处还在于,简化mapper
接口,但是有利也有弊吧。复杂的sql
如果这么写,开发效率反而会下降,也不易于看懂,所以复杂的sql
并不推荐这么写。
使用分页查询要注意,需要配置分页拦截器PaginationInterceptor
,用于获取记录总数。
@Configuration
public class MybatisplusConfig {
/**
* 要使用分页查询功能,就需要配置分页拦截器
*
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
除此之外,我们还可以在Service
层通过继承ServiceImpl
使用Mapper
。如果Service
类有接口,则需要接口也继承IService
接口。
// 继承IService接口,以使用CompanyService时能够调用方法
public interface CompanyService extends IService<Company> {
}
@Service
public class CompanyServiceImpl extends ServiceImpl<CompanyMapper, Company>
implements CompanyService {
}
// 使用
public static void main(String[] args) {
try {
ApplicationContext applicationContext = SpringApplication.run(MybatisplusStuApplication.class);
CompanyService companyService = applicationContext.getBean(CompanyService.class);
Company company = companyService.selectById(4);
} catch (Exception e) {
e.printStackTrace();
}
}
如果Service
类没有接口,直接继承ServiceImpl
就可以了。
// 非接口方式,直接继承ServiceImpl类即可。
@Service
public class CompanyServiceNotInterface extends ServiceImpl<CompanyMapper, Company> {
}
// 使用
public static void main(String[] args) {
try {
ApplicationContext applicationContext = SpringApplication.run(MybatisplusStuApplication.class);
CompanyServiceNotInterface companyServiceNotInterface =
applicationContext.getBean(CompanyServiceNotInterface.class);
Company company1 = companyServiceNotInterface.selectById(3);
} catch (Exception e) {
e.printStackTrace();
}
}
现在来看下,为什么自己写的Service
没有注入Mapper
也能使用Mapper
接口的方法。其实很简单,就是ServiceImpl
中巧用泛型声明了Mapper
字段,并添加了@Autowired
注解,由Spring
自动注入了。
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
@Autowired
protected M baseMapper;
}
直接调用Mapper
接口的方法与调用ServiceImpl
的方法最大的区别在于ServiceImpl
在插入和更新的方法上添加了@Transactional
注解,支持事务。
最后说下关于分页的支持。通过分析PaginationInterceptor
拦截器的源码,看看它是怎么去获取总数的。下面是摘抄intercept
方法的部分源码。
// 生成获取总数的sql
SqlInfo sqlInfo = SqlUtils.getOptimizeCountSql(page.isOptimizeCountSql(), sqlParser, originalSql);
orderBy = sqlInfo.isOrderBy();
// 执行sql,将获取的总数写回page
this.queryTotal(overflowCurrent, sqlInfo.getSql(), mappedStatement, boundSql, page, connection);
SqlUtils.getOptimizeCountSql
这句是生成“获取总数”的sql
,因此重点也是在这里。因为生成的获取总数的sql
决定了这个分页插件是否可用,是否只是简单的在原来的查询语句外面去掉limt
之后包装一层select
。
public static SqlInfo getOptimizeCountSql(boolean optimizeCountSql, ISqlParser sqlParser, String originalSql) {
if (!optimizeCountSql) {
return SqlInfo.newInstance().setSql(getOriginalCountSql(originalSql));
}
// COUNT SQL 解析器
if (null == COUNT_SQL_PARSER) {
if (null != sqlParser) {
// 用户自定义 COUNT SQL 解析
COUNT_SQL_PARSER = sqlParser;
} else {
// 默认 JsqlParser 优化 COUNT
try {
COUNT_SQL_PARSER = DEFAULT_CLASS.newInstance();
} catch (Exception e) {
throw new MybatisPlusException(e);
}
}
}
// 返回COUNT SQL 解析器生成的查询总数的sql
return COUNT_SQL_PARSER.optimizeSql(null, originalSql);
}
如果optimizeCountSql
为false
,则只是简单的在原来的sql
外包装一层select
获取总数。该字段在Page
类中,默认为true
。不建议设置为false
。
/**
* 优化 Count Sql 设置 false 执行 select count(1) from (listSql)
*/
private boolean optimizeCountSql = true;
如果optimizeCountSql
为true
,则会使用COUNT SQL 解析器
生成优化后的查询总数的sql
。如果指定了COUNT SQL 解析器
则使用指定的解析器,如果没有则使用默认的解析器。这个就是在创建分页拦截器时配置的。
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor interceptor = new PaginationInterceptor();
// 自定义count sql解析器
interceptor.setSqlParser(new ISqlParser() {
@Override
public SqlInfo optimizeSql(MetaObject metaObject, String sql) {
return null;
}
});
return interceptor;
}
默认的COUNT SQL解析器
是JsqlParserCountOptimize
,由于它是使用jsqlparser
这个工具去优化sql
的,所以不打算继续往下分析了,感兴趣可以自己去看下。
public static ISqlParser COUNT_SQL_PARSER = null;
private static Class<ISqlParser> DEFAULT_CLASS = null;
static {
try {
DEFAULT_CLASS = (Class<ISqlParser>) Class.forName("com.baomidou.mybatisplus.plugins.pagination.optimize.JsqlParserCountOptimize");
} catch (ClassNotFoundException e) {
}
}
优化过的获取分页总数的SQL
与自己手写获取总数的SQL
没有区别,因此,这个分页插件还是推荐使用的。
关于jsqlparser
这个工具,我也是之前为插入语句自动添加create_time
这些通用字段写的一个插件时使用过,了解得不多,只记得jsqlparser
修改sql
使用了访问者模式。