Mybatis 的工作流程
1、创建全局配置文件,包含数据库连接信息、Mapper 映射文件路径等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration> <!-- 加载类路径下的属性文件 --> <properties resource="db.properties"/>
<!-- 设置一个默认的连接环境信息 --> <environments default="mysql_developer"> <!-- 连接环境信息,取一个任意唯一的名字 --> <environment id="mysql_developer"> <!-- mybatis使用jdbc事务管理方式 --> <transactionManager type="jdbc"/> <!-- mybatis使用连接池方式来获取连接 --> <dataSource type="pooled"> <!-- 配置与数据库交互的4个必要属性 --> <property name="driver" value="${mysql.driver}"/> <property name="url" value="${mysql.url}"/> <property name="username" value="${mysql.username}"/> <property name="password" value="${mysql.password}"/> </dataSource> </environment>
<!-- 连接环境信息,取一个任意唯一的名字 --> <environment id="oracle_developer"> <!-- mybatis使用jdbc事务管理方式 --> <transactionManager type="jdbc"/> <!-- mybatis使用连接池方式来获取连接 --> <dataSource type="pooled"> <!-- 配置与数据库交互的4个必要属性 --> <property name="driver" value="${oracle.driver}"/> <property name="url" value="${oracle.url}"/> <property name="username" value="${oracle.username}"/> <property name="password" value="${oracle.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="zhongfucheng/StudentMapper.xml"/> </mappers> </configuration>
|
2、读取配置文件,构建全局 SqlSessionFactory
1 2 3 4 5 6 7 8 9 10 11 12 13
| private static SqlSessionFactory sqlSessionFactory; /** * 加载位于src/mybatis.xml配置文件 */ static{ try { Reader reader = Resources.getResourceAsReader("mybatis.xml"); sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } }
|
3、获取当前线程的 SQLSession(事务默认开启)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private static ThreadLocal<SqlSession> threadLocal = new ThreadLocal<SqlSession>(); public static SqlSession getSqlSession(){ //从当前线程中获取SqlSession对象 SqlSession sqlSession = threadLocal.get(); //如果SqlSession对象为空 if(sqlSession == null){ //在SqlSessionFactory非空的情况下,获取SqlSession对象 sqlSession = sqlSessionFactory.openSession(); //将SqlSession对象与当前线程绑定在一起 threadLocal.set(sqlSession); } //返回SqlSession对象 return sqlSession; }
|
4、通过 SQLSession 读取 namdspace 对应的 Mapper 映射文件中的操作编号 id,从而读取并执行 SQL 语句,提交事务,最后关闭连接
- 每个 Mapper 映射文件里都定义了唯一的 namespace 标识
1 2 3 4 5 6 7 8 9 10 11
| try{ //映射文件的命名空间.SQL片段的ID,就可以调用对应的映射文件中的SQL sqlSession.insert("StudentID.add", student); sqlSession.commit(); }catch(Exception e){ e.printStackTrace(); sqlSession.rollback(); throw e; }finally{ MybatisUtil.closeSqlSession(); }
|
Mapper 映射文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!-- namespace属性是名称空间,必须唯一 --> <mapper namespace="StudentID">
<!-- resultMap标签:映射实体与表 type属性:表示实体全路径名 id属性:为实体与表的映射取一个任意的唯一的名字 --> <resultMap type="zhongfucheng.Student" id="studentMap"> <!-- id标签:映射主键属性 result标签:映射非主键属性 property属性:实体的属性名 column属性:表的字段名 --> <id property="id" column="id"/> <result property="name" column="name"/> <result property="sal" column="sal"/> </resultMap>
<insert id="add" parameterType="zhongfucheng.Student"> INSERT INTO ZHONGFUCHENG.STUDENTS (ID, NAME, SAL) VALUES (#{id},#{name},#{sal}); </insert>
</mapper>
|
关闭连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| /** * 关闭SqlSession与当前线程分开 */ public static void closeSqlSession(){ //从当前线程中获取SqlSession对象 SqlSession sqlSession = threadLocal.get(); //如果SqlSession对象非空 if(sqlSession != null){ //关闭SqlSession对象 sqlSession.close(); //分开当前线程与SqlSession对象的关系,目的是让GC尽早回收 threadLocal.remove(); } }
|
延迟加载
如果查询单表就可以满足需求,一开始先查询单表,当需要关联信息时,再关联查询,当需要关联信息再查询这个叫延迟加载。
延迟加载能提高数据库查询性能,因为单表查询比多表关联查询速度要快。
在全局配置文件中开启懒加载
1 2 3 4 5 6
| <settings> <!-- 延迟加载总开关,默认false --> <setting name="lazyLoadingEnabled" value="true" /> <!-- 设置按需加载,默认true --> <setting name="aggressiveLazyLoading" value="false" /> </settings>
|
在 Mybatis 中延迟加载就是在 resultMap 中配置具体的延迟加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <!-- 一对一查询延迟加载 的配置 --> <resultMap type="orders" id="orderCustomLazyLoading"> <!-- 完成了订单信息的映射配置 --> <!-- id:订单关联用户查询的唯 一 标识 --> <id column="id" property="id" /> <result column="user_id" property="userId" /> <result column="number" property="number" /> <result column="createtime" property="createtime" /> <result column="note" property="note" /> <!--
配置用户信息的延迟加载 select:延迟加载执行的sql所在的statement的id,如果不在同一个namespace需要加namespace sql:根据用户id查询用户信息【column就是参数】 column:关联查询的列 property:将关联查询的用户信息设置到Orders的哪个属性 -->
<!--当需要user数据的时候,它就会把column所指定的user_id传递过去给cn.itcast.mybatis.mapper.UserMapper.findUserById作为参数来查询数据--> <association property="user" select="cn.itcast.mybatis.mapper.UserMapper.findUserById" column="user_id"></association>
<!-- 在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略 局部的加载策略的优先级高于全局的加载策略 fetchType="lazy" 懒加载策略 fetchType="eager" ⽴即加载策略 --> <collection property="orderList" ofType="order" column="id" select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy"> </collection>
</resultMap>
|
懒加载基本原理:
使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象
当调⽤代理对象的延迟加载属性的 getting ⽅法时,拦截器发现需要延迟加载时,那么就会执行事先保存好的查询关联懒加载对象的 SQL,并赋值给代理对象
Mapper 接口的工作原理
通常一个 Mapper 映射文件都会写一个 Mapper 接口与之对应,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement。因此 Mapper 接口的方法不能重载
SqlSession 执行 sql 时会使用 JDK 动态代理为 Mapper 接口生成代理 proxy 对象,代理对象 proxy 会拦截接口方法,转而执行 MappedStatement 所代表的 sql,然后将 sql 执行结果返回。
1 2 3
| SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<User> byPaging = userMapper.selectUser();
|
Mybatis 四大组件
Mybatis 插件
mybatis 在一次 sqlSession 中对持久层的操作,底层依赖四大组件,分别是:
- ParameterHandler 参数处理器
- StatementHandler SQL 语法构建器
- Executor 执行器
- ResultSetHandler 结果集处理器
mybatis 提供插件机制,允许业务自定义插件,基于 JDK 动态代理对四大组件做代理,在持久化过程中嵌入定制逻辑。mybatis 插件的基本原理是:
- 实现自定义插件(需要实现 org.apache.ibatis.plugin.Interceptor 接口)
- mybatis 启动过程中插件注册给 SqlSessionFactory 的 InterceptorChain
- 注册方式 1:在 SqlSessionFactory 读取的全局配置文件中添加分页插件
- 注册方式 2:配置 SqlSessionFactory 的 Bean 时进行添加。可以在@Bean 或 xml 配置里直接添加
1 2 3 4 5 6 7 8
| <plugins> <!-- com.github.pagehelper为PageHelper类所在包名 --> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 使用MySQL方言的分页 --> <property name="helperDialect" value="sqlserver"/><!--如果使用mysql,这里value为mysql--> <property name="pageSizeZero" value="true"/> </plugin> </plugins>
|
- 执行 SQL 的不同阶段,会创建对应的四大组件,创建组件时会遍历 InterceptorChain 里所有的 mybatis 插件,如果插件匹配这个组件,就用 JDK 动态代理包装组件
- 创建插件时通过注解指定要匹配的组件类型、方法名和方法参数,如果组件存在对应的方法,则插件和组件匹配
- 如果组件被动态代理,调用方法前会判断该方法是否在插件的注解里配置了,如果是,先执行插件的增强逻辑,再执行方法
1 2 3 4 5 6 7 8 9 10 11 12
| @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 { // 省略 }
|
分页插件
分页插件是 mybatis 自己提供的,提供便捷的分页查询能力
分页插件的实现类:com.github.pagehelper.PageInterceptor
分页插件的使用:
- 通过工具类 PageHelper 将分页参数存到 threadLocal
- Executor 组件执行 query 方法时,执行 PageInterceptor 的增强逻辑,重写 sql,添加从 threadLocal 拿到的分页参数
1
| PageHelper.startPage(1, 10);
|
分页插件推荐使用 spring-boot-starter 的方式注册,直接引入依赖
1 2 3 4 5
| <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version> </dependency>
|
它会帮你间接引入 mybatis 框架和分页插件所需要的依赖,然后在配置类中直接给应用中的 SqlSessionFactory bean 注册 PageInterceptor 插件
Mybatis 缓存
Mybatis 针对相同的查询 sql,提供了缓存机制。
一级缓存
一级缓存:SqlSession 级别,同一个 SqlSession 中的相同 sql 会命中一级缓存。默认开启。关闭的方法是:设置 local-cache-scope 的值为 statement(默认 session)
- 由于使用了数据库连接池,默认是每次执行完 sql 后自动 commit 并关闭 SqlSession,这就导致两次查询不是同一个 SqlSession,所以一级缓存其实是失效的。为了使得一级缓存生效,需要把这些 sql 操作放在同一个事务里
- 只要发生了写操作,都会将作用域中的所有缓存全部清空!与写操作是否操作的是已缓存记录毫无关系!所以如果是作用域内频繁执行写操作,那么缓存基本没效果
- 因为一级缓存只能感知到 Session 内的写操作,所以容易缓存脏数据,生产环境建议设置为 statement 级别来关闭一级缓存
二级缓存
二级缓存:Mapper 级别,需要在 Mapper 映射文件中增加标签:<cache></cache>
才能生效
- 对于一些写操作频繁的数据的查询操作,可以单独禁用查询语句的二级缓存功能,在 mapper 映射文件的 select 查询中设置
useCache="false"
- 如果涉及到多表级联查询,想要二级缓存生效,需要两个 mapper 映射文件都开启缓存,且通过 cache-ref 标签建连关联,制造一个更大范围的二级缓存。否则关联查询的表如果更新了,缓存不会失效
- 只要发生了写操作,都会将作用域中的所有缓存全部清空!与写操作是否操作的是已缓存记录毫无关系!所以如果是作用域内频繁执行写操作,那么缓存基本没效果
- 在分布式环境中,二级缓存因为只缓存在本地,所以依然容易缓存脏数据,不建议开启
缓存优先级:二级缓存 > 一级缓存 > 数据库
主动刷新缓存
因为缓存作用域内的写操作导致全部缓存失效的机制,所以如果写频繁的数据,使用二级缓存其实作用不大。但如果对于查询数据的时效性要求不高,二级缓存就有用武之地,这里推荐使用它的主动刷新缓存机制
1、mapper 映射文件中的写操作 sql,把刷新缓存开关关闭(默认开启)。flushCache="false"
2、mapper 映射文件配置主动刷新缓存。<cache eviction="FIFO" flushInterval="1000 size="512" readOnly="true""></cache>
- flushInterval:刷新间隔,单位毫秒
- size:缓存队列大小,默认 1024
- eviction:缓存在队列空间不够时的淘汰策略。FIFO 即先入先出,常用的还有 LRU 最近最少使用
- readOnly:true 表示只读,查询到的 DO 对象不能做写操作。底层返回给调用者的只是 DO 对象的引用,因此有性能优势