一、 什么是MyBatis?
MyBatis是一个半ORM(对象关系映射)框架,它内部包装了JDBC,允许开发人员编写简洁的SQL语句,并且可以使用动态SQL和参数映射来进行高效和灵活的数据操作。MyBatis提供了许多高级功能,如缓存机制、批量操作、分页等,并可以与多种数据库和Web框架无缝集成。
二、#{} 和 ${} 的区别是什么?
答:
-
${}
:直接将参数值替换到SQL语句中。在SQL语句执行前,会直接将{}替换为对应的参数值,这种方式的好处是可以直接拼接字符串,但也带来了一些安全问题。使用${}时需要开发人员自行保证参数的合法性,否则可能会出现SQL注入等安全问题。
一个示例:根据参数按任意字段排序:
select * from users order by ${orderCols}
orderCols
可以是 name
、name desc
、name,sex asc
等,实现灵活的排序。
-
#{}:使用预编译的方式来处理SQL语句中的参数,将传入的参数值以安全的方式替换掉占位符。在SQL语句执行前,会先将#{}替换为一个问号占位符,然后使用PreparedStatement进行预编译,最后将实际的参数值设置到预编译语句中。使用#{}可以有效地防止SQL注入等安全问题,同时也可以避免一些数据类型转换的问题。
三、xml 映射文件中,除了常见的 select、insert、update、delete 标签之外,还有哪些标签?
答:还有很多其他的标签, <resultMap>
、 <parameterMap>
、 <sql>
、 <include>
、 <selectKey>
,加上动态 sql 的 9 个标签, trim|where|set|foreach|if|choose|when|otherwise|bind
等,其中 <sql>
为 sql 片段标签,通过 <include>
标签引入 sql 片段, <selectKey>
为不支持自增的主键生成策略标签
四、Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?
注:这道题也是京东面试官面试我被问的。
答:最佳实践中,通常一个 xml 映射文件,都会写一个 Dao 接口与之对应。Dao 接口就是人们常说的 Mapper
接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement
的 id 值,接口方法内的参数,就是传递给 sql 的参数。 Mapper
接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement
,举例:com.mybatis3.mappers. StudentDao.findStudentById
,可以唯一找到 namespace 为 com.mybatis3.mappers. StudentDao
下面 id = findStudentById
的 MappedStatement
。在 MyBatis 中,每一个 <select>
、 <insert>
、 <update>
、 <delete>
标签,都会被解析为一个 MappedStatement
对象。
Dao 接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao 接口里的方法可以重载,但是 Mybatis 的 xml 里面的 ID 不允许重复。
Mybatis 版本 3.3.0,亲测如下:
/**
* Mapper接口里面方法重载
*/
public interface StuMapper {
List<Student> getAllStu();
List<Student> getAllStu(@Param("id") Integer id);
}
然后在 StuMapper.xml
中利用 Mybatis 的动态 sql 就可以实现。
<select id="getAllStu" resultType="com.pojo.Student">
select * from student
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>
能正常运行,并能得到相应的结果,这样就实现了在 Dao 接口中写重载方法。
Mybatis 的 Dao 接口可以有多个重载方法,但是多个接口对应的映射必须只有一个,否则启动会报错。
Dao 接口的工作原理是 JDK 动态代理,MyBatis 运行时会使用 JDK 动态代理为 Dao 接口生成代理 proxy 对象,代理对象 proxy 会拦截接口方法,转而执行 MappedStatement
所代表的 sql,然后将 sql 执行结果返回。
补充:
Dao 接口方法可以重载,但是需要满足以下条件:
-
仅有一个无参方法和一个有参方法
-
多个有参方法时,参数数量必须一致。且使用相同的
@Param
,或者使用param1
这种
测试如下:
PersonDao.java
Person queryById();
Person queryById(@Param("id") Long id);
Person queryById(@Param("id") Long id, @Param("name") String name);
PersonMapper.xml
<select id="queryById" resultMap="PersonMap">
select
id, name, age, address
from person
<where>
<if test="id != null">
id = #{id}
</if>
<if test="name != null and name != ''">
name = #{name}
</if>
</where>
limit 1
</select>
org.apache.ibatis.scripting.xmltags. DynamicContext. ContextAccessor#getProperty
方法用于获取 <if>
标签中的条件值
public Object getProperty(Map context, Object target, Object name) {
Map map = (Map) target;
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
}
return null;
}
parameterObject
为 map,存放的是 Dao 接口中参数相关信息。
((Map)parameterObject).get(name)
方法如下
public V get(Object key) {
if (!super.containsKey(key)) {
throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
}
return super.get(key);
}
-
queryById()
方法执行时,parameterObject
为 null,getProperty
方法返回 null 值,<if>
标签获取的所有条件值都为 null,所有条件不成立,动态 sql 可以正常执行。 -
queryById(1L)
方法执行时,parameterObject
为 map,包含了id
和param1
两个 key 值。当获取<if>
标签中name
的属性值时,进入((Map)parameterObject).get(name)
方法中,map 中 key 不包含name
,所以抛出异常。 -
queryById(1L,"1")
方法执行时,parameterObject
中包含id
,param1
,name
,param2
四个 key 值,id
和name
属性都可以获取到,动态 sql 正常执行
五、MyBatis 是如何进行分页的?分页插件的原理是什么?
答:MyBatis 仅可以编写针对 ParameterHandler
、 ResultSetHandler
、 StatementHandler
、 Executor
这 4 种接口的插件,MyBatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler
的 invoke()
方法,当然,只会拦截那些你指定需要拦截的方法。
实现 MyBatis 的 Interceptor
接口并复写 intercept()
方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件
六、MyBatis 动态 sql 是做什么的?都有哪些动态 sql?能简述一下动态 sql 的执行原理不?
1、动态 SQL 的作用
-
实现条件查询:开发人员可以根据特定条件动态更改查询逻辑,而无需编写多个固定的 SQL 查询。
-
拼接 SQL 语句:开发人员可以在运行时根据具体需求构建 SQL 语句,从而适应更复杂的业务逻辑。
-
提高代码重用性:通过定义可重复使用的 SQL 片段,并通过标签在不同的查询中重用它们,减少重复的 SQL 代码。
-
增强 SQL 语句的灵活性:允许开发人员根据具体的业务需求精确地控制查询的行为。
-
避免 SQL 注入风险:通过使用参数化查询和避免字符串拼接,可以增强应用程序的安全性。
2、动态 SQL 的类型
MyBatis 提供了多种动态 SQL 标签,用于在 XML 映射文件中编写动态 SQL 语句。这些标签包括:
-
<if>:用于在生成的 SQL 语句中添加条件判断。根据指定的条件决定是否包含某个 SQL 语句片段。
-
<choose>、<when> 和 <otherwise>:类似于 Java 中的 switch 语句,根据条件选择执行不同的 SQL 语句片段。
-
<where>:用于在生成的 SQL 语句中添加 WHERE 子句。它可以自动处理条件语句的前缀,并在有条件语句存在时添加 WHERE 关键字。
-
<set>:用于在生成的 SQL 语句中添加 SET 子句。它主要用于更新操作,可以根据条件来动态生成需要更新的列。
-
<foreach>:用于在生成的 SQL 语句中进行循环操作。它可以遍历集合或数组,并根据指定的模板将集合元素或数组元素插入到 SQL 语句中。
-
<trim>:通过修剪 SQL 语句的开头和结尾来动态生成 SQL 片段。它可以用于去除不必要的 SQL 关键字或条件语句,并提供了一些属性来定义修剪规则。
-
<bind>:用于将表达式的结果绑定到一个变量上。可以在 SQL 语句中使用这个变量,避免重复计算表达式。
3、动态 SQL 的执行原理
动态 SQL 的执行原理主要基于以下步骤:
-
解析 SQL 映射文件:MyBatis 在启动时解析 XML 映射文件,读取其中的 SQL 语句和动态 SQL 标签。
-
计算表达式:在 SQL 执行时,MyBatis 使用 OGNL(Object-Graph Navigation Language)从 SQL 参数对象中计算表达式的值。
-
动态拼接 SQL:根据表达式的值,MyBatis 动态拼接 SQL 语句,生成最终的 SQL 查询或更新语句。
-
执行 SQL 语句:将生成的 SQL 语句发送到数据库执行,并获取查询结果或执行结果。
-
处理查询结果:将查询结果映射到 Java 对象中,并返回给调用方。
七、MyBatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?
答:
第一种是使用 <resultMap>
标签,逐一定义列名和对象属性名之间的映射关系。
第二种是使用 sql 列的别名功能,将列别名书写为对象属性名,比如 T_NAME AS NAME,对象属性名一般是 name,小写,但是列名不区分大小写,MyBatis 会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成 T_NAME AS NaMe,MyBatis 一样可以正常工作。
有了列名与属性名的映射关系后,MyBatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的
八、MyBatis 能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
1、多对一和多对多查询与一对一、一对多查询的关联
-
多对一查询:本质上和一对一查询类似。在实际操作中,如果原本是使用类似
selectOne()
这样的方法来执行一对一查询获取单个结果,当遇到多对一的情况时,只需将其修改为selectList()
方法即可。这是因为多对一关系意味着可能有多个源对象对应一个目标对象,所以查询结果可能是多个,需要用selectList()
来获取并处理。 -
多对多查询:与一对多查询有相似之处。同样地,把用于一对多查询中获取单个关联对象集合的
selectOne()
方法修改为selectList()
方法,就可以适用于多对多查询场景。因为多对多关系下,两边的对象都可能存在多个对应关系,查询结果往往是多个相关对象的集合。
2、 关联对象查询的两种实现方式
方式一:单独查询关联对象再赋值给主对象
这种方式是先单独发送一个 SQL 语句去查询关联对象,得到关联对象的结果后,再将其赋给主对象,最后返回完整的主对象。例如,先查询出一个用户对象,然后再单独发一个 SQL 去查询该用户对应的地址对象,把查到的地址对象赋给用户对象的相应属性,这样就构成了一个包含关联地址信息的完整用户对象返回。
方式二:嵌套查询(使用 JOIN)
-
含义:嵌套查询这里指的是使用
JOIN
操作来进行查询。在查询结果中,一部分列是主对象(比如A
对象)的属性值,另外一部分列是关联对象(比如B
对象)的属性值。通过这样一次JOIN
查询,就能够把主对象及其关联对象的相关信息一起查询出来。比如查询教师(Teacher)和学生(Student)的关联信息,通过JOIN
可以在一个查询结果中同时包含教师的相关列信息和学生的相关列信息。 -
好处:相较于单独查询关联对象再赋值的方式,这种嵌套查询只需要发送一个 SQL 查询语句,就能够获取到完整的主对象及其关联对象信息,减少了数据库交互次数,在一定程度上可以提高查询效率。
3、JOIN 查询结果去重复原理
当使用 JOIN
查询出来多条记录时,比如查询出了 100 条记录,如何确定其中主对象是 5 个而不是 100 个呢?这里就涉及到 MyBatis 的去重复机制。
MyBatis 是通过 <resultMap>
标签内的 <id>
子标签来实现去重复功能的。 <id>
子标签指定了能够唯一确定一条记录的 id
列(这里的 id
不一定是数据库表中的主键,也可以是根据业务逻辑确定的能够唯一标识一条记录的列组合,甚至可以有多个列构成联合主键的情况,此时就体现了联合主键的语意)。MyBatis 会根据 <id>
列的值来对查询出来的 100 条记录进行去重复处理,只保留具有不同 <id>
值的记录作为不同的主对象。
同样地,对于主对象的关联对象,也是依据这个原理来进行去重复处理的。不过在一般情况下,通常是主对象可能会出现重复记录(因为 JOIN
操作可能会导致主对象的信息在结果集中多次出现),而关联对象一般不会出现重复情况(当然这也取决于具体的数据和业务逻辑)。
例如给出的例子中,通过 JOIN
查询出来 6 条记录,其中第一、二列是 Teacher
对象列,第三列是 Student
对象列。经过 MyBatis 的去重复处理后,最终结果是确定为 1 个老师和 6 个学生,而不是错误地认为是 6 个老师和 6 个学生。这就是通过 <id>
子标签指定的列来准确区分不同的主对象以及对其关联对象进行正确去重复处理的结果。
t_id | t_name | s_id |
---|---|---|
1 | teacher | 38 |
1 | teacher | 39 |
1 | teacher | 40 |
1 | teacher | 41 |
1 | teacher | 42 |
1 | teacher | 43 |
九、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,支持延迟加载的原理都是一样的
十、MyBatis 的 xml 映射文件中,不同的 xml 映射文件,id 是否可以重复
答不同的 xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;毕竟 namespace 不是必须的,只是最佳实践而已。
原因就是 namespace+id 是作为 Map<String, MappedStatement>
的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同
十一、MyBatis 都有哪些 Executor 执行器?它们之间的区别是什么?
MyBatis 有三种基本的 Executor
执行器:
-
SimpleExecutor
: 每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。 -
ReuseExecutor
: 执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement>内,供下一次使用。简言之,就是重复使用 Statement 对象。 -
BatchExecutor
:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同。
作用范围:Executor
的这些特点,都严格限制在 SqlSession 生命周期范围内
十二、MyBatis 是否可以映射 Enum 枚举类?
MyBatis 可以映射枚举类,不单可以映射枚举类,MyBatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler
,实现 TypeHandler
的 setParameter()
和 getResult()
接口方法。 TypeHandler
有两个作用:
-
一是完成从 javaType 至 jdbcType 的转换;
-
二是完成 jdbcType 至 javaType 的转换,体现为
setParameter()
和getResult()
两个方法,分别代表设置 sql 问号占位符参数和获取列查询结果。
十三、简述 MyBatis 的 xml 映射文件和 MyBatis 内部数据结构之间的映射关系?
MyBatis 将所有 xml 配置信息都封装到 All-In-One 重量级对象 Configuration 内部。
在 xml 映射文件中, <parameterMap>
标签会被解析为 ParameterMap
对象,其每个子元素会被解析为 ParameterMapping 对象。
<resultMap>
标签会被解析为 ResultMap
对象,其每个子元素会被解析为 ResultMapping
对象。每一个 <select>、<insert>、<update>、<delete>
标签均会被解析为 MappedStatement
对象,标签内的 sql 会被解析为 BoundSql 对象
十四、为什么说 MyBatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?
Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 MyBatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
面试题看似都很简单,但是想要能正确回答上来,必定是研究过源码且深入的人,而不是仅会使用的人或者用的很熟的人,以上所有面试题及其答案所涉及的内容,在我的 MyBatis 系列博客中都有详细讲解和原理分析
十五、MyBatis 映射文件中,如果 A 标签通过 include 引用了 B 标签的内容,请问,B 标签能否定义在 A 标签的后面,还是说必须定义在 A 标签的前面?
虽然 MyBatis 解析 xml 映射文件是按照顺序解析的,但是,被引用的 B 标签依然可以定义在任何地方,MyBatis 都可以正确识别。
原理是,MyBatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,MyBatis 会将 A 标签标记为未解析状态,然后继续解析余下的标签,包含 B 标签,待所有标签解析完毕,MyBatis 会重新解析那些被标记为未解析的标签,此时再解析 A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了