🍄 MyBatis

吞佛童子2022年10月10日
  • frame
  • MyBatis
大约 11 分钟

🍄 MyBatis

1. 概述

半 ORM 框架

  1. MyBatis 是一款优诱的 持久层框架,它支持排 自定义SQL存褚过程 以及 高级映射
  2. MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作
  3. MyBatis 可以通过简单的 XML注解 来配置和映射原始类型、接口和 Java POJO (Plain Old Java Objects,普通老式 Java 对象) 为数据库中的记录。
  4. MyBatis 是 半 ORM 框架
    • 在查询关联对象 | 关联集合对象时,需要手动编写 SQL 来完成,因此是 半自动 ORM 框架
    • Hibernate 为 全自动 ORM 映射工具,使用其查询时,可以根据对象关系模型直接获取,因此是 全自动的

ORM 框架

  • 对象关系映射
  • 为了解决 关系型数据库数据 & 简单 Java 对象 的映射关系的技术
  • ORM 通过使用描述对象 & 数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中

2. 生命周期

  • Spring 框架 可以创建 线程安全的、基于事务的 SqlSession & Mapper,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期

1) SqlSessionFactoryBuilder

  • 用于创建 SqlSessionFactory,创建完成后就不需要了,因此该实例的生命周期只存在于 方法内部
  • SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例
  • 流程:
    • 获取配置,生成配置类 Configuration 实例
    • 通过 SqlSessionFactoryBuilder 生成 SqlSessionFactory
  • 环境配置:
    • 可配置多个 environment,但每个 SqlSessionFactory 实例只能选择一种环境
    • 每个数据库对应一种 environment,因此若要连接多个数据库,则需要创建多个 SqlSessionFactory 实例
    • 若没有配置,则会使用 默认配置

2) SqlSessionFactory

  • 用于创建 SqlSession,相当于一个数据库连接池,一旦被创建就应该在应用的运行期间一直存在,生命周期为 应用级,应该设置为 单例模式
  • 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的

3) SqlSession

  • 相当于 JDBC 的 Connection,SqlSession非线程安全的,因此 每个线程都应该有它自己的 SqlSession 实例,
  • 它的最佳的作用域是 一次请求 | 一次方法
try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的应用逻辑代码
}

4) Mapper 实例

  • Mapper 实例 是从 SqlSession 中获得的
  • Mapper 实例的最合适的作用域是 方法作用域
try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
  // ...
  session.close(); // 关闭会话
}

3. MyBatis 功能架构

Executor

  • 生命周期:
    • SqlSession

① SimpleExecutor

  • 每执行一次 update | select, 就开启一个 Statement 对象,用完立即关闭该 Statement 对象
  • 默认情况下的配置

② ReuseExecutor

  • 每次执行 update | select,首先以 sql 作为 key 查找 Statement 对象,存在就使用,不存在则创建并缓存
  • 重复使用 Statement 对象
  • 存放在 Map<String, Statement> 数据结构中

③ BatchExecutor

  • 重复使用 Statement 对象
  • 支持 批量更新
    • 每次执行 update 时,将所有 sql 添加到批处理(addBatch()) 中,等待批量执行(executeBatch())

4. MyBatis 参数设置

<settings>
<!--   全局开启 | 关闭 缓存-->
  <setting name="cacheEnabled" value="true"/>
<!--   全局开启 | 关闭 延迟加载-->
  <setting name="lazyLoadingEnabled" value="false"/>
<!--   是否允许单个语句返回 多结果集,需要 数据库驱动 支持-->
  <setting name="multipleResultSetsEnabled" value="true"/>
<!--   使用 列标签 代替列名,依赖 数据库驱动-->
  <setting name="useColumnLabel" value="true"/>
<!--   是否允许 JDBC 自动生成主键,需要 数据库驱动 支持-->
  <setting name="useGeneratedKeys" value="true"/>
