mybatis使用和源码分析

概念

它是一个半ORM框架,帮你处理了加载驱动、创建连接、statement等操作,最终调用JDBC方法。

优点

集成spring方便;
编写原生sql,优化起来灵活度高;
避免对dto的手动set、get以及设置参数,提供了映射标签,支持自定义映射;
兼容性和JDBC有关,非常好;

<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>

缺点

原生sql工作量大,同时依赖数据库,依赖程度高,可移植性差;

和Hibernate的区别

mybatis是半ORM,hibernate是完全的ORM,数据库无惯性好,移植数据库方便。

但在互联网项目中,数据库一旦选型,分库分表一做好就不会轻易更换,所以这个可移植性是没必要的,更需要严格控制sql执行性能的能力,所以mybatis才会在互利网公司中流行起来。

一二级缓存

版权声明:本文为CSDN博主「林海静」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jinhaijing/article/details/84230810

一级:sqlSession级,会话查到的数据放入一级缓存

二级:mapper.xml(namespace)级,会话关闭时(openSession.close();后,才能从二级缓存中查数据;),一级缓存放入二级缓存;

开启:

1)、开启全局二级缓存配置:<setting name="cacheEnabled" value="true"/>
2)、去mapper.xml中配置使用二级缓存: <cache></cache>
3)、我们的POJO需要实现序列化接口

<cache>中配置一些参数:

eviction:缓存的回收策略:
    • LRU – 最近最少使用的:移除最长时间不被使用的对象。
    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
    • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
    • 默认的是 LRU。
flushInterval:缓存刷新间隔
    缓存多长时间清空一次,默认不清空,设置一个毫秒值
readOnly:是否只读:
    true:只读;mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。
             mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快
    false:非只读:mybatis觉得获取的数据可能会被修改。
            mybatis会利用序列化&反序列的技术克隆一份新的数据给你。安全,速度慢
size:缓存存放多少元素;
type="":指定自定义缓存的全类名;
        实现Cache接口即可;

一对一配置

版权声明:本文为CSDN博主「Selenium39」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/niangou0915/article/details/90698773

model

@Data
public class User {
    private Integer userId;
    private String userName;
    private Integer cId;
    private Car car;
}
@Data
public class Car {
    private Integer carId;
    private String  carName;
}

嵌套查询:(会先后执行两条sql语句)

<mapper namespace="com.wantao.dao.UserDao">
    <resultMap type="com.wantao.bean.User" id="map1">
        <id column="user_id" property="userId"></id>
        <result column="user_name" property="userName" />
        <result column="c_id" property="cId" />
        <association select="com.wantao.dao.CarDao.findCarById"
            property="car" column="c_id">
        </association>
    </resultMap>
    <select id="findUserById" resultMap="map1">
        select
        *
        from tb_user u
        where u.user_id=#{id}
    </select>
</mapper>

<mapper namespace="com.wantao.dao.CarDao">
    <select id="findCarById" resultType="com.wantao.bean.Car">
        select car_id,car_name
        from
        tb_car
        where car_id=#{id}
    </select>
</mapper>

联合查询:

<mapper namespace="com.wantao.dao.UserDao">
    <resultMap type="com.wantao.bean.User" id="map1">
        <id column="user_id" property="userId"></id>
        <result column="user_name" property="userName" />
        <result column="c_id" property="cId" />
        <association property="car" javaType="com.wantao.bean.Car">
               <id column="car_id" property="carId"/>
               <result column="car_name" property="carName"/>
        </association>
    </resultMap>
    <select id="findUserById" resultMap="map1">
        select
        u.user_id,u.user_name,u.c_id,c.car_id ,c.car_name
        from tb_user u
        left join tb_car c on u.c_id = c.car_id
        where u.user_id=#{id}
    </select>
</mapper>

一对多配置

model

@Data
public class User {
    private Integer userId;
    private String userName;
    private List<Car> cars;
}
@Data
public class Car {
    private Integer carId;
    private String  carName;
    private Integer uId;
}

联合查询:

<mapper namespace="com.wantao.dao.UserDao">
    <resultMap type="com.wantao.bean.User" id="map1">
        <id column="user_id" property="userId"></id>
        <result column="user_name" property="userName" />
        <collection property="cars" ofType="com.wantao.bean.Car">
           <id property="carId" column="car_id"/>
           <result property="carName" column="car_name"/>
           <result property="uId" column="u_id"/>
        </collection>
    </resultMap>
    <select id="findUserById" resultMap="map1">
        select
        user_id,user_name,car_id,car_name,u_id
        from tb_user u
        left join tb_car c on u.user_id = c.u_id
        where u.user_id=#{id}
    </select>
</mapper>

源码分析

分页插件分析

 <!--pagehelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.3</version>
</dependency>

接口定义
image-20211106165812490

@Intercepts(
    {
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)

public class PageInterceptor implements Interceptor {
    private Dialect dialect;//自定义方言类

    @Override
    public Object plugin(Object target) {
        //TODO Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化,因此考虑在这个方法中做一次判断和初始化
        //TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
        ...就是把xml配的属性保存到this bean中。
    }
    //核心方法
     @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //从invocation中提取参数
         Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();

        //初始化一次的参数
        BoundSql boundSql=ms.getBoundSql(parameter);
        CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);

        //中间的逻辑就是看要不要进行分页,要的话就new一个BoundSql,从原来的boundSql中复用一部分属性,最终调用executor.query时用的新BoundSql

        //最终调用  走JDBC
        List resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
    }

插件从哪开始调用的?

