🍄 MyBatis
2022年10月10日
- frame
🍄 MyBatis
1. 概述
半 ORM 框架
- MyBatis 是一款优诱的 持久层框架,它支持排 自定义SQL、存褚过程 以及 高级映射。
- MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作
- MyBatis 可以通过简单的 XML 或 注解 来配置和映射原始类型、接口和 Java POJO (Plain Old Java Objects,普通老式 Java 对象) 为数据库中的记录。
- 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) 本地缓存
- 默认情况下,属性值为
SESSION
,表示 一个SqlSession
对应一个与之关联的本地缓存
SqlSession
执行的查询结果会被保存在本地缓存
中,当再次执行相同参数的相同查询
时,返回本地缓存的结果- 执行 修改性操作 | 事务提交 | 事务回滚 | 关闭 SqlSession 时,本地缓存被清空
- 被用来解决 循环引用 问题 & 加快重复嵌套查询速度
- 默认 开启,无法将其完全禁用
- 属性值为
STATEMENT
时,仅作用于 执行语句,相同 SqlSession 的不同查询不会进行缓存
2) 二级缓存
- 多个
SqlSession
共享 二级缓存,但若设置为session
,则会返回在 本地缓存 中唯一对象的引用 - 默认 关闭
6. MyBatis 延迟加载
- MyBatis 支持 association 关联对象 & collection 关联集合对象 的延迟加载
- 原理:
- 调用目标方法时,如果发现需要属性没有被加载,那么会先加载属性对象,然后继续完成本对象的方法调用过程
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. 批量插入性能对比
- 业务代码层循环,每次插入一条数据
- 性能最慢
- 使用 MyBatis Plus 批量插入
- 性能较快
- 数据量条数自动分为
1000
条进行批量插入
- 使用 MyBatis 原生 foreach 批量插入
- 条数远大于 1000 时,速度最快;否则和 MyBatis Plus 批量插入 性能差不多
- 传入多少条数据就一次全部传入 MySQL,当条数过多时,可能超出 MySQL 可执行的最大 SQL 大小[default = 4M]
- 此时需要 业务设置每次传合适的条数,然后循环批量插入
- 或调整 MySQL 参数
set global max_allowed_packet=10*1024*1024
9. MyBatis Plus
1) 特点
- 只做增强,不做改变,引入不会对现有工程产生影响
- 只需简单配置,即可快速单表 CRUD,节省时间
- 有 代码生成、自动分页、逻辑删除、自动填充等功能