
先吹一下:
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
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 中的
- select * from orders where order_id=#{id}
-
type="com.jourwon.pojo.Order" id="orderResultMap"> - id属性来映射主键字段–>
- <id property="id" column="order_id">
-
-
"orderno" column ="order_no"/> -
"price" column="order_price" /> - 复制代码


- <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>
- 复制代码
当然可以,不止支持一对一、一对多的关联查询,还支持多对多、多对一的关联查询。

一对一
比如订单和支付是一对一的关系,这种关联的实现:
实体类:
- public class Order {
- private Integer orderId;
- private String orderDesc;
-
- /**
- * 支付对象
- */
- private Pay pay;
- //……
- }
- 复制代码
结果映射
- <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>
- 复制代码
一对多
比如商品分类和商品,是一对多的关系。
实体类
- public class Category {
- private int categoryId;
- private String categoryName;
-
- /**
- * 商品列表
- **/
- List
products; - //……
- }
- 复制代码
结果映射
- <resultMap type="Category" id="categoryBean">
- <id column="categoryId" property="category_id" />
- <result column="categoryName" property="category_name" />
-
-
-
- <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>
- 复制代码
那么多对一、多对多怎么实现呢?还是利用
新增标签中添加:keyProperty=" ID " 即可
id="insert" useGeneratedKeys="true" keyProperty="userId" > - insert into user(
- user_name, user_password, create_time)
- values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP})
- 复制代码
这时候就可以完成回填主键
- 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种情况:
看看批量保存的两种用法:
- //推荐使用
- <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>
- 复制代码
-
- <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);
- }
- 复制代码
"com.jourwon.mapper.EmployeeMapper" -
-
id="addEmp"> - insert into employee(lastName,email,gender)
- values(#{lastName},#{email},#{gender})
-
- 复制代码
一级缓存: 基于 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
List 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
List 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 {
-
List handleResultSets(Statement var1) throws SQLException; -
-
Cursor 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 getMapper(Class type, SqlSession sqlSession ) { - MapperProxyFactory
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
mapperInterface; - ……
- protected T newInstance(MapperProxy
mapperProxy) { - return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
- }
-
- public T newInstance(SqlSession sqlSession) {
- MapperProxy
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通用分页拦截器:

工众号【不脱发有志青年】货区更多面试免费资料