1. 半ORM框架

  • Mybatis属于半ORM,因为SQL语句需要自己写
  • 与其他比较标准的ORM 框架(比如 Hibernate )不同,Mybatis并没有将Java对象与数据库关联起来,而是将Java方法与SQL语句关联起来,Mybatis允许用户充分利用数据库的各种功能,例如存储、视图、各种复杂的查询以及某些数据库的专有特性
  • 自己写SQL语句的好处是,可以根据自己的需求,写出最优的SQL语句。灵活性高。但是,由于是自己写SQL语句,导致平台可移植性不高

    9. 与SpringBoot的整合

    9.1. 自动装配的类

  • 依赖于SpringBoot的自动装配功能

  • @MapperScan注解引入了@Import(MapperScannerRegistrar.class)

    1. MapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar接口,所以在ConfigurationClassReaderloadBeanDefinitionsFromRegistrars方法中会调用这个类的registerBeanDefinitions方法,这个方法注册了MapperScannerConfigurer这个BeanDefinition

    9.2. Mapper的扫描过程

  • MapperScannerConfigurer类实现了BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware这三个接口,在这个类的postProcessBeanDefinitionRegistry方法中,将扫描指定路径下的所有接口并注册为一个个的beanclass=MapperFactoryBean的BeanDefinition。这个方法是在refresh阶段的invokeBeanFactoryPostProcessor的invokeBeanDefinitionRegistryPostProcessors中处理的

  • 在myabtis-spring-boot-autoconfigure包的META-INF/spring.factories中引入了MybatisAutoConfiguration,这样当通过ConfigurationClassParse处理自动装配的功能时,就会将这个类的BeanDefinition注册到容器中

    9.3. Mapper的初始化过程

  • 当Spring容器在初始化一些需要注入Mapper的Bean的时候,由于依赖注入的机制,所以也会实例化Mapper,也就是MapperFactoryBean。因为MapperFactoryBean的父类SqlSessionDaoSupport有一个通过set注入SqlSessionTemplate的方法然后根据PropertyDescriptor这个部分,需要初始化SqlSessionFactoryBean,然后会调用MybatisAutoConfiguration中提供的@Bean注解的sqlsessionFactory方法进行初始化SqlsessionFactory,这里会处理SpringBoot的配置文件中mybatis开头的的配置项信息,然后调用SqlSessionFactoryBean的getObject方法,最后会调用到buildSqlSessionFactory方法,这里会处理所有的myabtis-config相关的东西。

  • 如果此处有设置mapperLocations配置项,那么通过成员变量MapperBuilderAssistant去解析对应路径下的mapper文件,并将所有的SQL(DML\DQL)操作封装为一个个MappedStatement放入到Configuration中,同时也会将mapper文件中namespace指定的接口最终包装为一个个MapperProxyFactory放入Configuration的成员变量MapperRegistry的knownMappers中,同时会对对应的接口用MapperAnnotationBuilder进行mybatis的@Select这类注解的解析,并将没有在mapper文件中的SQL操作将之封装为一个个MappedStatement放入Configuration中
  • MapperBuilderAssistant这个类是用来管理namespace与二级缓存Cache的对应关系的,每个MappedStatement有一个Cache,这个Cache就是MapperBuilderAssistant的成员变量Cache,这样保证一个namespace下的所有statement共享同一个cache,用标签开启。当然可以通过来使得当前的namespace与指定的namespace共用同一个cache。每个Cache有一个cacheId用来唯一标识这个cache,这个id就是namespace
  • 如果mapperLocations不包括某个mapper接口的话,因为它是MapperFactoryBean,最终继承了DaoSupport,这个类实现了InitializingBean接口,在初始化阶段调用invokeInitMethods的时候会执行这个类的afterProperties方法,会调用到MapperFactoryBean的checkDaoConfig方法,最终会对这个mapper接口进行扫描处理,将之封装为MapperProxyFactory放入Configuration的成员变量MapperRegistry的knownMappers中,他的方法封装为MappedStatement放入到Configuration中。然后再调用对应的getObject方法获取真正的代理生成的Mapper对象,从MapperRegistry的knownMappers中获取到对应的MapperProxyFactory,然后调用newInstance方法(这里的InvacationHandler是MapperProxy,后续真正调用mapper的方法的时候是调用这里的invoke回调方法进行处理),进行代理对象的生成,并注入到需要的地方,至此mapper的初始化和注入完成

    9.4. Mapper的处理过程

  • 在调用的时候,会直接调用对应MapperProxy的invoke方法,会判断方法有没有对应的方法缓存,没有缓存的话,会实例化PlainMethodInvoker。

  • 在实例化这个类的时候需要传入一个MapperMethod对象,这里会将对应的接口、方法、Configuration信息封装为一个MapperMethod对象,用来实例化PlainMethodInvoker。
  • 然后调用PlainMethodInvoker的invoke方法,最终会调用ampperMethod的execute方法,参数是sqlsession实际最后是交给SqlSessionTemplate去执行对应的crud操作。
  • 然后会调用SqlSessionTemplate方法的sessionFactory.openSession获取连接,在获取连接的时候创建一个Executor,默认是SimpleExecutor,如果设置了cacheEnabled=true的话,那么会包装成一个CachingExecutor,如果有插件的话,在这里会对这个Executor进行层层代理。
  • 然后会将Configuration、Executor封装为一个DefaultSqlSession返回,然后调用这个类的curd方法,最终的所有方法都是由它的Executor执行的。
  • 如果开启二级缓存也就是mapper中有了标签或者使用了的配置,用的是CachingExecutor执行crud操作,二级缓存有,就从二级缓存取,如果二级缓存没有,就从一级缓存取,如果一级缓存没有就查询数据库,然后放入一级再放入二级,如果配置项local-cache-scope=statement那么会清理掉一级缓存
  • 如果没有耳机缓存,那么执行上面的后半部分关于一级缓存的操作

    9.5. Mybatis的交由Spring管理的事务流程

  • 对被@Transactional标注的方法的类,Spring会生成一个代理类,当执行对应的方法的时候,会在调用DataSourceTransactionManager的doBegin方法,在这里会通过DataSource获取到数据库连接对象Connection,并且会将之放入当前线程的ThreadLocal中,代码如下:

    1. TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder())
  • 然后在Mybatis通过Executor获取连接的时候,会通过SpringManagedTransaction这个类的openConnection方法调用到DataSourceUtils.getConnection(this.dataSource);方法获取到已经放入ThreadLocal中的Connection,这样Mybatis拿到的就是Spring已经准备好的数据库连接,后续的事务操作被Spring接管