在InterceptorChain中,有如下一段代码:

private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

再观察它在哪被调用的
image-20211106170109043

原来mybatis四大组件都初始化构造时启用插件!四大组件为
– executor 执行Ms,BoundSql的,有Base,Caching,Simple、Reuse、Batch实现
– ParameterHandler 主要方法setParameters(PreparedStatement ps)
– StatementHandler 实现有Base,Simple,Callable,Prepared,Routing
– ResultHandler 用于处理结果,主要实现DefaultResultSetHandler

只有Executor是在newExecutor时(也就是获得SqlSession时)启用插件,其它三组件是在BaseStatementHandler的构造方法中启用的。

既然四种组件都进行了插件化,怎么控制在只哪个组件中生效?

仔细看分页插件的配置
image-20211106171242337

而控制生效实际是在
Plugin.wrap(target, this)
这段生成代理类的方法中实现的——
target其实就是哪个组件进行插件化时传的那个组件的实现类,
this指的就是当前实现了Interceptor的类。

wrap中使用JDK实现了一个动态代理,它实现了当前组件接口的所有方法,并传入一个InvocationHandler,它在invoke时会判断当前执行的方法签名是否跟@Intercepts中定义拦截的方法签名一致,一致才会执行interceptor方法(Plugin.wrap中的第二个参数)

也就是四大组件其实都生成了代理类,由@Intercepts中的配置控制在哪个类的哪个方法中才用这个Interceptor拦截。

spring中mybatis的配置

@MapperScan(basePackages = "com.jiangli.api.mapper", sqlSessionFactoryRef = "sqlSessionFactoryCommon")


@Bean
    public SqlSessionFactory sqlSessionFactoryCommon(@Qualifier("dataSourceModified") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);//设置数据源

        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        factoryBean.setConfiguration(configuration);//设置配置属性

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));//获得Resource[]   设置mapper.xml资源文件

        factoryBean.setTypeAliasesPackage("com.jiangli.api.model");//设置model类路径

        return factoryBean.getObject();
    }

mybatis从配置到源码分析

SqlSessionFactory配置比较直观,主要引用了DataSource,然后对属性、xml资源、model包进行了配置。

重点看@MapperScan。

@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
    ....


//对BeanDefinition这一层的操作,而@Bean是对Bean instance这一层的操作
public interface ImportBeanDefinitionRegistrar {
        //
        public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
    }


public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

实现了 registerBeanDefinitions 方法

深入MapperScannerRegistrar实现的registerBeanDefinitions方法
1. cps = new ClassPathMapperScanner(registry)
1. 把@MapperScan上设置的属性设置到cps上
1. 调用scanner.doScan(StringUtils.toStringArray(basePackages))【由value、basePackages、basePackageClasses合并而来】;

ClassPathMapperScanner的doScan方法
1. 调用父类ClassPathBeanDefinitionScanner(spring-context中的类)的doScan
对于每一个路径basePackage,找到候选的BD(其实就是路径下所有的class),最终调用registry.registerBD方法
1. processBeanDefinitions
对上一步产生的BD设置class为MapperFactoryBean,对构造函数使用了参数为bd.getBeanClassName,为然后添加属性引用sqlSessionFactory、sqlSessionTemplate、autowireMode

其实到这一步基本流程已经确定
@MapperScan -> @Import -> registerBD(basePkgs) & process(主要是设置class为MapperFactoryBean),也就是对于pkg下每一个interface,注册了一个MapperFacotryBean。

再来看看MapperFacotryBean的主要方法
1. protected void checkDaoConfig() {
最顶层由InitializingBean的afterPropertiesSet调用
它自己的实现逻辑是如果Configuration中无法通过InterfaceClass找到mapper,就调用addMapper方法
1. public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
getSqlSession()会获得一个SqlSessionTemplate(sqlSessionFactory)(属性在MapperFactoryBean的父类SqlSessionDaoSupport中)
MapperFacotryBean主要是一个代理,实际获得的bean的逻辑都交由Configuration管理;

Configuration是什么
它内部有很多其它类和一些缓存,上面管理mapper的是MapperRegistry类。

MapperRegistry逻辑很简单,内部维护了一个Map<Class,MapperProxyFactory>,如果map找不到,就put一个new MapperProxyFactory(type),
然后使用MapperAnnotationBuilder对type进行解析。
要获取Mapper的时候,获得这个MapperProxyFactory,调用newInstance(sqlSession)方法获得具体实现类;

MapperProxyFactory的newInstance用的JDK代理实现,实现接口显然是MapperInterface,InvocationHandler是new MapperProxy(sqlSession, mapperInterface, methodCache);

MapperProxy
它的public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
方法在执行时会判断,如果不是Object定义的方法或是接口默认实现,会调用MapperMethod的execute执行
根据SqlCommand类型调用sqlSession的insert、update、delete等方法;
在DefaultSqlSession实现中,

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

    在BaseExecutor中,query方法逻辑如下
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    ...
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    ...
    queryFromDatabase又调用了org.apache.ibatis.executor.SimpleExecutor#doQuery,逻辑如下
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    ...
    org.apache.ibatis.executor.statement.PreparedStatementHandler#query逻辑如下,
      PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);

总结:使用JDK生成了一个动态代理类(在被@Reference引用时初始化),这个类实现了Mapper接口,调用其定义的方法时会调用MapperMethod根据sql的类型路由到SqlSession的insert、update等方法中。

SqlSession的执行逻辑则是根据参数获得BoundSql,通过Configuration生成StatementHandler,准备参数,执行query、update等方法并返回。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注