Mybaits概述

原理

当调用Mapper接口方法的时候,Mybatis会使用JDK动态代理返回一个Mapper代理对象,代理对象会拦截接口方法,根据接口的全路径和方法名,定位到sql,使用executor执行sql语句,然后将sql执行结果返回。

因为mybatis动态代理寻找策略是全限定名+方法名,不涉及参数,所以不支持重载。

优缺点

优点:

  1. SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
  2. 开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
  3. 与各种数据库兼容。

缺点:

  1. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

传统的JDBC编程

JDBC定义了连接数据库的接口规范,每个数据库厂商都会提供具体的实现,JDBC是一种典型的桥接模式。

  • 获取数据库连接;
  • 操作Connection,打开Statement对象;
  • 通过Statement对象执行SQL,返回结果到ResultSet对象;
  • 关闭数据库资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class javaTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
String URL="jdbc:mysql://127.0.0.1:3306/imooc?useUnicode=true&characterEncoding=utf-8";
String USER="root";
String PASSWORD="tiger";
//1.加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//2.获得数据库链接
Connection conn=DriverManager.getConnection(URL, USER, PASSWORD);
//3.通过数据库的连接操作数据库,实现增删改查(使用Statement类)
Statement st=conn.createStatement();
ResultSet rs=st.executeQuery("select * from user");
//4.处理数据库的返回结果(使用ResultSet类)
while(rs.next()){
System.out.println(rs.getString("user_name")+" "
+rs.getString("user_password"));
}

//关闭资源
rs.close();
st.close();
conn.close();
}
}

Hibernate与Mybatis

Hibernate建立在POJO和数据库表模型的直接映射关系上。通过POJO我们可以直接操作数据库的数据。相对而言,Hibernate对JDBC的封装程度比较高,我们不需要编写SQL,直接通过HQL去操作POJO进而操作数据库的数据。

Mybatis是半自动映射的orm框架,它需要我们提供POJO,SQL和映射关系,而全表映射的Hibernate只需要提供POJO和映射关系。

Hibernate编程简单,需要我们提供映射的规则,完全可以通过IDE实现,同时无需编写SQL,开发效率优于Mybatis。此外,它提供缓存、级联、日志等强大的功能,

Hibernate与Mybatis区别:

  1. Hibernate是全自动,而Mybatis是半自动。 Hibernate是全表映射,可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而Mybatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写sql来实现和管理。
  2. Hibernate数据库移植性较好。 Hibernate通过它强大的映射结构和hql语言,大大降低了对象与数据库的耦合性,而Mybatis由于需要手写sql,因此与数据库的耦合性直接取决于程序员写sql的方法,如果sql不具通用性而用了很多某数据库特性的sql语句的话,移植性也会随之降低很多,成本很高。
  3. Hibernate拥有完整的日志系统,Mybatis则欠缺一些。 Hibernate日志系统非常健全,涉及广泛,包括:sql记录、关系异常、优化警告、缓存提示、脏数据警告等;而Mybatis则除了基本记录功能外,功能薄弱很多。
  4. sql直接优化上,Mybatis要比Hibernate方便很多。 由于Mybatis的sql都是写在xml里,因此优化sql比Hibernate方便很多,解除了sql与代码的耦合。而Hibernate的sql很多都是自动生成的,无法直接维护sql;写sql的灵活度上Hibernate不及Mybatis。
  5. Mybatis提供xml标签,支持编写动态sql。

Mybatis组件的生命周期

  1. SqlSessionFactoryBuilder

    作用是生成SqlSessionFactory,构建完毕则作用完结,生命周期只存在于方法的局部。

  2. SqlSessionFactory

    创建SqlSession,每次访问数据库都需要通过SqlSessionFactory创建SqlSession。故SqlSessionFactory应存在于Mybatis应用的整个生命周期。

  3. SqlSession

    会话,相当于JDBC的Connection对象,生命周期为请求数据库处理事务的过程。

  4. Mapper

    作用是发送SQL,返回结果或执行SLQ修改数据库数据,它的生命周期在一个SqlSession事务方法之内。其最大的作用范围和SqlSession相同。

数据库事务

Mybatis数据库事务由SqlSession控制,我们可以通过SqlSession提交或回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void insertRoleTest() {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtil.openSqlSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);

Role role = new Role();
role.setId(2L);
role.setNote("hi");
role.setRoleName("teacher");

roleMapper.insertRole(role);
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
sqlSession.rollback();
} finally {
if(sqlSession != null) {
sqlSession.close();
}
}
}

MyBatis 都有哪些 Executor 执行器?它们之间的区别是什么?

