
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使用了访问者模式。