Mybatis-Plus 的应用场景及注入 SQL 原理分析
一、背景
1.1 传统 Mybatis 的弊端
1.1.1 场景描述
假设有两张表:一张商品表、一张订单表,具体表的字段如下:

现有如下需求:
分别根据 id 查询商品表和订单表所有信息
根据支付状态和通知状态查询订单表信息
对订单表增加一个订单状态,根据订单状态查询订单信息
1.1.2 需求
需求 a:根据 id 查询商品表:
根据 id 查询订单表所有信息:
需求 b:根据支付状态和通知状态查询订单表信息
需求 c:对订单表增加一个订单状态 status,根据订单状态查询订单信息。
传统 mybaits 需要三步:首先需要在订单表里加个字段,然后在订单的实体类添加这个属性,并且将所有 dao 层设计该状态的的查询 sql 都修改一遍,加上这个字段。

1.1.3 上述方式有什么问题呢?
需求 a:对于不同的实体类,即使查询的目的一致,仍然需要重复构造类似的 sql 语句,仅仅是表字段和表信息不同。
需求 b:对于相似的查询条件,针对某个单一场景必须构造不同的 sql,造成 sql 语句的大量冗余。
需求 c:将 dao 层所有涉及到新增字段的 sql 都需要修改一遍,这个过程比较繁琐且容易出错。
使用 mybatis-plus 就可以解决上述问题。
1.1.4 Myatis-plus 的解决方案
首先让 ProductMapper 和 OrderMapper 继承 BaseMapper 类:

需求 a:
分别根据 id 查询商品表和订单表:由于 BaseMapper 中提供了 selectById 的方法,可以直接根据具体业务场景,传入指定的参数例如(id=1)即可。无需书写具体的 sql 语句,至于 sql 自动生成的原理将在下面介绍;
需求 b:
此时利用 BaseMapper.selectList(Wapper queryWrapper)方法直接构造查询条件,例如查询支付状态为 2 和通知状态为 1 的订单信息
此时我们可以发现:使用了 Mybatis-plus 以后,我们更加聚焦于业务本身,对于上述相似的应用场景,无需构造雷同的 SQL,利用包装器直接传入查询条件。
需求 c:
前两步与传统 mybatis 一致,由于 MyBatis-plus 无需手动创建 SQL,因此减少了大量的重复劳动。
1.2 MyBatis-Plus 的定位
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
1.3 特性
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑;
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作;
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求;
支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错;
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题;
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作;
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere );
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用;
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询;
分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库;
内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询;
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作。
1.4 原理解析
本文针对 MyBatis-plus 中的核心功能:SQL 自动注入功能,进行流程分析及原理探究。
二、准备工作
先从一个简单的 demo 入手,感受一下 MyBatis-plus 的便捷性。
2.1 基础接口 BaseMapper
源码中提供了一个基础接口,里面包含了基本的增删改查方法。

2.2 创建实体类对象

2.3 业务接口 UserMapper
业务中根据具体实体对象,继承该抽象接口。

2.4 测试用例


控制台显示:MyBatis-plus 最终为我们自动生成了 SQL 语句。根据上述操作分析:UserMapper 继承了 BaseMapper,拥有了 deleteById 的方法,但是 MyBatis-plus 是基于 mybatis 的增强版,关键在于最终仍然需要提供具体的 SQL 语句,来进行数据库操作。
下面就通过 debug 由上而下分析 mybatis-plus 是如何生成业务 sql 以及自动注入的。
三、SQL 语句存储在哪里?
mappedStatements:描述 sql 信息
如下图所示:mybatis 为我们生成了一个代理对象,里面包含了一些重要的属性。


具体如下:
userMapper——>mybatisMapperProxy——>sqlSession——>sqlSessionFactory
——>configuration——>mappedStatements——>mappedStatement——>sql 语句
至此我们可以发现每一个 SQL 语句对应一个 mappedStatement,mappedstatements 存储在 configuration 文件(configuration 是 mybatis 的全局配置文件,包含数据源、mapper、其他配置信息)中。
四、SQL 语句是什么时候注入的?
4.1 AbstractMethod.addMappedStatement
基于上面的分析,想要知道 SQL 语句什么时候拿到的,就是要找到 mappedStatement 被添加的位置。追踪到 AbstractMethod 的抽象方法中。
找到了 addMappedStatement()方法

而 BaseMapper 的所有方法(deleteById、delete、insert、select、update 等)都继承了该抽象方法。

根据 mapper 方法(deleteById)显然是调用 addDeleteMappedStatement 方法。


这里我们可以发现,源码中根据不同的方法继承 AbstractMethod 实现了不同的实现类,并且实现了 injectMappedStatement 方法,sqlSource 也是在这个地方被添加进配置文件。
4.2 AbstractMethod.inject
继续研究 AbstractMethod 抽象类,inject 方法实现了自动注入 sql 的动作。



有上述源码可知,项目启动时,首先由默认注入器生成基础 CRUD 实现类对象,其次遍历实现类列表,依次注入各自的模板 SQL,最后将其添加至 mappedstatement。
五、SQL 语句是怎么生成的?
5.1 SQL 模板
上述方法中有两个关键的参数:SqlMethod、SqlSource;

继续研究源码发现:sqlMethod 本质上是一个枚举类,存储了两个关键的元素:
BaseMapper 中的方法名;
方法名对应的 sql 语句模板(即被<scripe>标签包裹的字符串)。
到这里我们基本了解了 mybaits-plus 实现 sql 自动生成的本质:根据不同的方法来提供一些通用的模板,项目启动后再加载进 mappedStatement。
5.2 SqlSource

此时 SqlSource 通过解析 SQL 模板、以及传入的表信息和主键信息构建出了 SQL 语句。
5.3 数据库表信息是如何获取的?


分析 initTableName()方法:获取表名信息源码中传入了实体类信息 clazz,其实就是通过实体上的 @TableName 注解拿到了表名;


我们在定义实体类的同时,指定了该实体类对应的表名。
分析 initTableFields()方法:
获取主键及其他字段信息

至此 tableInfo 的信息已经注入完成了。
在研究完解析 mapper 的核心过程之后,我们再简单看下 mapper 文件被添加到 configuration(mybatis 核心配置文件)的过程。
六、mapper 文件被添加的过程
ISqlInjector:Sql 注入器

MybatisMapperAnnotationBuilder:mapper 解析器
MybatisMapperAnnotationBuilder 中的 parse 方法获取了 sqlInjector(Sql 注入器)来进行 SQL 注入。

Mybatis 添加 mapper 的固有流程:MybatisMapperRegistry
调用 MapperAnnotionBuilder 解析器进行解析

MybatisConfiguration.addMapper

MybatisXMLConfigBuilder.mapperElemnt

MybayisXMLConfigBuilder.parseConfiguration
添加 mapper 文件的过程分析到这里就完成了。

七、总结
7.1 流程梳理
下面总结梳理了一下 mybatis-plus 解析 mapper 文件自动注入 sql 的主要流程。

7.2 Mybatis-plus 的 ORM 的核心思想
1)实体类和数据库表通过自定义注解来完成一一映射。
2)对象属性和字段同样使用注解来一一对应(命名注意要相同)。
3)为了提高复用性使得具体的 mapper 继承通用的增删改查接口。
4)利用模板方法和对象属性值动态拼接 SQL。
八、参考文档
MyBatis-plus 官方文档:https://mp.baomidou.com/
作者:vivo 互联网服务器团队-Li Lei
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/3b6412c5948e4ff489667f8c7】。文章转载请联系作者。
评论