10-Mybatis(13)
1. MyBatis缓存机制
Mybatis 中有两类缓存,分别是一级缓存和二级缓存
1. 一级缓存
一级缓存默认开启,二级缓存默认关闭,若开启,可在 SqlSession 之间共享缓存数据
- 一级缓存默认是会话级缓存。即创建一个 SqlSession 对象就是一个会话,一次会话可能会执行多次相同的查询,这样缓存了之后就能重复利用查询结果,提高性能,不过
commit, rollback, update, delete
等都会清除缓存 - 注意:不同 SqlSession 之间的修改不会影响彼此。eg:SqlSession1 读了数据 A,SqlSession2 将数据改为 B,此时 SqlSession1 再读还是得到 A,出现脏数据问题。所以,多 SqlSession 或分布式环境下,就可能有脏数据的情况发生,建议将一级缓存级别设置为 statement
2. 二级缓存
二级缓存是跨 SqlSession 级别的共享,同一个 namespace 下的所有操作语句,都影响着同一个 Cache
- 二级缓存也会有脏数据的情况,比如多个命名空间进行多表查询,各命名空间之间数据是不共享的,所以存在脏数据的情况
- eg:A、B 两张表进行联表查询,表 A 缓存了这次联表查询的结果,则结果存储在表 A 的 namespace 中,此时如果表 B 的数据更新了,是不会同步到表 A namespace 的缓存中,因此就会导致脏读的产生
- 一般而言 namespace 对应一个 mapper,对应一个表。namespace 对应一个唯一的命名空间,从而可以在不同的映射文件中使用相同的 SQL 语句 ID
- user 可以定义一个 selectById,order 也可以定义一个 selectById,因为命名空间不同,就不会冲突
开启二级缓存之后,会先从二级缓存查找,找不到再去一级缓存查找,如果一级缓存没有再去DB查询
二级缓存主要是利用 CachingExecutor 这个装饰器拦了一道,来看下 CachingExecutor#query
方法:
- MyBatis 的缓存本质上就是在本地利用 map 来存储数据
- 基础实现类是 PerpetualCache,并且使用了装饰器模式,提供了各种各样的 cache 进行功能的扩展
- BlockingCache 可以提供阻塞
- FifoCache
- LruCache
可以看到 mybaits 缓存还是不太安全,在分布式场景下肯定会出现脏数据。建议生产上使用 Redis 结合 SpringCache 进行数据的缓存,或利用 guava、caffeine 进行本地缓存
2. MyBatis执行原理
首先解析配置 mybatis-config.xml
,将配置文件的内容转为 Configuration 对象加载到内存中,包括一些 xxxMapper.xml
文件,将这些文件解析成 MappedStatement
对象,保存其 SQL 语句、参数映射等信息
1. MapperProxy
- Mapper 都是接口,实际的实现类是由 Mybatis 利用 JDK 动态代理生成的,具体是在 MapperProxyFactory 中生成代理类
2. MapperMethod
- 当调用 Mapper 接口中的方法时,代理对象会将调用信息传递给 MyBatis 内部的 MapperProxy 对象,实际上调用方法时,会触发 MapperMethod 来执行具体逻辑
- 里面分了
insert, update, delete
等不同的处理方法,这时候就可以执行具体的语句了
3. SqlCommand
- command 是 MapperMethod 内部的一个属性
- command 就是通过调用的
接口名 + 方法名
,从 Configuration 中找到 MappedStatement 对象,这个对象记录了 SQL 内容、出参入参等,这样就得到最终的 SQL 相关信息了,跟前面的配置串起来了
mybatis 还有缓存机制,因此会经过 CachingExecutor 的处理,这里主要是二级缓存的处理
4. JDBC
- 最终还是会到 JDBC 执行 SQL 的流程,获取 Connection(实际会从连接池中获取),且得到 Statement
- 然后调用 execute 执行(和 JDBC 一样)
- 最后通过 resultSetHandler 处理查询得到的结果,q其映射到对应的实体对象
3. MyBatis、Hibernate区别
1. Hibernate
是一款完整形态的 ORM 框架,可以从纯面向对象的角度来操作 DB
- 它不仅能完成对象模型和 DB 关系模型的映射,还能屏蔽不同 DB 不同 SQL 的差异,根据底层不同 DB 转化成对应的 SQL
- 且也提供了 HQL(Hibernate Query Language),面向对象的查询语句,和 SQL 类似但是不太相同,最终 HQL 会被转化成 SQL 执行
- 还提供了 Criteria 接口,是一套面向对象的 API,一些复杂的 SQL 可以利用 Criteria 来实现,非常灵活
缺点:
- 屏蔽的太多了,无法精细化的控制执行的 SQL。eg:因为自动生成 SQL,所以无法控制具体生成的语句,无法直接决定它走哪个的索引,且又经过了一层转化 SQL 的操作,所以性能也会有所影响
2. Mybaits
Mybatis 相对于 Hibernate 称为半 ORM 框架,因为 Hibernate 不需要写 SQL,而 Mybatis 需要写 SQL
- Mybatis 更加的灵活且轻量
- 能针对 SQL 进行优化,非常灵活地根据条件动态拼接 SQL 等,极端情况下性能占优
缺点:
- Mybatis 和底层的 DB 是强绑定的,如果更换底层 DB , 所有 SQL 需要重新修改一遍
4. #{}、${}区别
#{}
和 ${}
是两种插入参数到 SQL 语句,默认使用 #{}
(为了安全)
#{}
:是参数占位符,用于预处理语句中的参数,它通过 JDBC 的 PreparedStatement 来处理,这意味着参数会在 SQL 执行前被预编译,并通过占位符的形式提供给 SQL 语句(注意这个能力不是 Mybatis 提供的,这个是 MySQL 的预处理能力),可以有效的防止 SQL 注入${}
:用于字符串替换。在动态 SQL 场景,其中表达式的结果会直接替换到 SQL 语句中,并不会进行预处理。这意味着参数会直接嵌入到 SQL 语句里(这个用法有风险,可能会造成 SQL 注入)
1. 动态表名,列名,排序
一些动态 SQL 场景是无法通过预编译替代的,只能使用 ${}
- 动态指定表名、列名
<select id="selectByColumn" resultType="User">
SELECT * FROM users WHERE ${column} = #{value}
</select>
- 动态排序
<select id="selectAllUsers" resultType="User">
SELECT * FROM users ORDER BY ${orderByColumn} ${orderByType}
</select>
5. Xml+DAO运行原理
Dao 接口的核心工作原理:基于 Mybatis 提供的代理机制
- Dao 接口定义了与 DB 操作相关的方法,常对应于 XML 映射文件中定义的 SQL 语句
- XML 文件会与 Dao 接口与之对应,通过每个 SQL 语句的唯一标识符(标签的 id)对应
- 在 MyBatis 配置文件中,需要将 Dao 接口和它对应的 XML 文件关联起来
- 当项目启动时,MyBatis 使用 JDK 动态代理或 CGLIB 来为 Dao 接口生成代理对象
- 当调用 Dao 时,实际上是调用了代理对象的对应方法。代理对象负责将调用转发给 MyBatis 的执行层
6. 动态sql作用及执行原理
动态 SQL 允许根据不同的条件构建灵活的 SQL 语句。对于一些条件查询尤其重要。eg:当某个值不为空是带上这个条件来查询
原理:
- 动态 SQL 的执行基于 XML 映射文件中定义的 SQL 片段与标签(eg:if、choose、when、otherwise、where、foreach 等),这些标签被解析,在运行时根据传入的参数值评估,最终形成完整的 SQL 语句发送到 DB 执行
动态 SQL 流程:
- 解析动态 SQL:在映射文件加载时,MyBatis 会解析 XML 文件中的动态 SQL 标签
- 参数绑定:当执行 SQL 语句时,MyBatis 会根据传入的参数绑定具体的值
- 生成最终 SQL:根据参数值和动态 SQL 标签生成最终的 SQL 语句
- 执行 SQL:MyBatis 执行生成的 SQL 语句,并返回结果
常见的动态 SQL 如下:
<if>
:允许在 SQL 中根据条件包含不同的部分
<select id="findUsersByName" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
name = #{name}
</if>
</where>
</select>
<where>
:智能地插入 WHERE 关键字,并在必要时去除多余的AND
或OR
<foreach>
:适用于需要遍历列表或数组,生成重复的 SQL 片段。eg:批量插入或IN
条件查询
<select id="findUsersInIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<choose>, <when>, <otherwise>
加一起相当于 Java 中的 switch 语句,根据多个条件选择一个执行
<select id="findUsersByStatus" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="status == 'ACTIVE'">
status = 'ACTIVE'
</when>
<when test="status == 'INACTIVE'">
status = 'INACTIVE'
</when>
<otherwise>
status = 'PENDING'
</otherwise>
</choose>
</where>
</select>
7. 延迟加载实现原理
Mybatis 支持延迟加载,也称为懒加载
- 原理:通过 Mybatis 提供的代理对象来替代主对象,当访问具体关联的属性时,才真正触发 DB 的查询,特别是在处理大数据集或复杂对象关系时,对性能提升比较大
1. 单独配置
- 假设有 User 和 Order 的一对多关系
public class User {
private List<Order> orders;
}
- 配置 Order 的懒加载。当首次访问
user.orders
属性时,selectOrdersByUserId
查询会被触发来加载用户的订单 - 后续如果再次访问订单属性则不会再触发查询,可以直接从代理对象中获取之前缓存的数据
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="username" />
<collection property="orders" ofType="Order"
select="selectOrdersByUserId" column="user_id"
fetchType="lazy"/> <!-- 这属于局部配置,只应用于这个查询 -->
</resultMap>
<select id="selectUser" resultMap="userResultMap">
SELECT id as user_id, username
FROM User
WHERE id = #{userId}
</select>
<select id="selectOrdersByUserId" resultType="Order">
SELECT id, order_date, amount
FROM Order
WHERE user_id = #{userId}
</select>
2. 全局配置
MyBatis 配置文件 mybatis-config.xml
- lazyLoadingEnabled:启用延迟加载
- aggressiveLazyLoading
- 设置为 false 时,只有在访问具体属性时才会触发加载
- 设置为 true 时,访问任意延迟加载的属性都会加载所有延迟加载的属性
<settings>
<!-- 启用懒加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当访问任何属性时,都会加载所有懒加载属性 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 使用实际的参数而不是当前对象的哈希码来触发加载 -->
<setting name="useActualParamName" value="true"/>
</settings>
8. 插件运行原理及如何编写
实现一个插件,需要实现 Mybatis 提供的 Interceptor 接口
// Interceptor 上的 @Signature 注解表明拦截的目标方法,具体参数有 type、method、args
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}
Mybatis 底层是利用 Executor 类来执行 sql,再以下三个 handler 工序之间做拦截,就能实现切入的功能,也就是插件
StatementHandler
:执行 sqlParameterHandler
:设置参数,由 statement 对象将 sql 和实参传递给 DB 执行ResultSetHandler
:封装返回的结果
织入插件的逻辑:
- 实现 Interceptor,表明要拦截的方法,填充要切入的逻辑,然后将 Interceptor 注册到 mybatis 的配置文件(
mybatis-config.xml
)中 - Mybatis 加载时会解析文件,得到一堆 Interceptor 组成 InterceptorChain 并保存着
- 然后在创建
Executor, ParameterHandler, StatementHandler, ResultSetHandler
这几个类的对象时,就会从 InterceptorChain 得到相应的 Interceptor。通过 jdk 动态代理得到代理对象,如果没有合适的 Interceptor 则会返回原对象
介绍下 Interceptor 接口,定义了 intercept, plugin, setProperties
三个方法
intercept()
:实现自定义拦截逻辑plugin()
:用于创建当前拦截器的代理对象,实现了 InvocationHandler 接口setProperties()
:用于设置插件属性,通常是从配置文件传入的参数
在 MyBatis 中,每个插件都是一个拦截器,多个拦截器形成一个拦截器链
插件具体执行流程如下:
- 当被调用
ParameterHandler, ResultSetHandler, StatementHandler, Executor
对象时,会触发 Plugin 的invoke()
- 根据插件注解上定义的
@Intercepts
、@Signature
中的的配置信息(方法名、参数等) 动态判断是否需要拦截该方法的执行 - 如果需要则调用插件的
intercept()
执行拦截逻辑,而 intercept 内执行一定逻辑后,调用invocation.proceed()
触发后续的调用
1. 插件示例
@Intercepts({
@Signature(
type= StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取被代理对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取 SQL 语句
String sql = statementHandler.getBoundSql().getSql();
System.out.println("SQL: " + sql);
// 继续执行其他插件链和目标方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// 创建代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 从配置中获取属性
String someProperty = properties.getProperty("someProperty");
System.out.println("Some Property: " + someProperty);
}
}
Mybatis 配置注册 mybatis-config.xml
<plugins>
<plugin interceptor="com.example.ExamplePlugin">
<property name="someProperty" value="value"/>
</plugin>
</plugins>
9. JDBC缺点及解决
1. JDBC
public class JdbcExample {
private static final String URL = "jdbc:mysql://localhost:3306/mydatabase";
private static final String USER = "root";
private static final String PASSWORD = "******";
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 1. 加载 DB 驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取 DB 连接
connection = DriverManager.getConnection(URL, USER, PASSWORD);
// 3. 创建 SQL 查询
String sql = "SELECT id, username, age FROM users WHERE id = ?";
// 4. 创建 PreparedStatement
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 1);
// 5. 执行查询
resultSet = preparedStatement.executeQuery();
// 6. 处理结果集
if (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
int age = resultSet.getInt("age");
System.out.println("ID: " + id + ", Username: " + username + ", Age: " + age);
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 7. 关闭资源
try {
if (resultSet != null) resultSet.close();
if (preparedStatement != null) preparedStatement.close();
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
2. Mybatis
public class MyBatisExample {
public static void main(String[] args) {
String resource = "mybatis-config.xml";
Reader reader;
try {
// 1. 读取 MyBatis 配置文件
reader = Resources.getResourceAsReader(resource);
// 2. 构建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 3. 获取 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// 4. 获取 Mapper
UserMapper mapper = session.getMapper(UserMapper.class);
// 5. 执行查询
User user = mapper.selectUserById(1);
System.out.println("ID: " + user.getId() + ", Username: " + user.getUsername() + ", Age: " + user.getAge());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 区别
JDBC 是一个强大的用于连接和操作 DB 的标准 Java API。带来的问题:
- JDBC 操作通常涉及重复的代码。如: 加载驱动、创建连接、关闭连接等
- JDBC 需要处理 SQLException,这要求在每个 DB 操作中都进行错误处理
- SQL 语句通常直接硬编码在 Java 代码中,难以维护
- JDBC 需要手动处理结果集
Mybatis 解决了这些问题:
- MyBatis 提供了一种机制来封装 JDBC 重复代码,减少了代码冗余以及异常处理,让开发只需关注 SQL 和业务逻辑
- MyBatis 允许将 SQL 语句配置在 XML 文件或通过注解配置,解决了代码耦合问题,使得代码更加清晰、易维护
- MyBatis 自动将 SQL 查询结果映射到 Java 对象或集合,解决了手动处理结果集问题
- MyBatis 支持动态 SQL 语句的构建,可以根据不同条件灵活生成 SQL,解决硬编码问题
- MyBatis 还内置了缓存机制,提升性能
10. DB类型转Java类型?
利用 TypeHandler 类型转换器
- Mybait 内置了很多默认的 TypeHandler 实现
- 这些 TypeHandler 会被注册到 TypeHandlerRegistry 中,底层就是用了很多 map 来存储类型和 TypeHandler 的对应关系
- eg:通过 JdbcType 找 TypeHandler ,通过 JavaType 找 TypeHandler
- TypeHandler 里面会实现类型的转化(DateTypeHandler 举例)
自定义类型转换器实现 TypeHandler 接口的这几个方法即可
- 在 mybatis 加载的时候,就会把这些 TypeHandler 实现类注册了,然后有对应的类型转化时,就会调用对应的方法进行转化
11. 自带的连接池
原理:
- 连接池会保存一定数量的 DB 连接,待获取连接时,直接从池里面拿连接,不需要重新建连,提高响应速度,预防突发流量,用完之后归还连接到连接池中,而不是关闭连接
Mybatis 中有池化的、非池化两类数据源:
UnpooledDataSource
(非池化)PooledDataSource
(池化)
1. 非池化
- Mybatis 是基于 JDBC 的,所以实际通过数据源获取连接是依靠 JDBC 的能力
- UnpooledDataSource,当你从中想要获取一个连接的话,实际会调用
doGetConnection
方法- 初始化 driver,然后通过 DriverManager 获取连接,这就是 JDBC 的功能,然后把连接配置下,设置下超时时间、是否自动提交事务,事务隔离级别
- 通过调用这个方法就新建连了一条连接,所以这叫非池化,因为每次获取都是新连接。然后调用 close 就直接把连接关闭了
2. 池化
PooledDataSource 的实现,两个关键点
- pop 说明是从一个地方得到连接
- getProxyConnection 说明得到的是一个代理连接
PooledDataSource 是通过一个 PoolState 对象来管理连接
- 核心:两个 list 存储了:空闲连接、活跃连接,其他就是一些统计数据了
- List 里面存储的是 PooledConnectiton,实现了代理逻辑,且保存了真正的 Connection 和代理的 Connection
- 代理其实只是拦截了
CLOSE
方法,也就是连接关闭的方法,让它只是归还到连接池中,而不是真正的关闭
获取连接 popConnection 逻辑流程:
- 利用 PoolState 加锁,同步执行以下流程
- 判断空闲连接列表是否有空闲连接
- 如果有则获得这个连接
- 如果没有空闲连接。则判断活跃的连接是否超过限定值
- 不超过则通过 JDBC 获取新的连接,再由 PooledConnection 封装,获得这个连接
- 超过最大限定值,则获得最老的连接(PooledConnection ),看看连接是否超时
- 如果超时。则进行一些统计,把上面一些未提交的事务进行回滚,然后获得底层 JDBC 的连接,再新建 PooledConnection 封装这个底层连接,并把之前老连接PooledConnection 对象设置为无效
- 如果没有超时连接。则 wait 等待,默认等待 20s
归还连接 pushConnection 逻辑流程:
- poolstate 加锁
- 直接从连接活跃链表里面移除当前的连接
- 判断连接是否有效(vaild)
- 无效。则记录下 badConnectionCount 数量,直接返回不做其他处理
- 有效。则判断连接空闲列表数量是否够了
- 不够。则判断当前连接是否有未提交事务,有的话回滚,然后新建 PooledConnection 封装当前 PooledConnection 底层的 JDBC 连接,并加入连接空闲列表中,并把当前的 PooledConnection 置为无效,并唤醒等待的连接的线程
- 够了。判断当前连接是否有未提交事务,有的话回滚,然后直接关闭底层 JDBC 的连接,,并把当前的 PooledConnection 置为无效
对 PooledConnection 的结构疑惑,这个东西除了拦截 close 方法还有什么用了?
- PooledConnection 里面有个关键的 vaild 属性,这个很有用处,因为归还连接到连接池之后,不能保证外面是否持有连接的引用,所以 PooledConnection 里加了个 valid 属性,在归还的之后把代理连接置为无效,这样即使外面有这个引用,也无法使用这个连接
- 所以上面才会有把已经超时的 PooledConnection 代理的底层的 JDBC 连接拿出来,然后新建一个 PooledConnection 再封装底层 JDBC 连接的操作,因为老的 PooledConnection 已经被设为无效啦
对 Mybatis 连接池设计已经心知肚明。默认情况下 Mybatis 使用 PooledDataSource 数据源,不过在生产环境中,通常会使用第三方的连接池库(eg:C3P0、HikariCP 等),来替代 MyBatis 自带的连接池,以获得更好的性能和配置灵活性
12. MyBatis优点
- 与 JDBC 相比,Mybatis 减少了复杂的代码配置,让开发者不用过于关注繁琐连接操作
- 通过将 SQL 语句保持在 XML 文件或注解中,很好的将逻辑层与数据层分离,灵活性高
- 内置一级缓存和二级缓存机制,可提高 DB 查询性能
- 支持多种 DB 和连接池,在多种开发环境中都能使用
- 提供了强大的动态 SQL 功能,能够根据条件灵活生成 SQL 语句
- 与 Hibernate 相比,MyBatis 更加轻量级,也提供了更多的 SQL 控制权,适合复杂查询和高性能需求的场景
13. MyBatis缺点
- MyBatis 不会像全自动 ORM 框架(Hibernate)那样自动生成 SQL 语句, 对于一些简单的查询修改操作,也需要自己写 SQL,增加了工作量
- 对大型项目来说,可能需要大量的 xml 文件,会导致配置的复杂性和维护的困难
- 因为 MyBatis 缺乏 SQL 自动能力,因此维护成本会高一些。eg:修改了表结构或字段名,都需要手动修改相应的 SQL,还容易漏改