dcddc

西米大人的博客

0%

系统学习Mybatis

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 对象的引用,因此有性能优势