MyBatis 有三种基本的 Executor 执行器,SimpleExecutorReuseExecutorBatchExecutor

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 生命周期范围内。

映射器

select元素

元素 说明 备注
id 和Mapper命名空间的组合是唯一的 命名空间和id组合不唯一,则抛异常
parameterType 类的全路径或者别名 基本数据类型,JavaBean,Map等
resultType 基本数据类型或者类的全路径,可使用别名(需符合别名规范) 允许自动匹配的请况下,结果集将通过JavaBean的规范映射,不能和resultMap同时使用
resultMap 自定义映射规则 Mybatis最复杂的元素,可以配置映射规则、级联、typeHandler等

自动映射

autoMappingBehavior不为NONE时,Mybatis会提供自动映射的功能,只要返回的列名和JavaBean的属性一致,Mybatis就会帮助我们回填这些字段。实际上大部分数据库规范使用下划线分割单词,而Java则是用驼峰命名法,于是需要使用列的别名使得Mybatis能够自动映射,或者在配置文件中开启驼峰命名方式。

1
2
3
4
<!--SQL列的别名与pojo的属性一样,则SQL查询的结果会自动映射到pojo-->
<select id="getRole" parameterType="long" resultMap="roleMap">
SELECT id, role_name as roleName, note FROM role WHERE id = #{id}
</select>

自动映射可以在setting元素中配置autoMappingBehavior属性值设定其策略。包含三个值:

  • NONE,取消自动映射
  • PARTIAL,只会自动映射,没有定义嵌套结果集映射的结果集
  • FULL,会自动映射任意复杂的结果集(无论是否嵌套)

默认值是PARTIAL,默认情况下可以做到当前对象的映射,使用FULL是嵌套映射,性能会下降。

如果数据库是规范命名的,即每个单词用下划线分隔,而POJO是驼峰式命名的方式,此时可设置mapUnderscoreToCamelCase为true,这样就可以实现从数据库到POJO的自动映射了。

传递多个参数

1.使用注解方式传递参数

1
public List<Role> findRoleByCondition(@Param("roleName") String roleName, @Param("note")String note);

RoleMapper.xml

