1. 半ORM框架
- Mybatis属于半ORM,因为SQL语句需要自己写
- 与其他比较标准的ORM 框架(比如 Hibernate )不同,Mybatis并没有将Java对象与数据库关联起来,而是将Java方法与SQL语句关联起来,Mybatis允许用户充分利用数据库的各种功能,例如存储、视图、各种复杂的查询以及某些数据库的专有特性
自己写SQL语句的好处是,可以根据自己的需求,写出最优的SQL语句。灵活性高。但是,由于是自己写SQL语句,导致平台可移植性不高
9. 与SpringBoot的整合
9.1. 自动装配的类
依赖于SpringBoot的自动装配功能
@MapperScan注解引入了@Import(MapperScannerRegistrar.class)
MapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar接口,所以在ConfigurationClassReader的loadBeanDefinitionsFromRegistrars方法中会调用这个类的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中,代码如下:
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder())
然后在Mybatis通过Executor获取连接的时候,会通过SpringManagedTransaction这个类的openConnection方法调用到DataSourceUtils.getConnection(this.dataSource);方法获取到已经放入ThreadLocal中的Connection,这样Mybatis拿到的就是Spring已经准备好的数据库连接,后续的事务操作被Spring接管
9.6. Mybatis的问题
9.6.1. 一级缓存问题
- Mybatis的一级缓存由配置项local-cache-scope控制,默认值是session,也就是一个数据库连接会话。所以一级缓存要生效需要依赖于Spring的事务,这样才能保证整个方法的所有SQL操作都在一个session中,从而使得后面的查询可以用到前面的结果。如果有更新操作,那么会清理掉一级缓存
但是这样会导致一个问题:假设有两个线程,线程1做下面操作
select * from user where id = 1; //1
//do something
select * from user where id = 1; //2
如果这个线程在进行第一步和第二步之间的其他操作时,其他线程执行了
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的缓存使用