disgare 的博客
首页
博客
分类
标签
首页
博客
分类
标签
  • 网络

    • 计算机网络学习笔记
    • 网络安全相关
    • 域名和子网掩码
    • CORS 跨域资源共享
    • DNS、HTTP 与 HTTPS
    • Server-Sent Events (SSE)
    • WebSocket 长连接
  • 计算机基础

    • 操作系统 IO 相关知识
    • 操作系统学习笔记
    • 程序的机器级表示
    • 音频文件基础
    • 正则表达式相关概念
    • ffmpeg 的安装以及实现音频切分功能
    • Hex 和 Base64 编码
    • XML 的使用
  • 数据结构与算法

    • 动态规划算法学习笔记
    • 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
    • 集合与数据结构学习笔记
    • 面试常见算法总结
    • 算法导论第二部分排序学习笔记
    • 算法导论第一部分学习笔记
  • Java

    • 对象之间的映射与转换
    • 反射学习笔记
    • 泛型相关概念
    • 关于 boolean 类型的坑
    • 如何使用 lambda 表达式实现排序
    • CompletableFuture 相关用法
    • CompletableFuture 源码浅要阅读
    • FutureTask 源码阅读
    • Guava 常用 API
    • Guava 源码阅读:Multimap 相关
    • Jackson 的各种使用
    • Java 的 Excel 相关操作
    • java 的常见性能问题分析以及出现场景
    • java 基础知识
    • JAVA 枚举的基础和原理
    • Java 图片文件上传下载处理
    • Java 序列化
    • Java 异常
    • Java 语法糖
    • Java 中关于字符串处理的常用方法
    • Java 中强、软、弱、虚引用
    • JAVA 注解小结
    • Java Http 访问框架
    • Java Stream 的使用
    • Java8 新特性
    • netty 学习笔记
    • Scanner 的各种用法
    • Servlet 学习笔记
    • String、StringBuffer、StringBuilder 学习笔记
  • JVM

    • 虚拟机执行子系统
    • JVM 自动内存管理
    • Linux 中 JVM 常用工具以及常见问题解决思路
  • Linux

    • crontab 表达式
    • Linux 常见命令
    • Linux 文件系统
  • 中间件

    • 关于定时任务原理
    • 详解 kafka
    • ES 搜索引擎
    • flink 提交流程
    • Grape-RAG
    • Hadoop 基础原理
  • 多线程

    • 多线程基础学习笔记
    • 简单了解并发集合
    • 如何手写单例
    • 深入理解 java 多线程安全
    • 生产者消费者问题
    • 线程池作用、用法以及原理
    • AQS 组件
    • ThreadLocal 原理以及使用
  • 非关系型数据库

    • Redis 集群
    • Redis 数据结构、对象与数据库
    • Redis 学习笔记
  • 关系型数据库

    • B+ 树的插入、删除和数据页分裂机制
    • MySQL 的 binglog、redolog、undolog
    • MySQL 的记录存储结构、存储引擎与 Buffer Pool
    • MySQL 基本的特性
    • MySQL 开发规范
    • MySQL 事务与锁与 MVCC
    • MySQL 数据类型、字符集相关内容
    • MySQL 索引与索引优化
    • PostgreSQL 更新数据时 HOT优化
    • PostgreSQL 相关用法
  • Python

    • Python 基础语法
    • Python 学习
  • Spring 项目

    • Lombok 的常用注解
    • maven 小结
    • MyBatis 框架的使用
    • MyBatis 重要知识点总结
      • 大体架构
      • Mapper 创建过程
        • SqlSessionFactoryBuilder
        • SqlSessionFactory
        • SqlSession
        • Mapper
      • 一个查询语句的执行过程
        • Dao 接口里的方法,参数不同时,方法能重载吗
        • 总结
      • #{} 与 ${} 的不同
      • 物理分页与逻辑分页
      • 缓存
      • 执行器
      • 插件
      • 延迟加载
    • MybatisPlus 的使用
    • Spring 框架基础使用
    • Spring 事务相关
    • Spring IOC 的原理及源码
    • Spring AOP 的使用和原理
    • SpringBoot 的原理
    • SpringBoot 基础使用
    • SpringWeb 重要知识点
  • 分布式

    • 初步了解 docker
    • 从 ACID 到 BASE 事务处理的实现
    • 访问远程服务
    • 分布式 id
    • 分布式缓存相关问题
    • 分布式集群理论和分布式事务协议
    • 分布式架构的观测
    • 分布式一致性算法
    • 负载均衡 Load Balancing
    • 关于分布式系统 RPC 中高可用功能的实现
    • 集群间数据同步的目的
    • 三高问题下的系统优化
    • 数据库分库分表
    • 详解 Spring Cloud
    • Dubbo 基础概念
    • Gossip 协议
    • nginx 学习笔记
    • Protobuf 通信协议
    • Zookeeper 基础学习
  • 架构设计

    • 参数校验与异常处理
    • 抽象方法与设计模式
    • 代码整洁之道
    • 权限系统设计
    • 用低内存处理大量数据
    • 设计模式——策略模式
    • 设计模式——过滤器模式在 Spring 中的实践
    • 状态模式
    • 统一结果返回
    • 为什么要打日志?怎么打日志?打什么日志?
    • 运维监控常见指标含义
    • 资深研发进阶
    • DDD 架构学习笔记
    • Java 常用的规则引擎
    • MVC 架构学习笔记
  • AI

    • 如何编写 Prompt
    • Agent 工程架构
    • LLM 相关内容
    • NLP 相关知识
    • vibe coding 最佳实践
    • windows 下 ollama 迁移到 D 盘
  • 开发工具

    • 如何画时序图、流程图、状态流转图
    • excel 关于 =vlookup 的用法
    • git 的学习以及使用
    • IDEA 插件推荐
    • IDEA 常用快捷键以及调试
    • Shell 脚本
    • swagger 的使用
  • 前端

    • 简单了解前端页面开发
    • 伪静态是什么
    • GitHub Pages 部署教程
    • Vercel 部署教程
    • vue-admin-template 简单使用
    • VuePress 博客搭建指南
  • 项目

    • 面试刷题网——技术方案
    • 影视资源聚合站——技术方案
  • 问题记录

    • 定时任务单线程消费 redis 中数据导致消费能力不足
    • 提供可传递的易受攻击的依赖项
    • Liteflow 在 SpringBoot 启动时无法注入组件问题 couldn‘t find chain with the id[THEN(NodeComponent)]
  • 金融

    • 股票分析——关于电力
    • 股票技术面——量价关系
    • 股票技术面——盘口
    • 股票技术面——基础
    • 基础的金融知识
    • 基金与股票
    • 韭菜的自我总结
    • 聊聊价值投资
  • 其他

    • 程序员职场工作需要注意什么
    • 创业全链路SOP:从灵光一现到系统化增长的实战指南
    • 观罗翔讲刑法随笔
    • 价格和价值
    • 立直麻将牌效益理论
    • 梅花易数学习笔记
    • 压力管理
