背景
提到 ORM 框架,我们现在使用最多的是 MyBatis,MyBatis 解决了 Hibernate 不够灵活的问题,但是由于 MyBatis 需要手动指定数据库表和实体类之间的映射关系,对于单表而言,简单的增删改查我们也不得不写大量的 xml 配置。
MyBatis 官方为此又推出了一个 MyBatis Generator 的项目,可以为我们生成 Mapper 接口和配置文件,这大大缓解了开发者的手工编写 Mapper 配置文件的工作。
如果只是针对每个表每次都生成 Mapper 文件还好,但是 MyBatis Generator 有一个大大的弊端,它生成的动态 SQL 不够灵活,条件基本上是判断参数不为空然后指定列的值。
后来开源社区又推出了一个 MyBatis-Plus 的项目,它集多种特性于一身,包括内置通用 Mapper、分页插件、代码生成等,这些功能使开发者对它爱不释手。
MyBatis-Plus BaseMapper 快速上手
MyBatis-Plus 最核心的功能要数通用 Mapper 了,只要我们的 Mapper 接口实现了 BaseMapper,就可以完成单表大部分的 CRUD 操作了,并且它还附带了一个功能强大的条件构造器。
假定我们的项目已经引入 SpringBoot,现在正在做一个登陆功能,用户表如下。
create table user
(
id bigint unsigned auto_increment comment '主键'
primary key,
username varchar(100) null comment '用户名',
password varchar(100) null comment '密码',
create_time datetime null comment '创建时间'
)
我们期望在项目中引入 MyBatis-Plus,首先我们需要在项目中引入 MyBatis-Plus 依赖和数据库驱动。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
然后需要在 application.proeprties 中配置数据源。
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=12345678
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
使用 MyBatis-Plus 官网提供的 Idea 插件 MyBatis-X 生成的代码结构如下。
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── zzuhkp
│ └── blog
│ ├── App.java
│ ├── domain
│ │ └── User.java
│ └─── mapper
│ └── UserMapper.java
└── resources
├── application.properties
└── mapper
└── UserMapper.xml
看下生成的 UserMapper 接口。
public interface UserMapper extends BaseMapper<User> {
}
空空如也,UserMapper 接口仅仅继承了接口 BaseMapper,这样就可以进行单表增删改查。看下我们的测试代码。
@MapperScan("com.zzuhkp.blog.mapper")
@SpringBootApplication
public class App implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(App.class);
}
@Autowired
private UserMapper userMapper;
@Override
public void run(String... args) throws Exception {
String username = "hkp";
String password = "123";
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getUsername, username).eq(User::getPassword, password);
User user = this.userMapper.selectOne(wrapper);
}
}
测试代码使用 LambdaQueryWrapper 构造复杂的查询条件,然后使用 UserMapper 继承的 BaseMapper 中的方法查询用户。
不得不说,使用 MyBatis-Plus 进行单表操作实在太方便了,只是再美味的菜肴吃多了也会索然无味,时间长了,我们不免会产生疑问,BaseMapper 是怎样帮我们注入 SQL 的?带着这个疑问我们继续下面的分析。
MyBatis-Plus BaseMapper 实现分析
我们先来思考下 BaseMapper 的实现思路。正常情况下,我们定义了 Mapper 接口,然后会在对应的 xml 文件中提供动态 SQL 及映射关系,或者直接在 Mapper 接口方法上添加注解,MyBatis 将 xml 中的配置或者注解作为元数据进行解析,然后将解析后的 SQL 语句存至 Configuration。参考 MyBatis 对 xml 或注解的实现,只要我们能够将元数据解析成动态 SQL 存至 Configuration 即可。这就是基本的实现思路了,那么 MyBatis-Plus 具体是怎么做的呢?
MyBatis-Plus 的整体思路是使用自己的组件替换 MyBatis 中的组件,以实现自定义的逻辑。包括但不限于以下的组件。
MyBatis 组件 | 替换成的 MyBatis-Plus 组件 | 主要作用 |
MybatisLanguageDriverAutoConfiguration | MybatisPlusLanguageDriverAutoConfiguration | 仅复制 MyBatis 组件代码,无特殊逻辑 |
MybatisAutoConfiguration | MybatisPlusAutoConfiguration | 创建自定义的 MybatisSqlSessionFactoryBean,设置必须的 bean 到全局配置 |
SqlSessionFactoryBean | MybatisSqlSessionFactoryBean | 使用自定义的 MybatisConfiguration、MybatisXMLConfigBuilder |
XMLConfigBuilder | MybatisXmlConfigBuilder | 使用自定义的 MybatisConfiguration |
Configuration | MybatisConfiguration | 使用自定义的 MybatisMapperRegistry、MybatisXMLLanguageDriver |
MapperRegistry | MybatisMapperRegistry | 使用自定义的 MybatisMapperProxyFactory、MybatisMapperAnnotationBuilder |
MapperProxyFactory | MybatisMapperProxyFactory | 使用自定义的 MybatisMapperMethod |
MapperMethod | MybatisMapperMethod | 分页支持 |
ParameterHandler | MybatisParameterHandler | 填充主键、自定义属性值 |
XMLLanguageDriver | MybatisXMLLanguageDriver | 使用自定义的 MybatisParameterHandler |
MapperAnnotationBuilder | MybatisMapperAnnotationBuilder | 动态 SQL 注入 |
按照官网的说法,MyBatis-Plus 在 MyBatis 的基础上只做增强不做改变。我们使用 mybatis-plus-boot-starter 替换了 mybatis 提供的 mybatis-boot-starter,引入这个 starter 之后,MyBatis-Plus 就会进行一些自动化配置。mybatis-plus-boot-starter 类路径下META-INF/spring.factories
文件内容如下。
# Auto Configure
org.springframework.boot.env.EnvironmentPostProcessor=\
com.baomidou.mybatisplus.autoconfigure.SafetyEncryptProcessor
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.autoconfigure.IdentifierGeneratorAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
让我们将重点放在 MybatisPlusAutoConfiguration 类,这个类会替代 MyBatis 官方的 MybatisAutoConfiguration 进行自动化配置,核心源码如下。
@Configuration
public class MybatisPlusAutoConfiguration implements InitializingBean {
private final MybatisPlusProperties properties;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
...省略部分代码
GlobalConfig globalConfig = this.properties.getGlobalConfig();
...省略部分代码
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
...省略部分代码
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
}
MybatisPlusAutoConfiguration 使用 MybatisSqlSessionFactoryBean 替代了 MyBatis 官方的的 SqlSessionFactoryBean,这个类主要使用 MybatisConfiguration 替代了 MyBatis 官方的 Configuration。
让我们将重点放在和 BaseMapper 实现有关的部分。不管是使用 MyBatis 还是 MyBatis-Plus,我们都会使用 mapperLocations 配置 mapper xml 文件的位置。MybatisSqlSessionFactoryBean 创建 SqlSessionFactory 时会解析 mapper xml 文件。核心代码如下。
public class MybatisSqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private Resource[] mapperLocations;
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
...省略部分代码
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
}
...省略部分代码
}
}
这里解析 mapper xml 文件使用的是 MyBatis 官网提供的 XMLMapperBuilder,但是传入的配置 targetConfiguration 类型是 MybatisConfiguration。XMLMapperBuilder 解析 mapper xml 文件时还会解析 namespace 中的接口,然后添加到配置中。核心源码如下。
public class XMLMapperBuilder extends BaseBuilder {
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
if (boundType != null && !configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
MyBatis-Plus 提供的 MybatisConfiguration 重写了 addMapper 方法。
public class MybatisConfiguration extends Configuration {
protected final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);
@Override
public <T> void addMapper(Class<T> type) {
mybatisMapperRegistry.addMapper(type);
}
}
MybatisConfiguration 重点在于使用了自定义的 MybatisMapperRegistry 添加 Mapper。
public class MybatisMapperRegistry extends MapperRegistry {
@Override
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
boolean loadCompleted = false;
...省略部分代码
try {
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
自定义的 MybatisMapperRegistry 添加 Mapper 时又用到了 自定义的 MybatisMapperAnnotationBuilder,在这里 Mybatis-Plus 除了解析 Mapper 接口方法上的注解就会添加自己的逻辑了。
public class MybatisMapperAnnotationBuilder extends MapperAnnotationBuilder {
@Override
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
...省略解析 Mapper 方法注解代码
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
try {
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
parserInjector();
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new InjectorResolver(this));
}
}
}
void parserInjector() {
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
}
到了这里解析 Mapper 接口的时候,MyBatis-Plus 会判断接口是否继承 Mapper 接口,如果是的话就会注入动态 CURD 动态 SQL。这里我们可以看一下注入 SQL 的接口。
public interface ISqlInjector {
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
这个接口的默认实现是 DefaultSqlInjector。
public class DefaultSqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(),
new Delete(),
new DeleteByMap(),
new DeleteById(),
new DeleteBatchByIds(),
new Update(),
new UpdateById(),
new SelectById(),
new SelectBatchByIds(),
new SelectByMap(),
new SelectOne(),
new SelectCount(),
new SelectMaps(),
new SelectMapsPage(),
new SelectObjs(),
new SelectList(),
new SelectPage()
).collect(toList());
}
}
以BaseMapper#selectList
方法为例,MyBatis-Plus 使用的 AbstractMethod 为 SelectList。
public class SelectList extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo), sqlOrderBy(tableInfo), sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
}
}
MyBatis-Plus 有能力从 BaseMapper 中根据泛型解析出实体类,然后从实体类中根据注解解析出数据库表字段的信息,最终,生成的动态 SQL 大概如下。
<script>
<choose>
<when test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
</when>
<otherwise></otherwise>
</choose>
SELECT
<choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>${keyColumn as keyProperty,column1 as property1,column2 as property2}</otherwise>
</choose>
FROM {tableName}
<if test="ew != null">
<where>
<if test="ew.entity != null">
<if test="ew.entity.keyProperty != null">keyColumn=#{{ew.entity.keyProperty}}</if>
<if test="ew.entity.propertyName != null and ew.entity.propertyName != ''">
AND columnName=#{ew.entity.propertyName,jdbcType={jdbcTypeName},javaType={javaTypeName},typeHandler={typeHandlerName},numericScale={numericScaleName}}
</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> AND</if> ${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if>
<if test="ew == null or ew.expression == null or ew.expression.orderBy == null or ew.expression.orderBy.size() == 0">
ORDER BY {column1} asc|desc,{column2} asc|desc
</if>
<choose>
<when test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
</when>
<otherwise></otherwise>
</choose>
</script>
注入自定义动态 SQL
MyBatis-Plus 注入动态 SQL 的接口是 ISqlInjector,如果你仔细观察了前面代码中的 MybatisPlusAutoConfiguration 类,你就会发现,MyBatis-Plus 会使用 Spring 类型为 ISqlInjector 的 bean 替代默认 DefaultSqlInjector,因此,如果你想注入自己的动态 SQL,不妨自己提供一个 ISqlInjector 接口的实现作为 bean。MyBatis-Plus 原生并未提供使用 join 多表联查的能力,感兴趣话可自行尝试使用 ISqlInjector 实现。