<!--   设置 MyBatis 自动映射 列到字段或属性 的方式,NONE - 关闭;PARTIAL - 只自动映射没有定义嵌套结果映射的字段;FULL - 自动映射任何复杂的结果集-->
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<!--    执行器,SIMPLE - 默认普通执行器;REUSE - 重复使用预处理语句;BATCH - 不仅重用语句,还会执行批量更新-->
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
<!--   是否允许在 嵌套语句 中使用 分页,允许则为 false-->
  <setting name="safeRowBoundsEnabled" value="false"/>
<!--   是否允许在 嵌套语句 中使用 结果处理器,允许则 false-->
  <setting name="mapUnderscoreToCamelCase" value="true"/>
<!--   本地缓存机制的作用域,SESSION - 本次 SqlSession 会话的所有查询;STATEMENT - 仅作用于 执行语句,相同 SqlSession 的不同查询不会进行缓存-->
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
<!--   指定对象的哪些方法触发一次延迟加载,不同方法逗号分隔-->
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

5. MyBatis 缓存

1) 本地缓存

  1. 默认情况下,属性值为 SESSION,表示 一个 SqlSession 对应一个与之关联的 本地缓存
  2. SqlSession 执行的查询结果会被保存在 本地缓存 中,当再次执行 相同参数的相同查询 时,返回本地缓存的结果
  3. 执行 修改性操作 | 事务提交 | 事务回滚 | 关闭 SqlSession 时,本地缓存被清空
  4. 被用来解决 循环引用 问题 & 加快重复嵌套查询速度
  5. 默认 开启,无法将其完全禁用
  6. 属性值为 STATEMENT 时,仅作用于 执行语句,相同 SqlSession 的不同查询不会进行缓存

2) 二级缓存

  1. 多个 SqlSession 共享 二级缓存,但若设置为 session,则会返回在 本地缓存 中唯一对象的引用
  2. 默认 关闭

6. MyBatis 延迟加载

  1. MyBatis 支持 association 关联对象 & collection 关联集合对象 的延迟加载
  2. 原理:
    • 调用目标方法时,如果发现需要属性没有被加载,那么会先加载属性对象,然后继续完成本对象的方法调用过程

7. MyBatis 插件

1) 原理

  • MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截以下方法的调用:
    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    • StatementHandler (prepare, parameterize, batch, update, query)
    • ParameterHandler (getParameterObject, setParameters)
    • ResultSetHandler (handleResultSets, handleOutputParameters)
  • MyBatis 通过 JDK 动态代理 的方式,为目标对象生成 代理对象,有一个工具类 Plugin.Interceptor,它实现类 InnovationHandler 接口,重写了 invoke()
  • 因此我们只有 实现 拦截器接口 Interceptor,重写 interceptor() 方法,就可对其拦截,加入自定义功能

2) 如何编写一个插件

3) PageHelper 分页插件

  • MyBatis 使用 RowBounds 对象进行分页,但是整个分页是求出结果集后,进行内存分页,而非物理分页
  • 分页插件实现了物理分页,根据输入参数,求出对应的物理分页参数
  • PageHelper 分页插件创建了 PageInterceptor 类 来实现 MyBatis 的 Interceptor 接口
