面渣逆袭:二十二图、八千字、二十问,彻底搞定MyBatis!(二)
10.Mybatis是否支持延迟加载?原理?
•Mybatis支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
•它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
•当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
11.如何获取生成的主键?
•新增标签中添加:keyProperty=" ID " 即可
<insert id="insert" useGeneratedKeys="true" keyProperty="userId" >
insert into user(
user_name, user_password, create_time)
values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP})
</insert>
•这时候就可以完成回填主键
mapper.insert(user);
user.getId;
12.MyBatis支持动态SQL吗?
MyBatis中有一些支持动态SQL的标签,它们的原理是使用OGNL从SQL参数对象中计算表达式的值,根据表达式的值动态拼接SQL,以此来完成动态SQL的功能。
![
•if
根据条件来组成where子句
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
•choose (when, otherwise)
这个和Java 中的 switch 语句有点像
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
•trim (where, set)
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
◆<set> 可以用在动态更新的时候
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
◆<where>可以用在所有的查询条件都是动态的情况
• foreach
看到名字就知道了,这个是用来循环的,可以对集合进行遍历
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>
13.MyBatis如何执行批量操作?
MyBatis批量操作
第一种方法:使用foreach标签
foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。
• item 表示集合中每一个元素进行迭代时的别名,随便起的变量名;
• index 指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用;
• open 表示该语句以什么开始,常用“(”;
• separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”;
• close 表示以什么结束,常用“)”。
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有以下3种情况:
1.如果传入的是单参数且参数类型是一个List的时候,collection属性值为list
2.如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array
3.如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的, map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key
看看批量保存的两种用法:
<!-- MySQL下批量保存,可以foreach遍历 mysql支持values(),(),()语法 --> //推荐使用
<insert id="addEmpsBatch">
INSERT INTO emp(ename,gender,email,did)
VALUES
<foreach collection="emps" item="emp" separator=",">
(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
</foreach>
</insert>
<!-- 这种方式需要数据库连接属性allowMutiQueries=true的支持
如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true -->
<insert id="addEmpsBatch">
<foreach collection="emps" item="emp" separator=";">
INSERT INTO emp(ename,gender,email,did)
VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
</foreach>
</insert>
第二种方法:使用ExecutorType.BATCH
• Mybatis内置的ExecutorType有3种,默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优;但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,在某些情况下不符合业务的需求。
具体用法如下:
//批量保存方法测试
@Test
public void testBatch() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
//可以执行批量操作的sqlSession
SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
//批量保存执行前时间
long start = System.currentTimeMillis();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
for (int i = 0; i < 1000; i++) {
mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
}
openSession.commit();
long end = System.currentTimeMillis();
//批量保存执行后的时间
System.out.println("执行时长" + (end - start));
//批量 预编译sql一次==》设置参数==》10000次==》执行1次 677
//非批量 (预编译=设置参数=执行 )==》10000次 1121
} finally {
openSession.close();
}
}
•mapper和mapper.xml如下
public interface EmployeeMapper {
//批量保存员工
Long addEmp(Employee employee);
}
<mapper namespace="com.jourwon.mapper.EmployeeMapper"
<!--批量保存员工 -->
<insert id="addEmp">
insert into employee(lastName,email,gender)
values(#{lastName},#{email},#{gender})
</insert>
</mapper>
14.说说Mybatis的一级、二级缓存?
1.一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为SqlSession,各个SqlSession之间的缓存相互隔离,当 Session flush 或 close 之后,该 SqlSession 中的所有 Cache 就将清空,MyBatis默认打开一级缓存。
Mybatis一级缓存
2.二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同之处在于其存储作用域为 Mapper(Namespace),可以在多个SqlSession之间共享,并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置。
Mybatis二级缓存示意图
原理
15.能说说MyBatis的工作原理吗?
我们已经大概知道了MyBatis的工作流程,按工作原理,可以分为两大步:生成会话工厂、会话运行。
MyBatis的工作流程
MyBatis是一个成熟的框架,篇幅限制,这里抓大放小,来看看它的主要工作流程。
构建会话工厂
构造会话工厂也可以分为两步:
构建会话工厂
• 获取配置
获取配置这一步经过了几步转化,最终由生成了一个配置类Configuration实例,这个配置类实例非常重要,主要作用包括:
◆读取配置文件,包括基础配置文件和映射文件
◆初始化基础配置,比如MyBatis的别名,还有其它的一些重要的类对象,像插件、映射器、ObjectFactory等等
◆提供一个单例,作为会话工厂构建的重要参数
◆它的构建过程也会初始化一些环境变量,比如数据源
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
SqlSessionFactory var5;
//省略异常处理
//xml配置构建器
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//通过转化的Configuration构建SqlSessionFactory
var5 = this.build(parser.parse());
}
• 构建SqlSessionFactory
SqlSessionFactory只是一个接口,构建出来的实际上是它的实现类的实例,一般我们用的都是它的实现类DefaultSqlSessionFactory,
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
会话运行
会话运行是MyBatis最复杂的部分,它的运行离不开四大组件的配合:
MyBatis会话运行四大关键组件
• Executor(执行器)
Executor起到了至关重要的作用,SqlSession只是一个门面,相当于客服,真正干活的是是Executor,就像是默默无闻的工程师。它提供了相应的查询和更新方法,以及事务方法。
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//通过Configuration创建executor
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
• StatementHandler(数据库会话器)
StatementHandler,顾名思义,处理数据库会话的。我们以SimpleExecutor为例,看一下它的查询方法,先生成了一个StatementHandler实例,再拿这个handler去执行query。
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;
}
再以最常用的PreparedStatementHandler看一下它的query方法,其实在上面的prepareStatement已经对参数进行了预编译处理,到了这里,就直接执行sql,使用ResultHandler处理返回结果。
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement)statement;
ps.execute();
return this.resultSetHandler.handleResultSets(ps);
}
•ParameterHandler (参数处理器)
PreparedStatementHandler里对sql进行了预编译处理
public void parameterize(Statement statement) throws SQLException {
this.parameterHandler.setParameters((PreparedStatement)statement);
}
这里用的就是ParameterHandler,setParameters的作用就是设置预编译SQL语句的参数。
里面还会用到typeHandler类型处理器,对类型进行处理。
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement var1) throws SQLException;
}
•ResultSetHandler(结果处理器)
我们前面也看到了,最后的结果要通过ResultSetHandler来进行处理,handleResultSets这个方法就是用来包装结果集的。Mybatis为我们提供了一个DefaultResultSetHandler,通常都是用这个实现类去进行结果的处理的。
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement var1) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
void handleOutputParameters(CallableStatement var1) throws SQLException;
}
它会使用typeHandle处理类型,然后用ObjectFactory提供的规则组装对象,返回给调用者。
整体上总结一下会话运行:
会话运行的简单示意图
PS:以上源码分析比较简单,在真正的源码大佬面前可能过不了关,有条件的建议Debug一下MyBatis的源码。
我们最后把整个的工作流程串联起来,简单总结一下:
MyBatis整体工作原理图
1.读取 MyBatis 配置文件——mybatis-config.xml 、加载映射文件——映射文件即 SQL 映射文件,文件中配置了操作数据库的 SQL 语句。最后生成一个配置对象。
2.构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
3.创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
4.Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
5.StatementHandler:数据库会话器,串联起参数映射的处理和运行结果映射的处理。
6.参数处理:对输入参数的类型进行处理,并预编译。
7.结果处理:对返回结果的类型进行处理,根据对象映射规则,返回相应的对象。