2021-08-05
Spring 项目
目录

MyBatis 重要知识点总结

记录一下 MyBatis 的底层原理以及相关重要知识点

# 大体架构

在这里插入图片描述 在接口层中 MyBatis 提供和数据库交互的两种方式:使用传统的 MyBatis 提供的 API 实现简单的增删改查,以及使用 Mapper 接口实现自己写的 sql 语句

数据处理层是 MyBatis 的核心,主要完成三个功能:通过传入参数构建动态 SQL 语句、SQL 语句的执行、封装查询结果集成 List< E >

在参数映射阶段完成了对于 java 数据类型和 jdbc 数据类型之间的转换,随后 MyBatis 通过传入的参数值,使用 Ognl 来动态的构造 SQL 语句

在 Executor 中执行 sql 并且返回结果,并且将返回的结果转化为我们熟悉的 java 类型

数据处理层的大致过程如下图所示: 在这里插入图片描述 重点关注一下这四大对象:

(1)SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。类似于 JDBC 里面的 Connection,在执行时,可以在 MAP 中通过全限定名加接口名称来唯一确定一条 SQL (2)Executor 接口,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。类似于 JDBC 里面的 Statement/PrepareStatement (3)MappedStatement 对象,该对象是对映射 SQL 的封装,用于存储要映射的 SQL 语句的 id、参数等信息 (4)ResultHandler 对象,用于对返回的结果进行处理,最终得到自己想要的数据格式或类型。可以自定义返回类型

# Mapper 创建过程

# SqlSessionFactoryBuilder

一旦创建了工厂就没用了,推荐成为局部变量

一般先使用两个 build 方法,不同点只是处理的是字节流还是字符流

最后都会调用 build 的重载方法,返回一个 DefaultSqlSessionFactory(实现工厂接口的类)

public class SqlSessionFactoryBuilder {
 
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
 
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
    
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
 
}
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

# SqlSessionFactory

