10-Mybatis(13)

1. MyBatis缓存机制

Mybatis 中有两类缓存,分别是一级缓存和二级缓存

1. 一级缓存

一级缓存默认开启,二级缓存默认关闭,若开启,可在 SqlSession 之间共享缓存数据

  • 一级缓存默认是会话级缓存。即创建一个 SqlSession 对象就是一个会话,一次会话可能会执行多次相同的查询,这样缓存了之后就能重复利用查询结果,提高性能,不过 commit, rollback, update, delete 等都会清除缓存
  • 注意:不同 SqlSession 之间的修改不会影响彼此。eg:SqlSession1 读了数据 A,SqlSession2 将数据改为 B,此时 SqlSession1 再读还是得到 A,出现脏数据问题。所以,多 SqlSession 或分布式环境下,就可能有脏数据的情况发生,建议将一级缓存级别设置为 statement
image.png

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 方法:

image.png
  • MyBatis 的缓存本质上就是在本地利用 map 来存储数据
  • 基础实现类是 PerpetualCache,并且使用了装饰器模式,提供了各种各样的 cache 进行功能的扩展
    • BlockingCache 可以提供阻塞
    • FifoCache
    • LruCache
image.png

可以看到 mybaits 缓存还是不太安全,在分布式场景下肯定会出现脏数据。建议生产上使用 Redis 结合 SpringCache 进行数据的缓存,或利用 guava、caffeine 进行本地缓存

2. MyBatis执行原理

首先解析配置 mybatis-config.xml,将配置文件的内容转为 Configuration 对象加载到内存中,包括一些 xxxMapper.xml 文件,将这些文件解析成 MappedStatement 对象,保存其 SQL 语句、参数映射等信息

1. MapperProxy

  • Mapper 都是接口,实际的实现类是由 Mybatis 利用 JDK 动态代理生成的,具体是在 MapperProxyFactory 中生成代理类
image.png

2. MapperMethod

  • 当调用 Mapper 接口中的方法时,代理对象会将调用信息传递给 MyBatis 内部的 MapperProxy 对象,实际上调用方法时,会触发 MapperMethod 来执行具体逻辑
  • 里面分了 insert, update, delete 等不同的处理方法,这时候就可以执行具体的语句了
image.png

3. SqlCommand

  • command 是 MapperMethod 内部的一个属性
image.png
  • command 就是通过调用的 接口名 + 方法名,从 Configuration 中找到 MappedStatement 对象,这个对象记录了 SQL 内容、出参入参等,这样就得到最终的 SQL 相关信息了,跟前面的配置串起来了
image.png

mybatis 还有缓存机制,因此会经过 CachingExecutor 的处理,这里主要是二级缓存的处理

4. JDBC

  1. 最终还是会到 JDBC 执行 SQL 的流程,获取 Connection(实际会从连接池中获取),且得到 Statement
image.png
  1. 然后调用 execute 执行(和 JDBC 一样)
image.png
  1. 最后通过 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 语句,默认使用 #{}(为了安全)

  1. #{}:是参数占位符,用于预处理语句中的参数,它通过 JDBC 的 PreparedStatement 来处理,这意味着参数会在 SQL 执行前被预编译,并通过占位符的形式提供给 SQL 语句(注意这个能力不是 Mybatis 提供的,这个是 MySQL 的预处理能力),可以有效的防止 SQL 注入
  2. ${}:用于字符串替换。在动态 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 提供的代理机制

  1. Dao 接口定义了与 DB 操作相关的方法,常对应于 XML 映射文件中定义的 SQL 语句
  2. XML 文件会与 Dao 接口与之对应,通过每个 SQL 语句的唯一标识符(标签的 id)对应
  3. 在 MyBatis 配置文件中,需要将 Dao 接口和它对应的 XML 文件关联起来
  4. 当项目启动时,MyBatis 使用 JDK 动态代理或 CGLIB 来为 Dao 接口生成代理对象
  5. 当调用 Dao 时,实际上是调用了代理对象的对应方法。代理对象负责将调用转发给 MyBatis 的执行层

6. 动态sql作用及执行原理

动态 SQL 允许根据不同的条件构建灵活的 SQL 语句。对于一些条件查询尤其重要。eg:当某个值不为空是带上这个条件来查询

原理:

  • 动态 SQL 的执行基于 XML 映射文件中定义的 SQL 片段与标签(eg:if、choose、when、otherwise、where、foreach 等),这些标签被解析,在运行时根据传入的参数值评估,最终形成完整的 SQL 语句发送到 DB 执行

动态 SQL 流程:

  1. 解析动态 SQL:在映射文件加载时,MyBatis 会解析 XML 文件中的动态 SQL 标签
  2. 参数绑定:当执行 SQL 语句时,MyBatis 会根据传入的参数绑定具体的值
  3. 生成最终 SQL:根据参数值和动态 SQL 标签生成最终的 SQL 语句
  4. 执行 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 关键字,并在必要时去除多余的 ANDOR
  • <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 工序之间做拦截,就能实现切入的功能,也就是插件

  1. StatementHandler:执行 sql
  2. ParameterHandler:设置参数,由 statement 对象将 sql 和实参传递给 DB 执行
  3. 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 中,每个插件都是一个拦截器,多个拦截器形成一个拦截器链

插件具体执行流程如下:

  1. 当被调用 ParameterHandler, ResultSetHandler, StatementHandler, Executor 对象时,会触发 Plugin 的 invoke()
  2. 根据插件注解上定义的 @Intercepts@Signature 中的的配置信息(方法名、参数等) 动态判断是否需要拦截该方法的执行
  3. 如果需要则调用插件的 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。带来的问题:

  1. JDBC 操作通常涉及重复的代码。如: 加载驱动、创建连接、关闭连接等
  2. JDBC 需要处理 SQLException,这要求在每个 DB 操作中都进行错误处理
  3. SQL 语句通常直接硬编码在 Java 代码中,难以维护
  4. JDBC 需要手动处理结果集

