Mybatis入门到源码分析
Valentine 转载请标明出处。
Mybatis介绍以及使用
MyBatis is a first class persistence framework with support for custom SQL, stored procedures and advanced mappings.MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results.MyBatis can use simple XML or Annotations for configuration and map primitives, Map interfaces and Java POJOs (Plain Old Java Objects) to database records. (摘抄自官网)
MyBatis 是一个持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式Java对象)为数据库中的记录。
对比JDBC和MyBatis
对比JDBC和MyBatis对比原生jdbc和使用mybatis,使用mybatis帮我们映射POJO对应数据库的表字段,生成执行的sql语句StatementHandler,设置传参ParamterHandler,设置返回结果ResultSetHandler。Mybatis允许自定义plugin来实现对这四个类的拦截,类似AOP,xxxPlugin implements Interceptor。
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
使用Mybatis
1、编程式
2、集成式managed 集成到spring
3、工作当中的使用方式,分析业务,定义表结构,generator生成需要的类
generator使用步骤
1.pom.xml 配置generator插件
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.3</version>
<configuration>
<configurationFile>${project.basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
</configuration>
</plugin>
2.配置 generatorConfig.xml
3.执行 mvn mybatis-generator:generate
4.生成Bean和Example
4、作用域SCOPE 生命周期 ——————scope
SqlSessionFactoryBuilder ——————method
SqlSessionFactiory ——————application
SqlSesssion ——————request/method
Mapper method ——————method
5、Mapper的xml与annotation形式
兼容形式:互补,但是两个同时存在会报错
Mapper.xml
Pros:跟接口分离、统一管理
Cons:过多的xml文件
Annotation
Pros:接口能看到sql可读性高,不需要再去找xml文件,方便
Cons:复杂的联合查询不好维护,代码可读性差
6、Config文件部分解读 http://www.mybatis.org/mybatis-3/configuration.html
-
Environment
配置数据库环境 - TypeHandler (java和表字段类型的转换实现)
a) 定义com.gupao.dal.typehandlers.TestTypeHandle
b) 注册com.gupao.dal.config.MybatisConfig#localSessionFactoryBean
c) 注册到使用字段上
7、Plugins
拦截范围
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
a) 定义 com.gupao.dal.plugins.TestPlugin
b) 注册 com.gupao.dal.config.MybatisConfig#localSessionFactoryBean
c) 使用
配置文件解读
mapper文件解读
1.namespace
关联到接口方法,区分类似package的作用
2.resultMap/resultType
resultType
Pros:多表关联字段是清楚知道的,性能调优直观 Cros:创建很多实体
resultMap Pros:不需要写join语句 Cros:N+1问题,性能调优不直观
-
select insert update delete CRUD
动态SQL http://www.mybatis.org/mybatis-3/dynamic-sql.html
缓存
a) 一级缓存 b) 二级缓存
1、为什么要一级缓存?
好处:减少数据库压力
2、怎么验证一级缓存?
3、一级缓存有没有问题?
public void query(){
SqlSession sqlSession;
sqlSession.selectOne();
期间被update下面的数据就是脏数据
sqlSession.selectOne();//命中缓存,内存
}
为什么这样设计?
因为你会这样写代码的概率近乎为0。
Best practice
1.分页
a)逻辑分页 org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForSimpleResultMap 内存里分页
b) 物理分页 i. select … limit 0,10;
分页插件 https://github.com/pagehelper/Mybatis-PageHelper
批量操作Batch
for循环一个一个插入 性能低
foreach拼sql(性能最高,推荐使用) 性能高 有sql长度限制,定好List大小,因为数据库有最大传输packet的限制,就像发送TCP包一样,一个包有最大的限制,show variables like '%packet%';,查出max_allowed_packet本地默认最大传输包4M,slave_max_allowed_packet 从库最大传输包 1G;还有一个就是它会基于网络的buffer,show variables like '%net_buffer%';默认是16K。
mybatis的整个架构
Mybatis的logging模块(这里就挑选了一个比较有意思的模块分析一下)
mybatis的log,把我们一次请求的各个部分都拆分了,ConnectionLogger、PreparedStatementLogger、ResultSetLogger、StatementLogger,各部分有各部分的日志输出,Executor通过动态代理来执行sql的日志输出,如果是isDebugEnabled就这样打日志。
Mybatis实现一个查询的时序图
问题
1、 MyBatis在Spring集成下没有mapper的xml文件会不会报错,为什么?
如果有@Annotation,没有xml不会报错,如果两个都没有就会报错,因为spring解释的时候就把它catch住了。
查看@Select注解,反推org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#loadXmlResource类,里面查看
2、Mapper在spring管理下其实是单例,为什么可以是一个单例?
因为Mapper的主要作用是寻找sql
3、手写Plugin,多个interceptor到底谁先执行?顺序由谁决定的?
sqlSessionFactoryBean.setPlugins(new Interceptor[]{pageInterceptor()});这里setPlugins传入的是数组,查看源码可知道,
org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement是for循环configuration.addInterceptor(plugin);添加插件的,所以是按照添加顺序执行的。
4、怎么验证一级缓存的存在?
使用sqlSessionTemplate.getSqlSessionFactory().openSession(),获取sqlSession来操作
public void select() {
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession();
long start = System.currentTimeMillis();
sqlSession.selectOne("selectSOne");
System.out.println("cost "+ (System.currentTimeMillis() - start));
start = System.currentTimeMillis();
sqlSession.selectOne("selectSOne");
System.out.println("cost "+ (System.currentTimeMillis() - start));
}
5、为什么Mybatis的动态代理和正宗的动态代理不一样?
使用动态代理就要实现InvocationHandler接口,ProxyDemoProxy要实现InvocationHandler接口,还要持有ProxyImpl的实例,被代理的是IProxyInterface。
但是mybatis的动态代理不是正宗的动态代理,
被代理的是xxxMapper,动态代理是MapperProxy实现了InvocationHandler接口,MapperProxy是持有mapperInterface的,但是mapperInterface的实现去哪了?
Mybatis的动态代理
org.apache.ibatis.binding.MapperProxy@136aa0c 代理
com.gupaoedu.mybatis.mapper.TestMapper 被代理
正宗的动态代理
com.gupaoedu.mybatis.proxymy.ProxyInterfaceImpl
com.gupaoedu.mybatis.proxymy.ProxyInterfaceImpl@16d8735
至少正宗的长的还是差不多的,那究竟为什么mybatis的不一样呢?MapperProxy为什么可以不需要impl呢?
因为Mapper只需要找到对应的sql就可以了,所以可以不需要impl,而且MapperProxy的MapperInterface是从Configuration传进来的。
6、MyBatis为什么还使用switch...case的语句,还符合开闭原则吗?
对拓展开放,对修改关闭,为什么mybatis里面会存在很多像如下那样的代码,因为这些代码是不变的,增删改查就那几个操作。用策略模式去拓展的情况下,原有测试过的代码是不需要变化的,只需要增加新的代码就能完成功能了,这就是开闭原则。
org.apache.ibatis.binding.MapperMethod.execute() {
switch (command.getType()) {
case INSERT:...
case UPDATE:...
case DELETE:...
case SELECT:...
case FLUSH:...
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
}
在接口层面定义动作,在抽象类层面定义共性的方法。基于模板模式,符合开闭原则。为什么CachingExecutor不是抽象类,因为它不会变的,如果它是会变的,就把CachingExecutor变成AbstractCachingExecutor
7、Mybatis是怎么实现错误日志输出的?
mybatis就使用到了ErrorContext,因为它是ThreadLocal的,而每个sqlSession也是线程级别的,如有某个sql执行异常,即直接通过threadLocal输出,这样做更加方便开发人员排错。
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
8、MapperProxy的invoke方法里面为什么有第50行这样的代码呢?
if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} 这样一行代码的判断呢?
因为Object有toString()等方法,mapper也有,所以如果mapper运用toString()等方法的时候,就会走这个逻辑。(没有就会报错)
9、 TestMapper 作者为什么要设计这样的形式来做?为什么不是一个class而是一个interface?
1.因为它最终的目的是找到xxxMapper.xml对应的id的sql的;
2.如果有人没有用过mybatis的话,他们可能往xxximpl里面填充jdbc的代码。
3.本来就是做一个代理,而且是一个阉割版的代理,阉割版的代理的话,这个方法也不是太合适。
10、org.apache.ibatis.executor.BaseExecutor#queryFromDatabase 322行这行代码的意义
用来做延迟加载的,如果get出来的value不等于空且不等于这个占位符,嵌套查询的时候的延迟加载就可以加载
org.apache.ibatis.executor.BaseExecutor.DeferredLoad#canLoad
public boolean canLoad() {
return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
}
org.apache.ibatis.executor.BaseExecutor#deferLoad 延迟加载
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getNestedQueryMappingValue 嵌套查询
org.apache.ibatis.executor.CachingExecutor#deferLoad 延迟加载
11、MyBatis的plugin实现机制
只有初始化这四个类(Executor、ParameterHandler、ResultSetHandler、StatementHandler)的时候才会需要去plugin,每一次构造这四个类的时候都需要plugin。
最终有用到Executor的query方法和ParameterHandler的getParameterObject方法,都会被拦截,加上你写的代码,用作更加完整的日志输出。
org.apache.ibatis.session.Configuration#
newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
org.apache.ibatis.plugin.InterceptorChain#pluginAll
org.apache.ibatis.plugin.Plugin#wrap
被代理的是Exector但是塞进去的是Plugin,那Plugin必然实现InvocationHandle,最终返回代理对象CachingExecutor注意这里是代理再次被代理,CachingExecutor持有SimpleExecutor委派SimpleExecutor做事。
12、lazy loading 是怎么做到的?
org.apache.ibatis.executor.resultset.DefaultResultSetHandler
#createResultObject(org.apache.ibatis.executor.resultset.ResultSetWrapper,org.apache.ibatis.mapping.ResultMap,
org.apache.ibatis.executor.loader.ResultLoaderMap,java.lang.String)
// 是嵌套查询而且是lazy的情况下,才会懒加载
配置的时候这样配置,传了一个cglib的代理工厂
factory.getConfiguration().setLazyLoadingEnabled(true);
factory.getConfiguration().setAggressiveLazyLoading(false);
factory.getConfiguration().setProxyFactory(new CglibProxyFactory());
13、spring集成Mybatis的原理:
通过 progamming(编程式)形式往managed(集成式)的形式去迁移, programming ---> managed
spring使用SqlSessionFactoryBean帮我们构造SqlSessionFactory,当你每次使用的时候,它都会帮你去做sqlSessionTemplate,然后这个sqlSessionTemplate会帮你去集成,拿到SqlSession
14、Spring怎么集成Mybatis?
两种方式:xml、annotation
annotation形式:
MapperScannerRegistrar 实现 ImportBeanDefinitionRegistrar
spring官方的解释定义一个ImportBeanDefinitionRegistrar的实现类,然后在有@Configuration注解的配置类上使用@Import导入,这样被@MapperScan扫到的Mapper接口就会加载到spring的IOC容器中去。
MapperScannerRegistrar 实现 ImportBeanDefinitionRegistrar,xxxRegistrar符合COC(convention over configuration 约定优于配置)原则。
判断条件就是接口而且是独立的,就会把它扫进BeanDefinitions
org.mybatis.spring.mapper.ClassPathMapperScanner#isCandidateComponent
XML形式:
MapperScannerConfigurer 实现 BeanDefinitionRegistryPostProcessor
也是将bean动态注册到Spring中BeanDefinitionRegistryPostProcessor中定义的postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)方法 可以让我们实现自定义的注册bean定义的逻辑。
15、Spring集成SqlSession的方式?
mapper 在mybatis里面是方法级别的,在Spring里面是容器级别的。
集成容器的时候还是mapperProxy,变化的是spring里面用的是SqlSessionTemplate,而原生mybatis使用的是DefaultSqlSessiond,不过最终SqlSessionTemplate也是调用DefaultSqlSession。
SqlSessionTemplate的sqlSessionProxy是我们自己拿到的最纯正的SqlSession
代理是用org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor,帮我们做了sqlSession.commit(true)
org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke;
org.mybatis.spring.SqlSessionUtils#getSqlSession(...)
其实在spring里面也是一样的,每次都调用了session = sessionFactory.openSession(executorType),它最后的调用,还是用了DefaultSqlSession,SqlSessionTemplate帮我们做了类似与拦截、AOP、事务等等。