MyBatis-Plus BaseMapper 是怎样实现的?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 背景提到 ORM 框架,我们现在使用最多的是 MyBatis,MyBatis 解决了 Hibernate 不够灵活的问题,但是由于 MyBatis 需要手动指定数据库表和实体类之间的映射关系,对于单表而言,简单的增删改查我们也不得不写大量的 xml 配置。

背景


提到 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 中的组件,以实现自定义的逻辑。包括但不限于以下的组件。


image.png


按照官网的说法,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 实现。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3月前
|
SQL Java Kotlin
MybatisPlus怎么拓展自定义BaseMapper
通过扩展Mybatis-Plus的`BaseMapper`,可以自定义SQL模板以满足特定业务需求。例如,当遇到唯一键冲突而不希望抛出异常时,可使用`INSERT IGNORE`语法。首先,创建`InsertIgnore`类继承`AbstractMethod`并定义`insertIgnore`方法及其SQL模板。接着,在自定义的`UltraBaseMapper`接口中声明`insertIgnore`方法,并让业务Mapper继承此接口。最后,通过`UltraSqlInjector`类将`InsertIgnore`方法注册到Mybatis-Plus插件中。
126 1
|
4月前
|
Java 数据库连接 Maven
文本,使用SpringBoot工程创建一个Mybatis-plus项目,Mybatis-plus在编写数据层接口,用extends BaseMapper<User>继承实体类
文本,使用SpringBoot工程创建一个Mybatis-plus项目,Mybatis-plus在编写数据层接口,用extends BaseMapper<User>继承实体类
MyBatis-Plus之BaseMapper
MyBatis-Plus之BaseMapper
193 0
|
Java 数据库连接 数据库
mybatis plus中BaseMapper接口
在使用的过程中,BaseMapper会指定范型T,T又通过TableName关联了某个数据库的某张表,因此实际上BaseMapper的所有操作都是针对某个数据库的某张表来执行。
|
SQL 安全 Java
MyBatisPlus的CRUD 接口BaseMapper
MyBatisPlus的CRUD 接口BaseMapper
|
XML Java 关系型数据库
MyBatisPlus基本入门------连接数据库查询 -----BaseMapper基本操作
MyBatisPlus基本入门------连接数据库查询 -----BaseMapper基本操作
|
SQL Java 数据库连接
Data Access 之 MyBatis Plus(五)- 自定义 BaseMapper
Data Access 之 MyBatis Plus(五)- 自定义 BaseMapper
Data Access 之 MyBatis Plus(五)- 自定义 BaseMapper
|
SQL 缓存 Java
Data Access 之 MyBatis Plus(一)- BaseMapper CRUD(Part B)
Data Access 之 MyBatis Plus(一)- BaseMapper CRUD(Part B)
Data Access 之 MyBatis Plus(一)- BaseMapper CRUD(Part B)
|
SQL XML Java
Data Access 之 MyBatis Plus(一)- BaseMapper CRUD(Part A)
Data Access 之 MyBatis Plus(一)- BaseMapper CRUD(Part A)
Data Access 之 MyBatis Plus(一)- BaseMapper CRUD(Part A)