因为包含配置文件中的所有信息,可以视作数据库但不是数据库,必须一直存在(因为要创建 sqlsession),建议设为全局变量

Configuration:将 mybatis 配置文件中的信息保存到该类中,之后的大部分操作都会从这个类中拿数据,可以说主体就是这个类,Factory 只是个包装

在这个类之中有一个非常重要的类,MappedStatement,这个类封装了一个增删改查标签,这个类可以由全限定名加方法名唯一确定

SqlSessionFactory 会为每个 SqlSession 会配一个 Executor 执行器,然后返回一个 DefaultSqlSession(SqlSession 的实现类)对象

public class DefaultSqlSessionFactory implements SqlSessionFactory {
 
  private final Configuration configuration;
 
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
	  final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); 
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# SqlSession

连接请求,负责和数据库交互,不是线程安全的,使用完关闭,推荐将 close 方法放在 finally 中,和 ReentrantLock 一样

所有的增删改查操作,都会由 SqlSession 实现类中以下三个方法执行,选择和修改,并且最后都会调用执行器执行sql语句

这样也变向说明了,为什么 executor 执行器通常都会执行选择和修改

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  
  @Override
  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  
  @Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
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

其他方法都是调用以上方法的

    public int delete(String statement) {
        return this.update(statement, (Object)null);
    }
    
    public int insert(String statement, Object parameter) {
        return this.update(statement, parameter);
    }
1
2
3
4
5
6
7

# Mapper

会话中的一个具体的业务,包含多个sql请求,调用getMapper方法获得

    public <T> T getMapper(Class<T> type) {
        return this.configuration.getMapper(type, this);
    }
1
2
3

之后调用MapperRegistry中的方法

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12

所以它最后返回了对应接口的一个代理对象,我们只用这个代理对象实现增删改查方法

# 一个查询语句的执行过程

由于是代理对象,所以直接执行 invoke 方法,即无论用 mapper 中什么方法都会执行 invoke 方法

如果是查询语句,先运行到 SqlSession 中的 select 方法,然后执行 executor 中的 query 方法,executor 只是个接口,根据用户选择的不同会执行不同的 executor,比如:

如果开启二级缓存执行 CachingExecutor,先判断缓存

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null) {
       		//。。。。。。。。
        }
        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
1
2
3
4
5
6
7

一般情况下执行SimpleExecutor中的doQuery,执行一级缓存

    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        List var9;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = this.prepareStatement(handler, ms.getStatementLog());
            var9 = handler.query(stmt, resultHandler);
        } finally {
            this.closeStatement(stmt);
        }
        return var9;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

底层在 executor 中使用 StatementHandler 来执行,这个类可以创建出 Statement 对象,这个类用来执行预编译,设置参数,接受数据等操作,也就是说这个类执行 JDBC 操作(statement 说明)

这个类有两个重要对象

	//接受的数据封装在这个类中
    protected final ResultSetHandler resultSetHandler;
    //为预编译的sql语句设置参数
    protected final ParameterHandler parameterHandler;
1
2
3
4

综上,SqlSession方法中的getMapper传入的是接口的class对象,它的作用是作为类的全限定名配合之后调用的方法名唯一确定一个sql语句(MappedStatement);而它的返回值类型虽然可以调用它里面的方法,但是方法的实现已经被 jdk 完全代理了

# Dao 接口里的方法,参数不同时,方法能重载吗

最佳实践中,通常一个 xml 映射文件,都只会有一个 Dao 接口与之对应

因为接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。 Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement

在 MyBatis 中,每一个 select、insert 等标签,都会被解析为一个 MappedStatement 对象

综上,Dao 接口里的方法可以重载,但是 Mybatis 的 xml 里面的 ID 不允许重复。举个例子:

Person queryById();

Person queryById(@Param("id") Long id);

Person queryById(@Param("id") Long id, @Param("name") String name);
1
2
3
4
5
<select id="queryById" resultMap="PersonMap">
    select
      id, name, age, address
    from person
    <where>
        <if test="id != null">
            id = #{id}
        </if>
        <if test="name != null and name != ''">
            name = #{name}
        </if>
    </where>
    limit 1
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • queryById() 方法执行时,if 标签获取的所有条件值都为 null,所有条件不成立,动态 sql 可以正常执行
  • queryById(1L) 方法执行时,包含了 id 和 param1 两个 key 值。当获取 if 标签中 name 的属性值时,进入 ((Map)parameterObject).get(name) 方法中,map 中 key 不包含 name,所以抛出异常
  • queryById(1L,"1")方法执行时,parameterObject 中包含 id,param1,name,param2 四个 key 值,id 和 name 属性都可以获取到,动态 sql 正常执行