Mybatis 解决了这些问题:

  1. MyBatis 提供了一种机制来封装 JDBC 重复代码,减少了代码冗余以及异常处理,让开发只需关注 SQL 和业务逻辑
  2. MyBatis 允许将 SQL 语句配置在 XML 文件或通过注解配置,解决了代码耦合问题,使得代码更加清晰、易维护
  3. MyBatis 自动将 SQL 查询结果映射到 Java 对象或集合,解决了手动处理结果集问题
  4. MyBatis 支持动态 SQL 语句的构建,可以根据不同条件灵活生成 SQL,解决硬编码问题
  5. MyBatis 还内置了缓存机制,提升性能

10. DB类型转Java类型?

利用 TypeHandler 类型转换器

  • Mybait 内置了很多默认的 TypeHandler 实现
image.png
  • 这些 TypeHandler 会被注册到 TypeHandlerRegistry 中,底层就是用了很多 map 来存储类型和 TypeHandler 的对应关系
    • eg:通过 JdbcType 找 TypeHandler ,通过 JavaType 找 TypeHandler
image.png
  • TypeHandler 里面会实现类型的转化(DateTypeHandler 举例)
image.png

自定义类型转换器实现 TypeHandler 接口的这几个方法即可

  • 在 mybatis 加载的时候,就会把这些 TypeHandler 实现类注册了,然后有对应的类型转化时,就会调用对应的方法进行转化

11. 自带的连接池

原理:

  • 连接池会保存一定数量的 DB 连接,待获取连接时,直接从池里面拿连接,不需要重新建连,提高响应速度,预防突发流量,用完之后归还连接到连接池中,而不是关闭连接

Mybatis 中有池化的、非池化两类数据源:

  • UnpooledDataSource(非池化)
  • PooledDataSource(池化)

1. 非池化

  • Mybatis 是基于 JDBC 的,所以实际通过数据源获取连接是依靠 JDBC 的能力
  • UnpooledDataSource,当你从中想要获取一个连接的话,实际会调用 doGetConnection方法
    • 初始化 driver,然后通过 DriverManager 获取连接,这就是 JDBC 的功能,然后把连接配置下,设置下超时时间、是否自动提交事务,事务隔离级别
  • 通过调用这个方法就新建连了一条连接,所以这叫非池化,因为每次获取都是新连接。然后调用 close 就直接把连接关闭了
image.png

2. 池化

PooledDataSource 的实现,两个关键点

  • pop 说明是从一个地方得到连接
  • getProxyConnection 说明得到的是一个代理连接
image.png

PooledDataSource 是通过一个 PoolState 对象来管理连接

  • 核心:两个 list 存储了:空闲连接、活跃连接,其他就是一些统计数据了
image.png
  • List 里面存储的是 PooledConnectiton,实现了代理逻辑,且保存了真正的 Connection 和代理的 Connection
image.png
  • 代理其实只是拦截了 CLOSE 方法,也就是连接关闭的方法,让它只是归还到连接池中,而不是真正的关闭
image.png

获取连接 popConnection 逻辑流程:

  1. 利用 PoolState 加锁,同步执行以下流程
  2. 判断空闲连接列表是否有空闲连接
    • 如果有则获得这个连接
    • 如果没有空闲连接。则判断活跃的连接是否超过限定值
      • 不超过则通过 JDBC 获取新的连接,再由 PooledConnection 封装,获得这个连接
      • 超过最大限定值,则获得最老的连接(PooledConnection ),看看连接是否超时
        • 如果超时。则进行一些统计,把上面一些未提交的事务进行回滚,然后获得底层 JDBC 的连接,再新建 PooledConnection 封装这个底层连接,并把之前老连接PooledConnection 对象设置为无效
        • 如果没有超时连接。则 wait 等待,默认等待 20s

归还连接 pushConnection 逻辑流程:

  1. poolstate 加锁
  2. 直接从连接活跃链表里面移除当前的连接
  3. 判断连接是否有效(vaild)
    1. 无效。则记录下 badConnectionCount 数量,直接返回不做其他处理
    2. 有效。则判断连接空闲列表数量是否够了
      • 不够。则判断当前连接是否有未提交事务,有的话回滚,然后新建 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优点

  1. 与 JDBC 相比,Mybatis 减少了复杂的代码配置,让开发者不用过于关注繁琐连接操作
  2. 通过将 SQL 语句保持在 XML 文件或注解中,很好的将逻辑层与数据层分离,灵活性高
  3. 内置一级缓存和二级缓存机制,可提高 DB 查询性能
  4. 支持多种 DB 和连接池,在多种开发环境中都能使用
  5. 提供了强大的动态 SQL 功能,能够根据条件灵活生成 SQL 语句
  6. 与 Hibernate 相比,MyBatis 更加轻量级,也提供了更多的 SQL 控制权,适合复杂查询和高性能需求的场景

13. MyBatis缺点

  1. MyBatis 不会像全自动 ORM 框架(Hibernate)那样自动生成 SQL 语句, 对于一些简单的查询修改操作,也需要自己写 SQL,增加了工作量
  2. 对大型项目来说,可能需要大量的 xml 文件,会导致配置的复杂性和维护的困难
  3. 因为 MyBatis 缺乏 SQL 自动能力,因此维护成本会高一些。eg:修改了表结构或字段名,都需要手动修改相应的 SQL,还容易漏改