大家好,我是老三,面渣逆袭系列继续,这节我们的主角是MyBatis,作为当前国内最流行的ORM框架,是我们这些crud选手最趁手的工具,赶紧来看看面试都会问哪些问题吧。

先吹一下:
Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。
MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
再说一下缺点
ORM是什么?

为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?
JDBC编程有哪些不足之处,MyBatis是如何解决的?

PS:直接用Hibernate的应该不多了吧,毕竟大家都是“敏捷开发”,但架不住面试爱问。
相同点

不同点
映射关系
SQL优化和移植性
MyBatis和Hibernate的适用场景?

Hibernate 是标准的ORM框架,SQL编写量较少,但不够灵活,适合于需求相对稳定,中小型的软件项目,比如:办公自动化系统
MyBatis 是半ORM框架,需要编写较多SQL,但是比较灵活,适合于需求变化频繁,快速迭代的项目,比如:电商网站
MyBatis基本使用的过程大概可以分为这么几步:

1、 创建SqlSessionFactory
可以从配置或者直接编码来创建SqlSessionFactory
String resource = "org/mybatis/example/mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);2、 通过SqlSessionFactory创建SqlSession
SqlSession(会话)可以理解为程序和数据库之间的桥梁
SqlSession session = sqlSessionFactory.openSession();3、 通过sqlsession执行数据库操作
可以通过 SqlSession 实例来直接执行已映射的 SQL 语句:
Blog blog = (Blog)session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);更常用的方式是先获取Mapper(映射),然后再执行SQL语句:
BlogMapper mapper = session.getMapper(BlogMapper.class);Blog blog = mapper.selectBlog(101);4、 调用session.commit()提交事务
如果是更新、删除语句,我们还需要提交一下事务。
5、 调用session.close()关闭会话
最后一定要记得关闭会话。
MyBatis生命周期?
上面提到了几个MyBatis的组件,一般说的MyBatis生命周期就是这些组件的生命周期。
SqlSessionFactoryBuilder
一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的生命周期只存在于方法的内部。
SqlSessionFactory
SqlSessionFactory 是用来创建SqlSession的,相当于一个数据库连接池,每次创建SqlSessionFactory都会使用数据库资源,多次创建和销毁是对资源的浪费。所以SqlSessionFactory是应用级的生命周期,而且应该是单例的。
SqlSession
SqlSession相当于JDBC中的Connection,SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的生命周期是一次请求或一个方法。
Mapper
映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的,它的生命周期在sqlsession事务方法之内,一般会控制在方法级。

当然,万物皆可集成Spring,MyBatis通常也是和Spring集成使用,Spring可以帮助我们创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到我们的 bean 中,我们不需要关心它们的创建过程和生命周期,那就是另外的故事了。
ps:接下来看看Mybatis的基本使用,不会有人不会吧?不会吧!


方法1:顺序传参法
public User selectUser(String name, int deptId);<select id="selectUser" resultMap="UserResultMap"> select * from user where user_name = #{0} and dept_id = #{1}</select>方法2:@Param注解传参法
public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);<select id="selectUser" resultMap="UserResultMap"> select * from user where user_name = #{userName} and dept_id = #{deptId}</select>方法3:Map传参法
public User selectUser(Map<String, Object> params);<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap"> select * from user where user_name = #{userName} and dept_id = #{deptId}</select>方法4:Java Bean传参法
public User selectUser(User user);<select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap"> select * from user where user_name = #{userName} and dept_id = #{deptId}</select>第1种: 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
<select id="getOrder" parameterType="int" resultType="com.jourwon.pojo.Order"> select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};</select>第2种: 通过resultMap 中的<result>来映射字段名和实体类属性名的一一对应的关系。
<select id="getOrder" parameterType="int" resultMap="orderResultMap"> select * from orders where order_id=#{id}</select> <resultMap type="com.jourwon.pojo.Order" id="orderResultMap"> <!–用id属性来映射主键字段–> <id property="id" column="order_id"> <!–用result属性来映射非主键字段,property为实体类属性名,column为数据库表中的属性–> <result property ="orderno" column ="order_no"/> <result property="price" column="order_price" /></reslutMap>

<select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">  <bind name="pattern" value="'%' + username + '%'" />  select id,sex,age,username,password from person where username LIKE #{pattern}</select>当然可以,不止支持一对一、一对多的关联查询,还支持多对多、多对一的关联查询。