mybatis 虽然允许重载,但是功能非常鸡肋,估计在设计的时候也没有考虑到重载的情况,不推荐使用

# 总结

在这里插入图片描述 上面中流程就是 MyBatis 内部核心流程,每一步流程的详细说明如下文所述:

(1)读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。

(2)加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。

(3)构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。

(4)创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

(5)Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

(6)MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

(7)输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

(8)输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

# #{} 与 ${} 的不同

它们都是占位符(占领一个空间,等待传入其他参数)

#{} 会被自动替换为 ? 号,然后这个sql语句作为值存放在 MappedStatement 里,然后使用 PreparedStatement 的内部参数设置方法对 ?进行填充,整个过程使用反射获取对象属性。这种方法会对语句进行预编译处理,所以防止sql注入(因为sql注入发生在sql编译过程中),推荐使用

${} 是静态字符串替换,在 properties 文件或者 sql 内部使用,直接把传入的字符进行替换,比如 ${driver} 会被静态替换为 com.mysql.jdbc. Driver ,这样的话不防止 sql 注入,不推荐使用

举个例子,比如查询 id 为1的订单详情信息,使用 #{} 传入的话 mysql 是会执行以下语句:

select * from order_detail where id = ?;
1

随后 mybatis 才会将1这个数据传输给 mysql。如果使用 ${} 的话会在 java 程序中进行字符串拼接,因此会执行以下语句

select * from order_detail where id = 1;
1

# 物理分页与逻辑分页

MyBatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,属于逻辑分页,这种分页方式一次读取大量数据,在数据库数据修改之后不能第一时间获取值,并且对数据内部的处理也需要时间,一般不使用 mybatis 自带的分页方式

物理分页是依赖数据库的 limit 进行分页的,我们可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页,因为可以配合 where 使用索引,效率较高

# 缓存

mybatis 的缓存分为两级

一级缓存是自动开启的 sqlsession 会话缓存,通过同一个 sqlsession 查找的数据会被缓存,所有的查询结果都存储在一个以对应语句为值的 Map 中,在 sqlsession 关闭时所有的缓存都被清空或者转到二级缓存中,推荐关了,毕竟在现在分布式的应用中单机缓存也没啥用

二级缓存推荐一直关闭(默认也是开启的),二级缓存是全局缓存,即 sqlsessionfactory 级别的,此后在执行相同的查询语句,会去缓存中找。每个缓存存放在对应的每个namespace中,是用CachingExecutor实现的

# 执行器

调用StatementHander对象发送sql语句

执行器有不同的类型:

SimpleExecutor:执行一条语句就关闭Statement对象 ReuseExecutor:执行一条语句,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用 BatchExecutor:批处理语句,不执行select,批量插入后,能返回数据库主键列表

执行器的活动范围在SqlSession生命周期中(因为它定义在SqlSession中)

# 插件

plugins,在 mybatis 中可以理解成拦截器

主要拦截以下四大对象:

Executor:执行器,它在sqlSession创建时被创建以及绑定 StatementHander:sql语句执行 ParameterHandler:规范,sql语句预编译时设置参数用的 ResultSetHandler:存放结果集

其实就是拦截executor这个丢sql的以及handler这个做翻译的,所有的插件工作原理都是对原来的对象生成代理,代码层面实现是执行所有拦截器链

表层:实现Interceptor (拦截器)接口并复写 intercept()拦截方法,覆写此接口可以自定义插件

里层:执行这 4 种接口对象的方法时,就会进入拦截方法,InvocationHandler 的 invoke() 方法

# 延迟加载

在MyBatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled

延迟加载就是懒加载,在表的关联查询时(比如一对多使用分布查询时),推迟对附表的查询时机,减少数据库访问的压力。又比如在获取某种数据的时候,只有在使用该数据的时候才从数据库中读取数据,这么做的好处是提高用户的访问速度

底层原理是通过拦截器实现,使用invoke方法对原来的sql进行修改

不光是 MyBatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的

#MyBatis
最后更新: 2/23/2026, 9:23:04 AM
MyBatis 框架的使用
MybatisPlus 的使用

← MyBatis 框架的使用 MybatisPlus 的使用→

最近更新
01
vibe coding 最佳实践
02-24
02
立直麻将牌效益理论
02-23
03
伪静态是什么
02-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式