前置知识
ORM
- 持久层框架:JDBC
- ORM(Object Relational Mapping)框架:在Jdbc基础之上的mybatis、hibernate。
什么是ORM:
- 广义上,ORM指的是面向对象的对象模型和关系型数据库的数据结构之间的相互转换。
- 狭义上,ORM可以被认为是,基于关系型数据库的数据存储,实现一个虚拟的面向对象的数据访问接口。理想情况下,基于这样一个面向对象的接口,持久化一个OO对象应该不需要要了解任何关系型数据库存储数据的实现细节。
JDBC的基本流程
- 获取driver对象
- 获取connection对象 建立连接
- 获取statement对象,并且注入sql
- 执行statement对象获取返回值
- 解析返回值
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver"); //反射
conn = DriverManager.getConnection("jdbc:mysql:****//:3306/***?useUnicode=true&characterEncoding=utf8", "root", "tvu1p2ack3");
String sql = "select id,age from user where id>?";
ps = conn.prepareStatement(sql);
ps.setObject(1, 0); //第一个"问号",传入2. -->把id大于2的记录都取出来
rs = ps.executeQuery();
while (rs.next()) {//rs.next()指向第一条
//传入的参数是列索引-->数据库中的第1列,第2列,第3列
System.out.println(rs.getInt("id") + "---" + rs.getInt("age"));
}
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
mybatis
1. 调用过程
如何加载配置
SqlSessionFactoryBean实现了InitializingBean、FactoryBean,在spring初始化的时候会进行初始化,调用afterPropertiesSet方法,这里面会buildSqlSessionFactory。在build的过程中,会对mybatis的配置文件、mapper.xml文件进行解析(XMLConfigBuilder解析),,包括读取配置文件,扫描mapper接口、以及xml文件、插件(扫描了的话加入到拦截器).
- 将mapper.xml中对应的select/update/delete/insert节点,包装成mappedStatement对象,然后把这个类对象,存方到了一个map中(==mappedStatements==),key值是namespace的value+节点的id,value就是mappedStatement;解析完成之后,还会对Mapper接口中的方法进行解析,并将每个方法的全限定类名作为key存入存入Configuration中的mappedStatements属性。所以最后同一个value会存储两次。
- ==mapperRegister中knownMappers==。过反射,根据当前namespace,获取到一个class,将class存到了另外一个map中,knownMappers中,key值是通过反射生成的class对象,value是根据class,生成的MapperProxyFactory对象,这两个map,在执行查询的时候,有用到。
如何从service到mapper
@MapperScan,这个注解使用了@Import,引入了mapperScannerRegistrar,mapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,在registerBeanDefinitions方法中,会对mapperScan注解声明的包,进行扫描,加入到beanDefinitionMap中。
==MapperFactoryBean 实现了FactoryBean、InitializingBean==
那mapperFactoryBean的getObject()方法是在什么时候执行呢?假如说我们在service中注入了mybatis的dao接口,在实例化service的时候,会进行属性注入,属性注入的时候,会发现,service中需要注入dao,这时候,会调用getBean()从spring容器中获取,如果dao,没有实例化,就会去实例化,在实例化完成之后,放到单实例池中,然后,会调用getObject()。在调用getObject的时候,会通过jdk动态代理来生成一个==代理对象==
这里生成代理对象和原生mybatis生成代理对象有一个区别,就是这里的sqlSession,原生的,用的是DefaultSqlSession,mybatis和spring整合之后,这里用的是==sqlSessionTemplate==,那为什么会是sqlSessionTemplate呢?
在实际调用目标方法的时候,会被==mapperProxy的invoke方法拦截==,和原生mybatis不同的时候,在判断是select还是update,之后,原生的mybatis会调用sqlSession.selectOne();和spring整合之后,会调用==SqlSessionTemplate.selectOne()==,然后再调用==内置拦截器sqlSessionInterceptor.invoke()==方法.
如何操作数据库
两种调用方式
-
List list = sqlSession.selectList("com.springsource.study.mybatis.OperChannelMapper.selectAll"); 我们先说这种方法来请求SQL,这里就是调用DefaultSqlSession的selectList()方法,首先会根据如此那终端额全类名+方法名,从mappedStatements这个map中获取到mappedStatement对象,这也就是为什么namespace一定要和接口的包名+类名一直的原因。获取到mappedStatement对象之后,就是先查询缓存,缓存中没有就从数据库查询,数据库查到之后,存到一级缓存中。
-
springboot 默认使用这种 OperChannelMapper operChannelMapper = sqlSession.getMapper(OperChannelMapper.class); 这里的入参是mapperInterface.class,会根据入参,从上面提到的knownMappers中获取到mapperProxyFactory对象,然后再调用mapperProxyFactory.newInstance()方法来生成代理对象
参数映射
BaseTypeHandler是一个抽象类,setNonNullParameter并没有实现,都是交给子类去实现,而每一个子类就是对应了数据库的一种类型。可以看到String里面调用了jdbc中的setString方法,而如果是int也会调用setInt方法。这些子类就是系统默认提供的一些typeHandler。也可以自定义typeHandler。
正是因为MyBatis中默认提供了常用数据类型的映射,所以我们写Sql的时候才可以省略参数映射关系,可以直接采用下面的方式,系统可以根据我们参数的类型,自动选择合适的typeHander进行映射:
结果集映射
ResultHandler
2. 一级缓存和二级缓存
一级缓存
Sqlsession 有sqlsessionFactory->configuration->有mappedStatedMent,resource、environment。
存储位置
DefaultSqlsession有两个属性configuration(全局)和Executor, 子类BaseExecutor里的PerpetualCache,内部维护了一个map。一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。 只有同一个会话才会使用到一级缓存,比如在同一个事务内就是一个会话,不同事务就是不同的会话。
一级缓存的生命周期有多长?
- MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
- SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;
SqlSession 一级缓存的工作流程:
- 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果
- 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
- 如果命中,则直接将缓存结果返回;
- 如果没命中:去数据库中查询数据,得到查询结果;将key和查询到的结果分别作为key,value对存储到Cache中;将查询结果返回;
一级缓存的不足:
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:
- session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中。
- statement 级别的缓存,避坑: 为了避免这个问题,可以将一级缓存的级别设为 statement 级别的,这样每次查询结束都会清掉一级缓存。
二级缓存
概念定义
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是==namespace== 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。
实际上MyBatis 用了一个==装饰器==的类来维护,就是==CachingExecutor==。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如PerpetualCache 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
==只有事务提交二级缓存才会写入==
==CachingExecutor内置了一个TransactionalCacheManager来管理cache==
开启二级缓存
application.yml或者mybatis-config.xml文件中配置cache-enabled=true
mybatis:
# 二级缓存开关 默认关闭
cache-enabled: true
***Mapper.xml文件中配置
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
不足之处
分布式环境多节点下,脏数据产生的可能性很大。比如A节点读取了脏数据在二级缓存,B节点修改了数据,这时候A节点上就会产生脏数据(除非没有增删改、间隔刷新、Lru等缓存过期策略清除)。缓存的不能所有节点共享。
3. 重要源码
- org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory
- org.mybatis.spring.SqlSessionTemplate#getConfiguration
- org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke
- org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
- org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)