image.png

9.6. Mybatis的问题

9.6.1. 一级缓存问题

  • Mybatis的一级缓存由配置项local-cache-scope控制,默认值是session,也就是一个数据库连接会话。所以一级缓存要生效需要依赖于Spring的事务,这样才能保证整个方法的所有SQL操作都在一个session中,从而使得后面的查询可以用到前面的结果。如果有更新操作,那么会清理掉一级缓存
  • 但是这样会导致一个问题:假设有两个线程,线程1做下面操作

    1. select * from user where id = 1; //1
    2. //do something
    3. select * from user where id = 1; //2

    如果这个线程在进行第一步和第二步之间的其他操作时,其他线程执行了

    1. update user set user_name = 'aaa' where id = 1;

    这样的更新操作,但是对于线程1而言,它是感知不到的,因为它已经把数据缓存到了本地了,这样就会出现脏数据的情况,所以为了解决这个问题,需要将local-cache-scope设置为statement,也就是弃用一级缓存

    9.6.2. 二级缓存问题

  • 二级缓存需要在Mapper.xml文件中使用开启这个Mapper对应的namespace使用一个cache来缓存数据;同时还需要设置cache-enable=true

  • 开启了二级缓存之后,那么对于这个namespace下的所有查询操作,Mybatis都会缓存到本地,更新操作会清理掉对应的缓存。但是如果其他的namespace中对这个namespace中缓存的数据进行了修改,那么这个namespace是感知不到的
  • 所以为了解决这个问题,可以使用将让多个namespace共享一个cache,这样当有更新的时候就会清理掉对应的旧数据
  • 然而,如果服务有多个节点的话,那么一个节点的更新,其他节点也是无法感知到的,所以在现在的分布式环境下,二级缓存也是需要关闭的,就是在配置文件中设置cache-enable=false
  • 或者可以将其他的缓存中间件来作为mybatis的缓存使用