1
2
3
4
5
<select id="findRoleByCondition" resultMap="roleMap">
SELECT id, role_name, note FROM role
WHERE role_name like concat('%', #{roleName}, '%')
and note like concat('%', #{note}, '%')
</select>

2.使用JavaBean传递参数

将参数组织成JavaBean,通过getter和setter方法设置参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RoleParam {
private String roleName;
private String note;

public String getRoleName() {
return roleName;
}

public void setRoleName(String roleName) {
this.roleName = roleName;
}

public String getNote() {
return note;
}

public void setNote(String note) {
this.note = note;
}
}

接口RoleMapper

1
public List<Role> findRoleByParams2(RoleParam roleParam);

RoleMapper.xml

1
2
3
4
5
<select id="findRoleByParams2" parameterType="com.tyson.pojo.RoleParam" resultMap="roleMap">
SELECT id, role_name, note FROM role
WHERE role_name like concat('%', #{roleName}, '%')
and note like concat('%', #{note}, '%')
</select>

参数个数多于5,建议使用JavaBean方式。

使用resultMap映射结果集

1
2
3
4
5
6
7
8
9
10
11
12
<resultMap id="roleMap" type="role">
<id column="id" property="id" javaType="long" jdbcType="BIGINT"/>
<result column="role_name" property="roleName" javaType="string" jdbcType="VARCHAR"/>
<!--定义结果类型处理器标识-->
<result column="note" property="note" typeHandler="com.tyson.typeHandler.MyStringTypeHandler"/>
</resultMap>

<select id="findRoleByParams1" resultMap="roleMap">
SELECT id, role_name, note FROM role
WHERE role_name like concat('%', #{roleName}, '%')
and note like concat('%', #{note}, '%')
</select>

insert元素

执行插入之后会返回一个整数,表示插入的记录数。parameterType 为 role(mybatis-config.xml 定义的别名)。

1
2
3
<insert id="insertRole" parameterType="role">
INSERT into role(id, role_name, note) VALUES(#{id}, #{roleName}, #{note})
</insert>

主键回填

设计表的时候有两种主键,一种自增主键,一般为int类型,一种为非自增的主键,例如用uuid等。

自增主键

role表指定id字段为自增字段,对应的Role实体类提供getter和setter方法,便可以使用Mybatis的主键回填功能。通过keyProperty指定主键字段,并使用useGeneratedKeys告诉Mybatis这个主键是否使用数据库内置策略生成。

1
2
3
4
5
   <!--useGeneratedKeys:默认false,使MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
keyProperty:默认值unset,用于设置getGeneratedKeys方法或selectKey子元素返回值将赋值到哪个属性中-->
<insert id="insertRoleUseGeneratedKeys" parameterType="role" useGeneratedKeys="true" keyProperty="id">
INSERT into role(role_name, note) VALUES(#{roleName}, #{note})
</insert>

传入的role无需设置id,Mybatis在插入记录时会自动回填主键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void insertRoleUseGeneratedKeysTest() {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtil.openSqlSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);

Role role = new Role();
role.setNote("hello");
role.setRoleName("worker");

roleMapper.insertRoleUseGeneratedKeys(role);
log.info(role.toString());
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
sqlSession.rollback();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}

也可以通过selectKey设置主键回填。

1
2
3
4
5
6
7
8
9
10
11
12
13
<insert id="insertRole" parameterType="role">
<!--selectKey会将 SELECT LAST_INSERT_ID()的结果放入到传入的pojo的主键;
keyProperty 对应的pojo中的主键的属性名;
order设置为BEFORE,先执行selectKey语句(SELECT LAST_INSERT_ID()),然后执行插入语句;
order设置为AFTER,先执行插入语句,然后执行selectKey语句;
SELECT LAST_INSERT_ID():得到刚insert 进去记录的主键值,只适用与自增主键;
resultType:主键类型
-->
<selectKey keyProperty="id" resultType="long" order="AFTER">
select LAST_INSERT_ID()
</selectKey>
INSERT into role(role_name, note) VALUES(#{roleName}, #{note})
</insert>
非自增主键

假设增加如下需求,当表role没有记录时,则插入第一条记录时id设为1,否则取最大的id加2,设置为新的主键,这个时候可以使用selectKey来处理。

1
2
3
4
5
6
7
<insert id="myInsertRole" parameterType="role">
<!--order为BEFORE,selectKey语句在insert语句插入之前执行-->
<selectKey keyProperty="id" resultType="java.lang.Long" order="BEFORE">
select if(max(id) is null, 1, max(id) + 2) as newId from role
</selectKey>
INSERT into role(id, role_name, note) VALUES(#{id}, #{roleName}, #{note})
</insert>

selectKey标签的语句会被先执行,然后把查询到的id放到role对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void myInsertRoleTest() {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtil.openSqlSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);

Role role = new Role();
role.setNote("hello");
role.setRoleName("worker");

roleMapper.myInsertRole(role);
log.info(role.toString());
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
sqlSession.rollback();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}

假设主键是VARCHAR类型,以uuid()方式生成主键。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tyson.mapper.CustomerMapper">
<insert id="insertCustomer" parameterType="com.tyson.pojo.Customer">
<!--order为BEFORE,uuid()在insert语句插入之前执行-->
<selectKey keyProperty="id" resultType="java.lang.String" order="BEFORE">
select uuid()
</selectKey>
insert customer(id, name) values(#{id}, #{name})
</insert>
</mapper>

update和delete元素

update和delete元素用于更新记录和删除记录。插入和删除记录执行完成会返回一个整数,表示插入或删除几条记录。

1
2
3
4
5
6
7
8
9
10
   <update id="updateRole" parameterType="role">
update role set
role_name = #{roleName},
note = #{note}
where id = #{id}
</update>

<delete id="deleteRole" parameterType="long">
delete from role where id = #{id}
</delete>

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 接口,就是人们常说的 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。 Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement ,举例: com.mybatis3.mappers. StudentDao.findStudentById ,可以唯一找到 namespace 为 com.mybatis3.mappers. StudentDao 下面 id = findStudentByIdMappedStatement 。在 MyBatis 中,每一个 <select><insert><update><delete> 标签,都会被解析为一个 MappedStatement 对象。

Dao接口方法可以重载吗?

Dao 接口里的方法可以重载,但是 Mybatis 的 XML 里面的 ID 不允许重复。

1
2
3
4
5
6
7
8
9
/**
* Mapper接口里面方法重载
*/
public interface StuMapper {

List<Student> getAllStu();

List<Student> getAllStu(@Param("id") Integer id);
}

然后在 StuMapper.xml 中利用 Mybatis 的动态 sql 就可以实现。

1
2
3
4
5
6
7
8
<select id="getAllStu" resultType="com.pojo.Student">
select * from student
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>

Mybatis 的 Dao 接口可以有多个重载方法,但是多个接口对应的映射必须只有一个,否则启动会报错。

MyBatis 是否可以映射 Enum 枚举类?

MyBatis 可以映射枚举类,不单可以映射枚举类,MyBatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler ,实现 TypeHandlersetParameter()getResult() 接口方法。 TypeHandler 有两个作用,一是完成从 javaType 至 jdbcType 的转换,二是完成 jdbcType 至 javaType 的转换,体现为 setParameter()getResult() 两个方法,分别代表设置 sql 问号占位符参数和获取列查询结果。

动态SQL

Mybatis的动态SQL主要包括以下几种元素。

元素 作用
if 单条件分支判断
choose(when、otherwise) 相当于Java的switch、case
foreach 在in语句等列举条件常用
trim(where、set) 用于处理SQL拼装问题

MyBatis 动态 sql 是做什么的?都有哪些动态 sql?能简述一下动态 sql 的执行原理不?

MyBatis 动态 sql 可以让我们在 Xml 映射文件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,MyBatis 提供了 9 种动态 sql 标签 trim|where|set|foreach|if|choose|when|otherwise|bind

其执行原理为,使用 OGNL 从 sql 参数对象中计算表达式的值,根据表达式的值动态拼接 sql,以此来完成动态 sql 的功能。

if元素

if元素和test属性联合使用。

1
2
3
4
5
6
<select id="getRoleByRoleName" parameterType="string" resultMap="roleMap">
select id, role_name, note, reg_time FROM role where 1=1
<if test="roleName != null and roleName != ''">
and role_name like concat('%', #{roleName}, '%')
</if>
</select>

choose元素

choose、when和otherwise类似于Java的switch、case和default。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findRoles" parameterType="role" resultMap="roleMap">
select id, role_name, note, reg_time from role where 1=1
<choose>
<when test="roleName != null and roleName != ''">
and role_name = #{roleName}
</when>
<when test="note != null and note != ''">
and note = #{note}
</when>
<otherwise>
and id != 1
</otherwise>
</choose>
</select>
  • 当roleName不为空,则只用roleName作为条件查询;
  • 当roleName为空,note不为空,则用note作为条件进行查询;
  • 当roleName和note都为空时,则以 id != 1 作为查询条件

where元素

where元素解析时会自动将第一个字段的and去掉。

1
2
3
4
5
6
7
8
9
10
11
<select id="findRoles" parameterType="role" resultMap="roleMap">
select id, role_name, note, reg_time from role
<where>
<if test="roleName != null and roleName != ''">
and role_name like concat('%', #{roleName}, '%')
</if>
<if test="note != null and note != ''">
and note like concat('%', #{note}, '%')
</if>
</where>
</select>

使用trim也可以达到同样的效果。prefix代表语句前缀,prefixOverrides代表需要去掉的字符串。

1
2
3
4
5
6
7
8
9
10
11
<select id="findRoles" parameterType="role" resultMap="roleMap">
select id, role_name, note, reg_time from role
<trim prefix="where" prefixOverrides="and">
<if test="roleName != null and roleName != ''">
and role_name like concat('%', #{roleName}, '%')
</if>
<if test="note != null and note != ''">
and note like concat('%', #{note}, '%')
</if>
</trim>
</select>

set元素

当在 update 语句中使用if标签时,如果前面的if没有执行,则或导致逗号多余错误。使用set标签可以将动态的配置 SET 关键字,并剔除追加到条件末尾的任何不相关的逗号。使用 if+set 标签修改后,如果某项为 null 则不进行更新,而是保持数据库原值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<update id="updateRole" parameterType="role">
update role
<set>
<if test="roleName != null and roleName != ''">
role_name = #{roleName},
</if>
<if test="note != null and note != ''">
note = #{note},
</if>
<if test="regTime != null">
reg_time = #{regTime}
</if>
</set>
where id = #{id}
</update>

foreach元素

foreach用于遍历元素,支持数组、List和Set接口的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<insert id="batchInsertRole" parameterType="java.util.List">
<selectKey keyProperty="id" resultType="java.lang.Long" order="AFTER">
select LAST_INSERT_ID()
</selectKey>
insert into role(role_name, note, reg_time) values
<!--mapper接口参数没有使用@Param指定参数名称,则collection名称默认为list-->
<foreach collection="roleList" item="role" separator=",">
(#{role.roleName}, #{role.note}, #{role.regTime,javaType=Date, jdbcType=VARCHAR})
</foreach>
</insert>

<select id="findRolesInIds" parameterType="java.util.List" resultMap="roleMap">
select id, role_name, note, reg_time from role
<if test="ids != null">
where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
</select>

实际使用

分页

RowBounds分页

RowBounds分页是Mybatis内置的基本功能,在任何的select语句中都可以使用,它是在SQL语句查询出所有结果之后,对结果进行截断,当SQL语句返回大量结果时,容易造成内存溢出。其适用于返回数据量小的查询。

RowBounds有两个重要的参数limit和offeset,offeset表示从哪一条记录开始读取,limit表示限制返回的记录数。

下面通过角色名称模糊查询角色信息。

1
2
3
4
5
6
7
<!--使用resultMap进行结果映射, 用typeHandler对note字段进行转化-->
<select id="getRoleByRoleName" parameterType="string" resultMap="roleMap">
select id, role_name, note, reg_time from role where 1=1
<if test="roleName != null and roleName != ''">
and role_name like concat('%', #{roleName}, '%')
</if>
</select>

RoleMapper接口定义。

1
2
3
4
5
6
7
import com.tyson.pojo.Role;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.session.RowBounds;

public interface RoleMapper {
public List<Role> getRoleByRoleName(@Param("roleName") String roleName, RowBounds rowBounds);
}

测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void getRoleByRoleNameTest() {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtil.openSqlSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
List<Role> roles = roleMapper.getRoleByRoleName("man", new RowBounds(0, 5));
roles.forEach(role -> {
log.info(role.toString());
});
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}

测试结果返回五条记录。

预编译

#{ } 被解析成预编译语句,预编译之后可以直接执行,不需要重新编译sql。

1
2
3
4
//sqlMap 中如下的 sql 语句
select * from user where name = #{name};
//解析成为预编译语句;编译好SQL语句再取值
select * from user where name = ?;

${ } 仅仅为一个字符串替换,每次执行sql之前需要进行编译,存在 sql 注入问题。

1
2
3
select * from user where name = '${name}'
//传递的参数为 "ruhua" 时,解析为如下,然后发送数据库服务器进行编译。取值以后再去编译SQL语句。
select * from user where name = "ruhua";

数据库接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。如果一条sql语句需要反复执行,每次都进行语法检查和优化,会浪费很多时间。预编译语句就是将sql语句中的值用占位符替代,即将sql语句模板化。一次编译、多次运行,省去了解析优化等过程。

mybatis是通过PreparedStatement和占位符来实现预编译的。

mybatis底层使用PreparedStatement,默认情况下,将对所有的 sql 进行预编译,将#{}替换为?,然后将带有占位符?的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。

预编译的作用:

  1. 预编译阶段可以优化 sql 的执行。预编译之后的 sql 多数情况下可以直接执行,数据库服务器不需要再次编译,可以提升性能。
  2. 预编译语句对象可以重复利用。把一个 sql 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个sql,可以直接使用这个缓存的 PreparedState 对象。
  3. 防止SQL注入。使用预编译,而其后注入的参数将不会再进行SQL编译。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一个参数。

缓存

目前流行的缓存服务器有Redis、Ehcache、MangoDB等。缓存是计算机内存保存的数据,在读取数据的时候不用从磁盘读入,具备快速读取的特点,如果缓存命中率高,可以极大提升系统的性能。若缓存命中率低,则使用缓存意义不大,故使用缓存的关键在于存储内容访问的命中率。

一级缓存和二级缓存

Mybatis对缓存提供支持,默认情况下只开启一级缓存,一级缓存作用范围为同一个SqlSession。在SQL和参数相同的情况下,我们使用同一个SqlSession对象调用同一个Mapper方法,往往只会执行一次SQL。因为在使用SqlSession第一次查询后,Mybatis会将结果放到缓存中,以后再次查询时,如果没有声明需要刷新,并且缓存没超时的情况下,SqlSession只会取出当前缓存的数据,不会再次发送SQL到数据库。若使用不同的SqlSession,因为不同的SqlSession是相互隔离的,不会使用一级缓存。

二级缓存作用范围是Mapper(Namespace),可以使缓存在各个SqlSession之间共享。二级缓存默认不开启,需要在mybatis-config.xml开启二级缓存:

1
2
3
4
<!-- 通知 MyBatis 框架开启二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

并在相应的Mapper.xml文件添加cache标签,表示对哪个mapper 开启缓存:

1
<cache/>

二级缓存要求返回的POJO必须是可序列化的,即要求实现Serializable接口。

当开启二级缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

参考文章:

[1] https://github.com/Tyson0314/Java-learning/blob/master/%E6%A1%86%E6%9E%B6/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAMybatis%E6%8A%80%E6%9C%AF%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E6%88%98.md

[2] JavaGuide (gitee.io)