1

系统学习Mybatis

 1 year ago
source link: https://dcbupt.github.io/2022/06/28/blog_article/%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0%E7%B3%BB%E5%88%97/%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0Mybatis/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Mybatis 的工作流程

1、创建全局配置文件,包含数据库连接信息、Mapper 映射文件路径等

<?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

   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(事务默认开启)

   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 标识
try{
//映射文件的命名空间.SQL片段的ID,就可以调用对应的映射文件中的SQL
sqlSession.insert("StudentID.add", student);
sqlSession.commit();
}catch(Exception e){
e.printStackTrace();
sqlSession.rollback();
throw e;
}finally{
MybatisUtil.closeSqlSession();
}

Mapper 映射文件:

<!-- 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>

关闭连接:

   /**
* 关闭SqlSession与当前线程分开
*/
public static void closeSqlSession(){
//从当前线程中获取SqlSession对象
SqlSession sqlSession = threadLocal.get();
//如果SqlSession对象非空
if(sqlSession != null){
//关闭SqlSession对象
sqlSession.close();
//分开当前线程与SqlSession对象的关系,目的是让GC尽早回收
threadLocal.remove();
}
}

如果查询单表就可以满足需求,一开始先查询单表,当需要关联信息时,再关联查询,当需要关联信息再查询这个叫延迟加载。
延迟加载能提高数据库查询性能,因为单表查询比多表关联查询速度要快。

在全局配置文件中开启懒加载

   <settings>
<!-- 延迟加载总开关,默认false -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 设置按需加载,默认true -->
<setting name="aggressiveLazyLoading" value="false" />
</settings>

在 Mybatis 中延迟加载就是在 resultMap 中配置具体的延迟加载

   <!-- 一对一查询延迟加载 的配置 -->
<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 执行结果返回。

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 配置里直接添加
<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 动态代理包装组件
    • 创建插件时通过注解指定要匹配的组件类型、方法名和方法参数,如果组件存在对应的方法,则插件和组件匹配
  • 如果组件被动态代理,调用方法前会判断该方法是否在插件的注解里配置了,如果是,先执行插件的增强逻辑,再执行方法
@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 拿到的分页参数
PageHelper.startPage(1, 10);

分页插件推荐使用 spring-boot-starter 的方式注册,直接引入依赖

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK