Mybatis及MybatisPlus原理分析

发布时间 2023-09-30 02:29:35作者: strongmore

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来处理返回值

关于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。
  1. 从MappedStatement中获取Cache对象
  2. 根据此对象从TransactionalCacheManager中获取对应的TransactionalCache
  3. 查询是否有缓存值
  4. 有则直接返回,没有走一级缓存的流程
  5. 保存到TransactionalCache对象中
  6. 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

@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。

特性

  1. PaginationInterceptor用来处理类型为IPage的参数,MybatisMapperMethod用来处理返回类型为IPage的返回值。
  2. MetaObjectHandler用来在insert或update时自动设置属性值,配合注解@TableField的fill字段使用,在MybatisParameterHandler处理参数时生效。
  3. 注解@TableLogic,标注此注解的属性,调用内置的删除方法时,不是真正的delete,而是update,内置select时默认加上此条件。
  4. 注解@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源码分析

解析过程

  1. MapperAutoConfiguration开启自动配置,在MybatisAutoConfiguration之前生效
  2. 定义SqlSessionFactory和SqlSessionTemplate
  3. 使用注解@tk.mybatis.spring.annotation.MapperScan替代@org.mybatis.spring.annotation.MapperScan
  4. 它的解析类MapperScannerRegistrar内部使用新的tk.mybatis.spring.mapper.ClassPathMapperScanner替代老的org.mybatis.spring.mapper.ClassPathMapperScanner
  5. 它会将Mapper接口的实现类替换为tk.mybatis.spring.mapper.MapperFactoryBean,代替老的org.mybatis.spring.mapper.MapperFactoryBean
  6. 新的MapperFactoryBean重写了checkDaoConfig()方法,会处理下边的配置的子接口
    mapper:
      mappers: tk.mybatis.mapper.common.Mapper
    
    获取方法上的SelectProvider,UpdateProvider,DeleteProvider,InsertProvider注解信息,生成SqlSource对象,并设置到MappedStatement对象中
  7. 这样我们的接口只需要继承如tk.mybatis.mapper.common.Mapper接口就自然而然提供了增强功能
  8. 本质上还是利用了mybatis本身提供的Provider功能(通过Provider来自定义sql)