一对一<association>
比如订单和支付是一对一的关系,这种关联的实现:
实体类:
public class Order { private Integer orderId; private String orderDesc; /** * 支付对象 */ private Pay pay; //……}结果映射
<!-- 订单resultMap --><resultMap id="peopleResultMap" type="cn.fighter3.entity.Order"> <id property="orderId" column="order_id" /> <result property="orderDesc" column="order_desc"/> <!--一对一结果映射--> <association property="pay" javaType="cn.fighter3.entity.Pay"> <id column="payId" property="pay_id"/> <result column="account" property="account"/> </association></resultMap>查询就是普通的关联查
<select id="getTeacher" resultMap="getTeacherMap" parameterType="int"> select * from order o left join pay p on o.order_id=p.order_id where o.order_id=#{orderId} </select>一对多<collection>
比如商品分类和商品,是一对多的关系。
实体类
public class Category { private int categoryId; private String categoryName; /** * 商品列表 **/ List<Product> products; //……}结果映射
<resultMap type="Category" id="categoryBean"> <id column="categoryId" property="category_id" /> <result column="categoryName" property="category_name" /> <!-- 一对多的关系 --> <!-- property: 指的是集合属性的值, ofType:指的是集合中元素的类型 --> <collection property="products" ofType="Product"> <id column="product_id" property="productId" /> <result column="productName" property="productName" /> <result column="price" property="price" /> </collection> </resultMap>查询
查询就是一个普通的关联查询
<!-- 关联查询分类和产品表 --> <select id="listCategory" resultMap="categoryBean"> select c.*, p.* from category_ c left join product_ p on c.id = p.cid </select> ? 那么多对一、多对多怎么实现呢?还是利用<association>和<collection>,篇幅所限,这里就不展开了。
新增标签中添加: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;MyBatis中有一些支持动态SQL的标签,它们的原理是使用OGNL从SQL参数对象中计算表达式的值,根据表达式的值动态拼接SQL,以此来完成动态SQL的功能。

第一种方法:使用foreach标签
foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有以下3种情况:
看看批量保存的两种用法:
<!-- 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>一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为SqlSession,各个SqlSession之间的缓存相互隔离,当 Session flush 或 close 之后,该 SqlSession 中的所有 Cache 就将清空,MyBatis默认打开一级缓存。

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同之处在于其存储作用域为 Mapper(Namespace),可以在多个SqlSession之间共享,并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置。

我们已经大概知道了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最复杂的部分,它的运行离不开四大组件的配合:

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 配置文件——mybatis-config.xml 、加载映射文件——映射文件即 SQL 映射文件,文件中配置了操作数据库的 SQL 语句。最后生成一个配置对象。
构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
StatementHandler:数据库会话器,串联起参数映射的处理和运行结果映射的处理。
参数处理:对输入参数的类型进行处理,并预编译。
结果处理:对返回结果的类型进行处理,根据对象映射规则,返回相应的对象。

我们一般把Mybatis的功能架构分为三层:
四个字回答:动态代理,我们来看一下获取Mapper的过程:

获取Mapper
我们都知道定义的Mapper接口是没有实现类的,Mapper映射其实是通过动态代理实现的。
BlogMapper mapper = session.getMapper(BlogMapper.class);七拐八绕地进去看一下,发现获取Mapper的过程,需要先获取MapperProxyFactory——Mapper代理工厂。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } else { try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception var5) { throw new BindingException("Error getting mapper instance. Cause: " + var5, var5); } } }MapperProxyFactory
MapperProxyFactory的作用是生成MapperProxy(Mapper代理对象)。
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; …… protected T newInstance(MapperProxy<T> mapperProxy) { return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy); } public T newInstance(SqlSession sqlSession) { MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache); return this.newInstance(mapperProxy); }}这里可以看到动态代理对接口的绑定,它的作用就是生成动态代理对象(占位),而代理的方法被放到了MapperProxy中。
MapperProxy里,通常会生成一个MapperMethod对象,它是通过cachedMapperMethod方法对其进行初始化的,然后执行excute方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession); } catch (Throwable var5) { throw ExceptionUtil.unwrapThrowable(var5); } }MapperMethod
MapperMethod里的excute方法,会真正去执行sql。这里用到了命令模式,其实绕一圈,最终它还是通过SqlSession的实例去运行对象的sql。
public Object execute(SqlSession sqlSession, Object[] args) { Object result; Object param; …… case SELECT: if (this.method.returnsVoid() && this.method.hasResultHandler()) { this.executeWithResultHandler(sqlSession, args); result = null; } else if (this.method.returnsMany()) { result = this.executeForMany(sqlSession, args); } else if (this.method.returnsMap()) { result = this.executeForMap(sqlSession, args); } else if (this.method.returnsCursor()) { result = this.executeForCursor(sqlSession, args); } else { param = this.method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(this.command.getName(), param); if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; …… }
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
Mybatis中如何指定使用哪一种Executor执行器?
插件的运行原理?
Mybatis会话的运行需要ParameterHandler、ResultSetHandler、StatementHandler、Executor这四大对象的配合,插件的原理就是在这四大对象调度的时候,插入一些我我们自己的代码。

Mybatis使用JDK的动态代理,为目标对象生成代理对象。它提供了一个工具类Plugin,实现了InvocationHandler接口。

使用Plugin生成代理对象,代理对象在调用方法的时候,就会进入invoke方法,在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会在这里被我们调用,然后就返回结果。如果不存在签名方法,那么将直接反射调用我们要执行的方法。
如何编写一个插件?
我们自己编写MyBatis 插件,只需要实现拦截器接口 Interceptor (org.apache.ibatis. plugin Interceptor ),在实现类中对拦截对象和方法进行处理。
实现Mybatis的Interceptor接口并重写intercept()方法
这里我们只是在目标对象执行目标方法的前后进行了打印;
public class MyInterceptor implements Interceptor { Properties props=null; @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("before……"); //如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法 // 如果不是它就会调用下个插件代理对象的invoke方法 Object obj=invocation.proceed(); System.out.println("after……"); return obj; }}然后再给插件编写注解,确定要拦截的对象,要拦截的方法
@Intercepts({@Signature( type = Executor.class, //确定要拦截的对象 method = "update", //确定要拦截的方法 args = {MappedStatement.class,Object.class} //拦截方法的参数)})public class MyInterceptor implements Interceptor { Properties props=null; @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("before……"); //如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法 // 如果不是它就会调用下个插件代理对象的invoke方法 Object obj=invocation.proceed(); System.out.println("after……"); return obj; }}最后,再MyBatis配置文件里面配置插件
<plugins> <plugin interceptor="xxx.MyPlugin"> <property name="dbType",value="mysql"/> </plugin></plugins> MyBatis是如何分页的?
MyBatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的原理是什么?
可以看一下一个大概的MyBatis通用分页拦截器:

[1]. MyBatis面试题(2020最新版)
[2].mybatis官网
[3].《深入浅出MyBatis基础原理与实战》
[4].聊聊MyBatis缓存机制
[5].《MyBatis从入门到精通》
