Mybatis及MybatisPlus原理分析
Mybatis简单使用
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class TestMybatis {
public static void main(String[] args) throws IOException {
UserMapper userMapper = getMapper(UserMapper.class);
User user = userMapper.selectById(1);
System.out.println(user);
}
private static <T> T getMapper(Class<T> mapperClass) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sessionFactory = sqlSessionFactoryBuilder.build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
return sqlSession.getMapper(mapperClass);
}
public interface UserMapper {
User selectById(Integer uid);
}
@Data
@NoArgsConstructor
public static class User {
private Integer uid;
private String uname;
private Integer deleted;
}
}
resources 下 mybatis/UserMapper.xml,注意, User和 UserMapper 为内部类,所以使用 $ 拼接
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.demo.mybatis.TestMybatis$UserMapper">
<select id="selectById" resultType="org.example.demo.mybatis.TestMybatis$User">
select uid,uname,deleted from tb_user where uid = #{uid};
</select>
</mapper>
mybatis/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!-- JDBC 驱动-->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<!-- url数据库的 JDBC URL地址。-->
<property name="url" value="jdbc:mysql://ip:3310/testdb"/>
<property name="username" value="root"/>
<property name="password" value="xxx"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/UserMapper.xml"/>
</mappers>
</configuration>
Mybatis源码分析
解析过程
- 根据mybatis-config.xml创建通过SqlSessionFactoryBuilder创建SqlSessionFactory对象
- 通过XPathParser解析器来帮助解析xml
- XMLConfigBuilder解析config.xml
- XMLMapperBuilder解析Mapper.xml
- XMLStatementBuilder解析select等sql语句
- 最终的语句包装类为MappedStatement
- SqlSessionFactory对象其中包含一个完整的Configuration对象
- Configuration对象中的字段mappedStatements保存语句,key为类路径+sql的Id
- mapperRegistry保存所有解析的Mapper接口
- 每次向mapperRegistry中添加Mapper接口时,会尝试查找相同包名下的mapper.xml文件来解析。
执行过程
- 通过SqlSessionFactory对象创建SqlSession对象,具体类型为DefaultSqlSession
- 其中包含一个执行器Executor,经过了拦截器的处理
- 通过SqlSession对象获取Mapper对象,底层使用JDK动态代理
- 拦截器为MapperProxy对象
- 通过方法获取一个MapperMethod对象,其中包含MappedStatement
- 通过MappedStatement创建一个BoundSql对象
- BoundSql对象包含要执行的sql和参数等信息
- 创建一个StatementHandler,其中通过DefaultParameterHandler来设置参数
- 最终通过PreparedStatement执行
- 通过DefaultResultSetHandler来处理返回值,先处理列名属性自动映射,再处理我们配置的ResultMap,具体逻辑在 getRowValue() 方法。
关于SQL语句中的${}和#{}的处理
- MappedStatement中包含一个SqlSource对象
- DynamicSqlSource:SQL语句中包含${}
- RawSqlSource:SQL语句中不包含${}
- ProviderSqlSource:接口方法上使用注解配置SQL
- StaticSqlSource:在MappedStatement获取BoundSql对象时,会将#{}转换成?,并将之前的SqlSource对象转换成StaticSqlSource类型。
对于${}的处理(需要手动加单引号),具体处理类为TextSqlNode的apply()方法,是拼接SQL(通过OGNL表达式获取属性值),对于#{}的处理,是PreparedStatement设置参数(防止SQL注入)。orderby只能使用${}。SqlSourceBuilder来解析#{uname,jdbcType=VARCHAR}这种表达式语法。
select * from tb_user where u_name like '${uname}'; -- 需要加单引号
select * from tb_user where u_name like #{uname};
关于默认参数名称
select * from tb_user where u_name like #{param1} and u_age = #{param2};
具体处理类为ParamNameResolver。
关于like的使用
select * from tb_user where u_name like concat('%', #{uname}, '%');
需要在 sql 中拼接 % 或者代码中拼接 %
懒加载
Configuration的lazyLoadingEnabled字段,默认false,proxyFactory默认使用JavassistProxyFactory创建代理,也支持Cglib。在创建ResultMap时可能使用。
缓存
- 一级缓存:BaseExecutor的localCache字段,内部为HashMap,同一个SqlSession(因为不同的SqlSession包含不同的Executor)。默认开启,增删改会清空缓存。
- 二级缓存:SqlSessionFactory级别
<!-- mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- UserMapper.xml -->
<cache />
存储在Configuration的caches字段中,key为Mapper.xml的namespace。也会存储到MappedStatement的cache字段中
- 主要是TransactionalCacheManager的transactionalCaches字段来管理,key为Cache类型,value为TransactionalCache类型(包装了Cache),必须commit之后缓存才实际保存到Cache中。入口类为CachingExecutor。
- 从MappedStatement中获取Cache对象
- 根据此对象从TransactionalCacheManager中获取对应的TransactionalCache
- 查询是否有缓存值
- 有则直接返回,没有走一级缓存的流程
- 保存到TransactionalCache对象中
- commit之后,保存到TransactionalCache中的delegate字段中,实际是委托它来存储的。
分页插件
配置使用
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" />
</plugins>
PageHelper.startPage(1,2);
原理分析
- 将分页信息保存到ThreadLocal中,实际上是一个Page对象。
- 拦截BaseExecutor的query()方法
- 生成一个count的sql并执行(通过jsqlparser解析原来的sql)
- 将查询得到的数据总数量count也保存到Page中
- 对原来的查询SQL拼接分页信息,如mysql拼接limit
- 将查询得到的list结果也保存到Page中
- 最终返回的就是Page对象
整合SpringBoot
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
- 配置Mapper.xml路径
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
- 配置Mapper接口扫描包路径
@SpringBootApplication
@MapperScan("com.imooc.cnblogs.service.mybatis")
public class CnblogsBackUpApplication {
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(CnblogsBackUpApplication.class)
.build()
.run(args);
}
}
- 或者对Mapper接口添加@Mapper注解,最终创建的Mapper实现类为MapperFactoryBean
- 会自动装配MybatisAutoConfiguration,自动配置SqlSessionFactory,SqlSessionTemplate,最终使用的 SqlSession 实现为动态代理,拦截器为 SqlSessionInterceptor。
@MapperScan注解最终使用ClassPathMapperScanner依赖配置的basePackage将Mapper接口的实现(MapperFactoryBean)注入了Spring容器中,只要是独立(非内部类中)的接口就可以
MybatisPlus源码分析
解析过程
- 使用MybatisSqlSessionFactoryBuilder替代原来的SqlSessionFactoryBuilder
- 使用MybatisXMLConfigBuilder替代原来的XMLConfigBuilder
- 使用MybatisConfiguration替代Configuration
- 使用MybatisMapperRegistry替代MapperRegistry
- 使用MybatisMapperProxyFactory替代MapperProxyFactory
- 使用MybatisMapperProxy替代MapperProxy
- 使用MybatisMapperAnnotationBuilder替代MapperAnnotationBuilder
- 使用GlobalConfig保存全局配置
- 如果接口类是com.baomidou.mybatisplus.core.mapper.Mapper的子类,就使用ISqlInjector注入动态SQL的实现,我们可以自定义自己的SQL注入器
执行过程
- 使用MybatisCachingExecutor替代CachingExecutor
- 重写了query()方法,支持分页查询
- 使用PaginationInterceptor插件
- 拦截BaseStatementHandler的prepare()方法
- 生成一个count的SQL
- 使用拦截的方法参数connection执行count的SQL
MybatisPlus默认开启类名和表名的转换(属性和列名),驼峰转下划线,具体处理类为TableInfoHelper。
特性
- 内置通用mapper,可以很方便的实现单表的大部分增删改查操作。
- PaginationInterceptor用来处理类型为IPage的参数,MybatisMapperMethod用来处理返回类型为IPage的返回值。
- MetaObjectHandler用来在insert或update时自动设置属性值,配合注解@TableField的fill字段使用,在MybatisParameterHandler处理参数时生效。
- 注解@TableLogic,标注此注解的属性,调用内置的删除方法时,不是真正的delete,而是update,内置select时默认加上此条件。
- 注解@EnumValue,配合MybatisEnumTypeHandler处理器
IPage<User> findByPage(IPage<User> page); //页码从1开始
@TableField(value = "identity_type")
@ApiModelProperty(value = "身份类型 1: poc售点 2: 二批经销商")
private PocIdentityTypeEnum identityType;
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum PocIdentityTypeEnum {
/**
* 售点身份类型
*/
POC(1, "售点"),
WS(2, "二批商");
@EnumValue
private Integer type;
@JsonValue
private String description;
}
TkMybatis源码分析
解析过程
- MapperAutoConfiguration开启自动配置,在MybatisAutoConfiguration之前生效
- 定义SqlSessionFactory和SqlSessionTemplate
- 使用注解@tk.mybatis.spring.annotation.MapperScan替代@org.mybatis.spring.annotation.MapperScan
- 它的解析类MapperScannerRegistrar内部使用新的tk.mybatis.spring.mapper.ClassPathMapperScanner替代老的org.mybatis.spring.mapper.ClassPathMapperScanner
- 它会将Mapper接口的实现类替换为tk.mybatis.spring.mapper.MapperFactoryBean,代替老的org.mybatis.spring.mapper.MapperFactoryBean
- 新的MapperFactoryBean重写了checkDaoConfig()方法,会处理下边的配置的子接口
获取方法上的SelectProvider,UpdateProvider,DeleteProvider,InsertProvider注解信息,生成SqlSource对象,并设置到MappedStatement对象中mapper: mappers: tk.mybatis.mapper.common.Mapper
- 这样我们的接口只需要继承如tk.mybatis.mapper.common.Mapper接口就自然而然提供了增强功能
- 本质上还是利用了mybatis本身提供的Provider功能(通过Provider来自定义sql)
- 通过 EntityHelper 来解析 javax.persistence 包下的 @Table,@Id,@Column等注解,保存实体类和表结构的映射关系,功能类似 mybatisplus 的 TableInfoHelper 类。