什么是MyBatis?
MyBatis 是一个开源、轻量级的数据持久化框架,是 JDBC 和 Hibernate 的替代方案。MyBatis 内部封装了 JDBC,简化了加载驱动、创建连接、创建 statement 等繁杂的过程,开发者只需要关注 SQL 语句本身。
MyBatis 支持定制化 SQL、存储过程以及高级映射,可以在实体类和 SQL 语句之间建立映射关系,是一种半自动化的 ORM 实现。其封装性低于 Hibernate,但性能优秀、小巧、简单易学、应用广泛。
Mybatis 和 iBatis?
Mybatis 比 iBatis 的改进:
- 有接口绑定,包括注解绑定 sql 和 xml 绑定 Sql
- 动态 sql 由原来的节点配置变成 OGNL 表达式
- 在一对一、一对多的时候引进了 association,在一对多的时候引入了 collection 节点,不过都是在resultMap里面配置
iBatis 和 MyBatis 区别:
- 在 sql 里面变量命名有原来的
#变量#
变成了#{变量}
,原来的$变量$
变成了${变量}
- 原来在 sql 节点里面的 class 都换名字叫 type
- 原来的 queryForObject、queryForList 变成了selectOne、selectList
- 原来的别名设置在映射文件里面放在了核心配置文件里
- iBatis 里面的核心处理类叫 SqlMapClient,MyBatis 里面的核心处理类叫做 SqlSession
MyBatis 和 Hibernate 的区别?
sql 优化方面:
- Hibernate 使用 HQL(Hibernate Query Language)语句,独立于数据库。不需要编写大量的 SQL,就可以完全映射,但会多消耗性能,且开发人员不能自主的进行 SQL 性能优化。提供了日志、缓存、级联(级联比 MyBatis 强大)等特性。
- MyBatis 需要手动编写 SQL,所以灵活多变。支持动态 SQL、处理列表、动态生成表名、支持存储过程。工作量相对较大。
开发方面:
- MyBatis 是一个半自动映射的框架,因为 MyBatis 需要手动匹配 POJO 和 SQL 的映射关系。
- Hibernate 是一个全表映射的框架,只需提供 POJO 和映射关系即可。
缓存机制比较:
- Hibernate 的二级缓存配置在 SessionFactory 生成的配置文件中进行详细配置,然后再在具体的表-对象映射中配置缓存。MyBatis 的二级缓存配置在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且 Mybatis 可以在命名空间中共享相同的缓存配置和实例,通过 cache-ref 来实现。
- Hibernate 对查询对象有着良好的管理机制,用户无需关心 SQL。所以在使用二级缓存时如果出现脏数据,系统会报出错误并提示。而 MyBatis 在这一方面,使用二级缓存时需要特别小心。如果不能完全确定数据更新操作的波及范围,避免 Cache 的盲目使用。否则脏数据的出现会给系统的正常运行带来很大的隐患。
Hibernate 优势:
- Hibernate 的 DAO 层开发比 MyBatis 简单,Mybatis 需要维护 SQL 和结果映射。
- Hibernate 对对象的维护和缓存要比 MyBatis 好,对增删改查的对象的维护要方便。
- Hibernate 数据库移植性很好,MyBatis 的数据库移植性不好,不同的数据库需要写不同 SQL。
- Hibernate 有更好的二级缓存机制,可以使用第三方缓存。MyBatis 本身提供的缓存机制不佳。
Mybatis 优势:
- MyBatis 可以进行更为细致的 SQL 优化,可以减少查询字段。
- MyBatis 容易掌握,而 Hibernate 门槛较高。
MyBatis 的缓存?
MyBatis 一级缓存最大的共享范围就是一个 SqlSession 内部,那么如果多个 SqlSession 需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示
当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个共同的 cache,也就是二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
二级缓存默认是不开启的,需要手动开启二级缓存,实现二级缓存的时候,MyBatis 要求返回的POJO必须是可序列化的。开启二级缓存的条件也是比较简单,通过直接在 MyBatis 配置文件中通过以下配置来开启二级缓存:
XML
<settings>
<setting name = "cacheEnabled" value = "true" />
</settings>
另外,还需要在 Mapper 的 xml 配置文件中加入 <cache> 标签,cache 标签有多个属性:
- eviction:缓存回收策略,有这几种回收策略
- LRU - 默认策略。最近最少回收,移除最长时间不被使用的对象
- FIFO - 先进先出,按照缓存进入的顺序来移除它们
- SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
- flushinterval:缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值
- readOnly:是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改
- size:缓存存放多少个元素
- type:指定自定义缓存的全类名(实现Cache 接口即可)
- blocking:若缓存中找不到对应的 key,是否会一直 blocking,直到有对应的数据进入缓存。
MyBatis 的工作原理?
MyBatis 四大核心对象:
- SqlSession 对象,该对象中包含了执行SQL语句的所有方法,类似于 JDBC 里面的 Connection。
- Executor 接口,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。类似于 JDBC 里面的 Statement/PrepareStatement。
- MappedStatement 对象,该对象是对映射 SQL 的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
- ResultHandler 对象,用于对返回的结果进行处理,最终得到自己想要的数据格式或类型。可以自定义返回类型。
MyBatis的工作原理如下图所示:
- 读取 MyBatis 的配置文件。
mybatis-config.xml
为 MyBatis 的全局配置文件,用于配置数据库连接信息。 - 加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件
mybatis-config.xml
中加载。mybatis-config.xml
文件可以加载多个映射文件,每个文件对应数据库中的一张表。 - 构造会话工厂。通过 MyBatis 的环境配置信息构建会话工厂 SqlSessionFactory。
- 创建会话对象。由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
- Executor 执行器。MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
- MappedStatement 对象。在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
- 输入参数映射。输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
- 输出结果映射。输出结果类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。
#{}
和${}
的区别?
#{}
是预编译处理,${}
是字符串替换。- Mybatis 在处理
#{}
时,会将 sql 中的#{}
替换为?
号,调用 PreparedStatement 的 set 方法来赋值; - Mybatis 在处理
${}
时,就是把${}
替换成变量的值。 - 使用
#{}
可以有效的防止 SQL 注入,提高系统安全性。
Mybatis 预编译?
JDBC 的预编译用法如下:
java
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/mybatis";
String user = "root";
String password = "123456";
//建立数据库连接
Connection conn = DriverManager.getConnection(url, user, password);
String sql = "insert into user(username, sex, address) values(?,?,?)";
PreparedStatement ps = conn.preparedStatement(sql);
ps.setString(1, "张三"); //为第一个问号赋值
ps.setInt(2, 2); //为第二个问号赋值
ps.setString(3, "北京"); //为第三个问号赋值
ps.executeUpdate();
conn.close();
预编译的好处:
- 预编译功能可以避免 SQL 注入,因为 SQL 已经编译完成,其结构已经固定,用户的输入只能当做参数传入进去,不能再破坏 SQL 的结果,无法造成曲解SQL原本意思的破坏。
- 预编译能提高 SQL 执行效率。当客户发送一条 SQL 语句给服务器后,服务器首先需要校验 SQL 语句的语法格式是否正确,然后把 SQL 语句编译成可执行的函数,最后才是执行 SQL 语句。其中校验语法,和编译所花的时间可能比执行 SQL 语句花的时间还要多。如果我们需要执行多次 insert 语句,但只是每次插入的值不同,MySQL 服务器也是需要每次都去校验 SQL 语句的语法格式以及编译,这就浪费了太多的时间。如果使用预编译功能,那么只对 SQL 语句进行一次语法校验和编译,所以效率要高。
预编译的实现过程:
MySQL执行预编译分为如三步:
- 第一步:执行预编译语句,如:
prepare myperson from 'select * from t_person where name=?'
- 第二步:设置变量,如:
set @name='Jim'
- 第三步:执行语句,如:
execute myperson using @name
如果需要再次执行 myperson,那么就不再需要第一步,即不需要再编译语句了:
- 设置变量,例如:
set @name='Tom'
- 执行语句,例如:
execute myperson using @name
Dao 接口的工作原理?
Dao 接口即 Mapper 接口:
- 接口的全限名,就是映射文件中的 namespace 的值;
- 接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;
- 接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个 <select>
、<insert>
、<update>
、<delete>
标签,都会被解析为一个 MapperStatement 对象。
Mapper 接口里的方法,是不能重载的,因为是使用全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。
Dao 接口和 XML 文件里的 SQL 是如何建立关系的?
(1)解析 XML
首先,Mybatis 在初始化 SqlSessionFactoryBean 的时候,找到 mapperLocations 路径去解析里面所有的 XML文件。
创建 SqlSource
Mybatis 会把每个 SQL 标签封装成 SqlSource 对象,然后根据 SQL 语句的不同,又分为动态 SQL 和静态 SQL。其中,静态 SQL 包含一段 String 类型的 sql 语句;而动态 SQL 则是由一个个 SqlNode 组成。
创建 MappedStatement
XML 文件中的每一个 SQL 标签就对应一个 MappedStatement 对象,这里面有两个属性很重要。
- id:全限定类名+方法名组成的 ID。
- sqlSource:当前 SQL 标签对应的 SqlSource 对象。
创建完 MappedStatement 对象,将它缓存到 Configuration#mappedStatements 中。
Configuration 对象就是 Mybatis 中的大管家,基本所有的配置信息都维护在这里。把所有的 XML 都解析完成之后,Configuration 就包含了所有的 SQL 信息。
到目前为止,XML 就解析完成了。当我们执行 Mybatis 方法的时候,就通过全限定类名+方法名找到 MappedStatement 对象,然后解析里面的 SQL 内容,执行即可。
(2)Dao 接口代理
首先,我们在 Spring 配置文件中,一般会这样配置:
XML
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.viewscenes.netsupervisor.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
或者你的项目是基于 SpringBoot 的,那么肯定也见过这种:
java
@MapperScan("com.xxx.dao")
它们的作用是一样的。将包路径下的所有类注册到 Spring Bean 中,并且将它们的 beanClass 设置为 MapperFactoryBean。MapperFactoryBean 实现了 FactoryBean 接口,俗称工厂 Bean。那么,当通过 @Autowired
注入这个 Dao 接口的时候,返回的对象就是 MapperFactoryBean 这个工厂 Bean 中的 getObject()
方法对象。
简单来说,它就是通过 JDK 动态代理,返回了一个 Dao 接口的代理对象,这个代理对象的处理器是 MapperProxy 对象。所有,我们通过 @Autowired
注入 Dao 接口的时候,注入的就是这个代理对象,我们调用到 Dao 接口的方法时,则会调用到 MapperProxy 对象的 invoke 方法。
(3)执行
如上所述,当我们调用 Dao 接口方法的时候,实际调用到代理对象的 invoke 方法。 在这里,实际上调用的就是 SqlSession 里面的东西了。
java
public class DefaultSqlSession implements SqlSession {
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms,
wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
}
}
是通过 statement 全限定类型+方法名拿到 MappedStatement 对象,然后通过执行器 Executor 去执行具体 SQL 并返回。
接口映射有几种实现方式?
接口映射(接口绑定)就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL 语句绑定,我们直接调用接口方法就可以,这样比原来 SqlSession 提供的方法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式:
- 一种是通过注解绑定,就是在接口的方法上面加上
@Select
、@Update
等注解里面包含 Sql 语句来绑定。 - 另外一种就是通过 xml 里面写 SQL 来绑定,在这种情况下要指定 xml 映射文件里面的 namespace 必须为接口的全路径名。
模糊查询 like 语句怎么写?
第1种:在 Java 代码中添加 sql 通配符。
java
// java
string wildcardname = "%tom%";
list<name> names = mapper.selectLike(wildcardname);
// xml
<select id="selectLike">
select * from users where name like #{value}
</select>
第2种:在 sql 语句中拼接通配符,利用 sql 的 concat 函数。
java
// java
string wildcardname = "tom";
list<name> names = mapper.selectLike(wildcardname);
// xml
<select id="selectLike">
select * from users where name like concat("%", #{value}, "%")
</select>
当实体类中的属性名和表中的字段名不一样,怎么办 ?
第1种解决方案:通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
XML
<select id="getOrder" parametertype="int" resultetype="cn.mybatis.domain.order">
select order_id id, order_no orderNo ,order_price price form orders where order_id=#{id};
</select>
第2种解决方案:通过<resultMap>
来映射字段名和实体类属性名的一一对应的关系。
XML
<select id="getOrder" parameterType="int" resultMap="orderResultMap">
select * from orders where order_id=#{id}
</select>
<resultMap id="orderResultMap" type="cn.mybatis.domain.order" >
<!--用id属性来映射主键字段-->
<id property="id" column="order_id">
<!--用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性-->
<result property= "orderNo" column="order_no"/>
<result property="price" column="order_price"/>
</reslutMap>
Mybatis 如何进行分页?
Mybatis 内部使用 RowBounds 对象进行分页,需要注意的是,它是针对 ResultSet 结果集执行的内存分页,而非数据库分页。
所以,生产环境中,不适合直接使用 MyBatis 原有的 RowBounds 对象进行分页。而是使用如下两种方案:
在 SQL 内手动书写数据库分页的参数来完成分页功能,样例代码如下:
sql
select * from t_user limit #{start}, #{pageSize}
也可使用开源的分页插件来完成数据库分页,如:
- Mybatis-PageHelper
- MyBatis-Plus
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义分页插件。在插件的拦截方法内,拦截待执行的 SQL ,然后重写 SQL ,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。
举例:SELECT * FROM t_user
,拦截 SQL 后重写为:select * FROM student LIMIT 0,10
。
Mybatis 的插件运行原理?
Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这4种接口的插件,Mybatis 通过动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的invoke()
方法,当然,只会拦截那些你指定需要拦截的方法。
实现 Mybatis 的 Interceptor 接口并复写intercept()
方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,别忘了在配置文件中配置你编写的插件。
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 一对一、一对多查询?
Mybatis 不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询。
多对一查询,其实就是一对一查询,只需要把selectOne()
修改为selectList()
即可;多对多查询,其实就是一对多查询,只需要把selectOne()
修改为selectList()
即可。
关联对象查询,有两种实现方式,一种是单独发送一个 sql 去查询关联对象,赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用 join 查询,一部分列是A对象的属性值,另外一部分列是关联对象B的属性值,好处是只发一个 sql 查询,就可以把主对象和其关联对象查出来。
MyBatis 里面的动态 Sql?
Mybatis 动态 SQL ,可以让我们在 XML 映射文件内,添加条件判断标签,达到动态拼接 SQL 的功能。Mybatis 提供了 9 种动态 SQL 标签,如下:
<if />
<choose />
<when />
<otherwise />
<trim />
<where />
<set />
<foreach />
<bind />
动态 SQL 执行原理为,内部使用 OGNL 的表达式,从 SQL 参数对象中计算表达式的值,根据表达式的值动态拼接 SQL ,以此来完成动态 SQL 的功能。
Xml 映射文件中的标签?
还有很多其他的标签,<resultMap>
、<parameterMap>
、<sql>
、<include>
、<selectKey>
,加上动态sql的9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind
等,其中<sql>
为 sql 片段标签,通过<include>
标签引入 sql 片段,<selectKey>
为不支持自增的主键生成策略标签。
include 标签定义的位置?
Mybatis 映射文件中,如果A标签通过 include 引用了B标签的内容,B标签定义的位置是可以在A标志之前,也可以在之后的。
虽然 Mybatis 解析 xml 映射文件是按照顺序解析的,但是被引用的B标签依然可以定义在任何地方,Mybatis 都可以正确识别。
原理是 Mybatis 解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时 Mybatis 会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis 会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。
不同 xml 文件,id是否可以重复?
不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;
原因就是 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 中
- BatchExecutor:完成批处理。
Mybatis 中如何执行批处理?
使用 BatchExecutor 完成批处理。
BatchExecutor 仅对修改操作(包括删除)有效哈 ,对 select 操作是不起作用。BatchExecutor 主要是用于做批量更新操作的,底层会调用 Statement 的 executeBatch()
方法实现批量操作。
Mybatis 中如何指定 Executor 执行器?
在 Mybatis 配置文件中,可以指定默认的 ExecutorType 执行器类型,也可以手动给 DefaultSqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数。
Mybatis 批量插入能返回主键列表吗?
Mybatis 在插入单条数据的时候有两种方式返回自增主键:
- 对于支持生成自增主键的数据库:增加 useGenerateKeys 和 keyProperty ,
<insert>
标签属性。 - 不支持生成自增主键的数据库:使用
<selectKey>
。
批量插入,其实一样的原理:
XML
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
insert into student (name, score) values
<foreach collection="list" item="item" index="index" separator=",">
(#{item.name}, #{item.score})
</foreach>
</insert>
Mybatis 能否映射枚举类?
Mybatis 可以映射枚举类,不单可以映射枚举类,Mybatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler,实现 TypeHandler 的 setParameter()
和 getResult()
接口方法。
TypeHandler 有两个作用,一是完成从 javaType 至 jdbcType 的转换,二是完成 jdbcType 至 javaType 的转换,体现为 setParameter()
和 getResult()
两个方法,分别代表设置 sql 问号占位符参数和获取列查询结果。
在 mapper 中传递多个参数?
- 直接在方法中传递参数,xml 文件用
#{0}
、#{1}
来获取 - 使用
@param
注解,这样可以直接在 xml 文件中通过#{name}
来获取
resultType、resultMap 的区别?
- 类的名字和数据库相同时,可以直接设置 resultType 参数为 Pojo 类
- 若不同,需要设置 resultMap 将结果名字和 Pojo 名字进行转换
mapper 接口的使用要求?
- Mapper 接口方法名和
mapper.xml
中定义的每个 sql 的 id 相同 - Mapper 接口方法的输入参数类型和
mapper.xml
中定义的每个 sql 的 parameterType 的类型相同 - Mapper 接口方法的输出参数类型和
mapper.xml
中定义的每个 sql 的 resultType 的类型相同 Mapper.xml
文件中的 namespace 即是 mapper 接口的类路径。