/**
 * Mybatis - 通用分页拦截器
 * <p>
 * GitHub: https://github.com/pagehelper/Mybatis-PageHelper
 * <p>
 * Gitee : https://gitee.com/free/Mybatis_PageHelper
 *
 * @author liuzh/abel533/isea533
 * @version 5.0.0
 */
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
    {
        @Signature(
                type = Executor.class, // 拦截的是 Executor 类的 针对不同参数的 query() 方法
                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 {
    private static final Log log = LogFactory.getLog(PageInterceptor.class);

    private volatile Dialect                        dialect;
    private          String                         countSuffix           = "_COUNT";
    protected        Cache<String, MappedStatement> msCountMap            = null;
    private          String                         default_dialect_class = "com.github.pagehelper.PageHelper";

    public PageInterceptor() {
        String bannerEnabled = System.getProperty("pagehelper.banner");
        if (StringUtil.isEmpty(bannerEnabled)) {
            bannerEnabled = System.getenv("PAGEHELPER_BANNER");
        }
        //默认 TRUE
        if (StringUtil.isEmpty(bannerEnabled) || Boolean.parseBoolean(bannerEnabled)) {
            log.debug("\n\n" +
                ",------.                           ,--.  ,--.         ,--.                         \n" +
                "|  .--. '  ,--,--.  ,---.   ,---.  |  '--'  |  ,---.  |  |  ,---.   ,---.  ,--.--. \n" +
                "|  '--' | ' ,-.  | | .-. | | .-. : |  .--.  | | .-. : |  | | .-. | | .-. : |  .--' \n" +
                "|  | --'  \\ '-'  | ' '-' ' \\   --. |  |  |  | \\   --. |  | | '-' ' \\   --. |  |    \n" +
                "`--'       `--`--' .`-  /   `----' `--'  `--'  `----' `--' |  |-'   `----' `--'    \n" +
                "                   `---'                                   `--'                        is intercepting.\n");
        }
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系,只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();
            //对 boundSql 的拦截处理
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
            }
            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            if(dialect != null){
                dialect.afterAll();
            }
        }
    }

    /**
     * Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化
     * <p>
     * 因此这里会出现 null 的情况 fixed #26
     */
    private void checkDialectExists() {
        if (dialect == null) {
            synchronized (default_dialect_class) {
                if (dialect == null) {
                    setProperties(new Properties());
                }
            }
        }
    }

    private Long count(Executor executor, MappedStatement ms, Object parameter,
                       RowBounds rowBounds, ResultHandler resultHandler,
                       BoundSql boundSql) throws SQLException {
        String countMsId = ms.getId() + countSuffix;
        Long count;
        //先判断是否存在手写的 count 查询
        MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
        if (countMs != null) {
            count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
        } else {
            if (msCountMap != null) {
                countMs = msCountMap.get(countMsId);
            }
            //自动创建
            if (countMs == null) {
                //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                if (msCountMap != null) {
                    msCountMap.put(countMsId, countMs);
                }
            }
            count = ExecutorUtil.executeAutoCount(this.dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
        }
        return count;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //缓存 count ms
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClass)) {
            dialectClass = default_dialect_class;
        }
        try {
            Class<?> aClass = Class.forName(dialectClass);
            dialect = (Dialect) aClass.newInstance();
        } catch (Exception e) {
            throw new PageException(e);
        }
        dialect.setProperties(properties);

        String countSuffix = properties.getProperty("countSuffix");
        if (StringUtil.isNotEmpty(countSuffix)) {
            this.countSuffix = countSuffix;
        }
    }

}

8. 批量插入性能对比

  1. 业务代码层循环,每次插入一条数据
    • 性能最慢
  2. 使用 MyBatis Plus 批量插入
    • 性能较快
    • 数据量条数自动分为 1000 条进行批量插入
  3. 使用 MyBatis 原生 foreach 批量插入
    • 条数远大于 1000 时,速度最快;否则和 MyBatis Plus 批量插入 性能差不多
    • 传入多少条数据就一次全部传入 MySQL,当条数过多时,可能超出 MySQL 可执行的最大 SQL 大小[default = 4M]
      • 此时需要 业务设置每次传合适的条数,然后循环批量插入
      • 或调整 MySQL 参数 set global max_allowed_packet=10*1024*1024

9. MyBatis Plus

1) 特点

  1. 只做增强,不做改变,引入不会对现有工程产生影响
  2. 只需简单配置,即可快速单表 CRUD,节省时间
  3. 有 代码生成、自动分页、逻辑删除、自动填充等功能
上次编辑于: 2022/10/10 下午8:43:48
贡献者: liuxianzhishou