目录
上文介绍了MyBatis的配置相关,详情请看《MyBatis之配置》。本文讲解MyBatis的映射器。
映射器是MyBatis最复杂且最重要的组件。它由一个接口加上XML文件(或者注解,但是注解并不常用,所以本文章只讨论XML)组成。在映射器中可以配置参数、各类的SQL语句、存储过程、缓存、级联等复杂的内容,并且通过简易的映射规则到指定的POJO或者其他对象上,映射器能有效消除JDBC底层的代码。
1 概述
元素名称 | 描述 | 备注 |
---|---|---|
select | 查询语句,最常用、最复杂的元素之一 | 可以自定义参数,返回结果集等 |
insert | 插入语句 | 执行后返回一个整数,代表插入的条数 |
update | 更新语句 | 执行后返回一个整数,代表更新的条数 |
delete | 删除语句 | 执行后返回一个整数,代表删除的条数 |
定义参数映射关系 | 即将被删除的元素,不建议使用 | |
sql | 允许定义一部分SQL,然后在各个地方引用它 | 例如,一张表列名,一次定义,可以在多个SQL语句中使用 |
resultMap | 用来描述从数据库结果集中来加载对象,它是最复杂、最强大的元素 | 它将提供映射规则 |
cache | 给定命名空间的缓存配置 | —— |
cache-ref | 其他命名空间缓存配置的引用 | —— |
2 select元素——查询语句
2.1 概述
元素 | 说明 | 备注 |
---|---|---|
id | 它和Mapper的命名空间组合起来是唯一的,供MyBatis调用 | 如果命名空间和id结合起来不唯一,MyBatis将抛出异常 |
parameterType | 可以给出类的全命名,也可以给出别名,但是别名必须是MyBatis内部定义或者自定义的 | 可以选择Java Bean、Map等简单的参数类型传递给SQL |
resultType | 定义类的全路径,在允许自动匹配的情况下,结果集将通过Java Bean的规范映射; 或定义为int、double、float、map等参数; 也可以使用别名,但是要符合别名规范,且不能和resultMap同时使用 |
常用的参数之一,比如统计总条数时可以把它的值设置为int |
resultMap | 它是映射集的引用,将执行强大的映射功能。我们可以使用resultType和resultMap其中的一个,resultMap能提供自定义映射规则的机会 | MyBatis最复杂的元素,可以配置映射规则、级联、typeHandler等 |
flushCache | 它的作用是在调用SQL后,是否要求MyBatis清空之前查询本地缓存和二级缓存 | 取值为布尔值,true/false。默认值为false |
useCache | 启动二级缓存的开关,是否要求MyBatis将此次结果缓存 | 取值为布尔值,true/false。默认值为true |
2.2 简单的select元素的应用
<select id="countUserByFirstName" parameterType="string"
resultType="int">
select count(1) from t_user where user_name like
concat(#{firstName},'%')
</select>
- id配合Mapper的全限定名,联合成为一个唯一的标识(在不考虑数据库厂商标识的前提下),用于标识这条SQL。
- parameterType表示这条SQL接受的参数类型,可以是MyBatis系统定义或者自定义的别名,比如int、string、float等,也可以是类的全限定名,比如com.hys.mybatis.example3.pojo.User。
- resultType表示这条SQL返回的结果类型,与parameterType一样,可以是系统定义或者自定义的别名,也可以是类的全限定名。
- #{firstName}是被传递进去的参数。
2.3 自动映射和驼峰映射
MyBatis提供了自动映射功能,在默认的情况下自动映射功能是开启的,使用它的好处在于能有效减少大量的映射配置,从而减少工作量。
在setting元素中有两个可以配置的选项autoMappingBehavior和mapUnderscoreToCamelCase,它们是控制自动映射和驼峰映射的开关。一般而言,自动映射会使用得多一些,因为可以通过SQL别名机制处理一些细节,比较灵活,而驼峰映射则要求比较严苛,所以在实际中应用不算太广。
2.4 传递多个参数
2.4.1 使用map接口传递参数
在MyBatis中允许map接口通过键值对传递多个参数。接口如下:
public List<Role> findRolesByMap(Map<String, Object> parameterMap);
映射XML:
<select id="findRolesByMap" parameterType="map"
resultType="role">
select id,role_name as roleName,note from t_role where
role_name like
concat('%',#{roleName},'%') and note like
concat('%',#{note},'%')
</select>
注意,参数roleName和note,要求的是map的键,也就是需要按如下代码的方式传递参数:
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Map<String, Object> parameterMap = new HashMap<>();
parameterMap.put("roleName", "1");
parameterMap.put("note", "1");
List<Role> roles = roleMapper.findRolesByMap(parameterMap);
2.4.2 使用注解传递多个参数
接口:
public List<Role> findRolesByAnnotation(@Param("roleName") String roleName, @Param("note") String note);
此时代码的可读性大大提高了,使用者能明确参数roleName是角色名称,而note是备注,一目了然。映射XML如下:
<select id="findRolesByAnnotation" resultType="role">
select
id,role_name as roleName,note from t_role where
role_name like
concat('%',#{roleName},'%') and note like
concat('%',#{note},'%')
</select>
注意,此时并不需要给出parameterType属性,让MyBatis自动探索便可以了。
2.4.3 通过Java Bean传递多个参数
先定义一个参数的POJO——RoleParams,代码如下:
public class RoleParams {
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;
}
}
接口:
public List<Role> findRolesByBean(RoleParams roleParam);
映射XML:
<select id="findRolesByBean"
parameterType="com.hys.mybatis.example3.pojo.RoleParams"
resultType="role">
select
id,role_name as roleName,note from t_role where
role_name like
concat('%',#{roleName},'%') and note like
concat('%',#{note},'%')
</select>
引入Java Bean定义的属性作为参数,然后查询:
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
RoleParams roleParams = new RoleParams();
roleParams.setRoleName("1");
roleParams.setNote("1");
List<Role> roles = roleMapper.findRolesByBean(roleParams);
2.4.4 混合使用
在某些情况下可能需要混合使用几种方法来传递参数。举个例子,查询一个角色,可以通过角色名称和备注进行查询,于此同时还需要支持分页,而分页的POJO实现代码如下:
public class PageParams {
private int start;
private int limit;
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
}
接口:
public List<Role> findRolesByMix(@Param("params") RoleParams roleParams, @Param("page") PageParams pageParams);
映射XML:
<select id="findRolesByMix" resultType="role">
select
id,role_name as
roleName,note from t_role where
role_name like
concat('%',#{params.roleName},'%') and note like
concat('%',#{params.note},'%')
limit #{page.start},#{page.limit}
</select>
这样就能使用混合参数了,其中MyBatis对params和page这类Java Bean参数提供EL支持,为编程带来了很多的便利。
2.4.5 总结
- 使用map传递参数导致了业务可读性的丧失,导致后续扩展和维护的困难,在实际的应用中要果断废弃这种方式。
- 使用@Param注解传递多个参数,受到参数个数(n)的影响。当n≤5时,这是最佳的传参方式,它比用Java Bean更好,因为它更加直观;当n>5时,多个参数将给调用带来困难,此时不推荐使用它。
- 当参数个数多于5个时,建议使用Java Bean方式。
- 对于使用混合参数的,要明确参数的合理性。
2.5 使用resultMap映射结果集
自动映射和驼峰映射规则比较简单,无法定义多的属性,比如typeHandler、级联等。为了支持复杂的映射,select元素提供了resultMap属性。先定义resultMap属性,代码如下:
<mapper namespace="com.hys.mybatis.example3.mapper.RoleMapper">
<resultMap type="role" id="roleMap">
<id property="id" column="id" />
<result property="roleName" column="role_name" />
<result property="note" column="note" />
</resultMap>
<select id="getRoleUseResultMap" parameterType="long"
resultMap="roleMap">
select id,role_name,note from t_role where
id=#{id}
</select>
</mapper>
- resultMap元素定义了一个roleMap,它的属性id代表它的标识,type代表使用哪个类作为其映射的类,可以是别名或者全限定名,role是com.hys.mybatis.example3.pojo.Role的别名。
- 它的子元素id代表resultMap的主键,而result代表其属性,id和result元素的属性property代表POJO的属性名称,而column代表SQL的别名。把POJO的属性和SQL的列名做对应,例如POJO的属性roleName,就用SQL的列名role_name建立映射关系。
- 在select元素中的属性resultMap制定了采用哪个resultMap作为其映射规则。
3 insert元素——插入语句
3.1 概述
属性 | 描述 | 备注 |
---|---|---|
id | SQL编号,用于标识这条SQL | 命名空间+id+databaseId唯一,否则MyBatis会抛出异常 |
parameterType | 参数类型,同select元素 | 和select一样,可以是单个参数或者多个参数 |
flushCache | 是否刷新缓存,可以配置true/false,为true时,插入时会刷新一级和二级缓存,否则不刷新 | 默认值为true |
timeout | 超时时间,单位为秒 | |
statementType | STATEMENT、PREPARED或CALLABLE中的一个。这会让MyBatis分别使用Statement、PreparedStatement(预编译)或CallableStatement(存储过程) | 默认值为PREPARED |
useGeneratedKeys | 是否开启JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键(比如MYSQL和SQL Server这样的数据库表的自增主键) | 默认值为false |
keyProperty | (仅对insert和update有用)唯一标记一个属性,MyBatis会通过useGeneratedKeys的返回值,或者通过insert语句的selectKey子元素设置它的键值。如果是复合主键,要把每一个名称用逗号(,)隔开 | 默认值为unset。不能和keyColumn连用 |
keyColumn | (仅对insert和update有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像PostgreSQL)中是必须的,当主键列不是表中的第一列时需要设置。如果是复合主键,需要把每一个名称用逗号(,)隔开 | 不能和keyProperty连用 |
3.2 简单的insert语句的应用
<insert id="insertRole" parameterType="role">
insert into
t_role(role_name,note) values(#{roleName},#{note})
</insert>
- id标识出这条SQL,结合命名空间让MyBatis能够找到它。
- parameterType代表传入参数类型。
3.3 主键回填
上述代码展示了最简单的插入语句,但是它并没有插入id列,因为Mysql中的表格采用了自增主键,Mysql数据库会为该记录生成对应的主键。有时候还可能需要继续使用这个主键,用以关联其他业务,因此有时候把它取到是十分必要的,比如新增用户时,首先会插入用户表的记录,然后插入用户和角色关系表,插入用户时如果没有办法取到用户的主键,那么就没有办法插入用户和角色关系表了,因此在这个时候要拿到对应的主键,以便后面的操作,MyBatis提供了这样的支持。
JDBC中的Statement对象在执行插入的SQL后,可以通过getGeneratedKeys方法获得数据库生成的主键(需要数据库驱动支持),这样便能达到获取主键的功能。在insert语句中有一个开关属性useGeneratedKeys,用来控制是否打开这个功能,它的默为false。当打开了这个开关,还要配置其属性keyProperty或keyColumn,告诉系统把生成的主键放入哪个属性中,如果存在多个主键,就要用逗号(,)将它们隔开。
<insert id="insertRole" parameterType="role"
useGeneratedKeys="true" keyProperty="id">
insert into
t_role(role_name,note)
values(#{roleName},#{note})
</insert>
useGeneratedKeys代表采用JDBC的Statement对象的getGeneratedKeys方法返回主键,而keyProperty则代表将用哪个POJO的属性去匹配这个主键,这里是id,说明它会用数据库生成的主键去赋值给这个POJO,测试主键回填的结果,如下:
在第19行代码之前并没有给role对象的id属性赋值,而在执行insertRole方法后,通过监控role对象,就可以发现MyBatis给这个对象的id赋了值,拿到这个值,就可以在业务代码中执行下一步的关联和操作了。
3.4 自定义主键
有时候主键可能依赖于某些规则,比如取消角色表(t_role)的id的递增规则,而将其规则修改为:
- 当角色表记录为空时,id设置为1。
- 当角色表记录不为空时,id设置为当前id加3。
MyBatis对这样的场景也提供了支持,它主要依赖于selectKey元素进行支持,它允许自定义键值的生成规则。映射XML如下:
<insert id="insertRole" parameterType="role">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
select
if (max(id)=null,1,max(id)+3) from t_role
</selectKey>
insert into
t_role(id,role_name,note)
values(#{id},#{roleName},#{note})
</insert>
上述代码定义了selectKey元素,它的keyProperty指定了采用哪个属性作为POJO的主键。resultType告诉MyBatis将返回一个long型的结果集,而order设置为BEFORE,说明它将于当前定义的SQL前执行。
4 update元素和delete元素
因为update元素和delete元素比较简单,所以把它们放在一起论述。它们和insert的属性差不多,执行完也会返回一个整数,用以标识该SQL语句影响了数据库的记录行数。映射XML如下:
<update id="updateRole" parameterType="role">
update t_role set
role_name=#{roleName},note=#{note} where id=#{id}
</update>
<delete id="deleteRole" parameterType="long">
delete from t_role where
id=#{id}
</delete>
最后MyBatis会返回一个整数,标识对应的SQL执行后会影响了多少条数据库表里的记录。至于参数可以参考select元素的参数规则,在MyBatis中它们的规则是通用的。
5 sql元素
sql元素的作用在于可以定义一条SQL的一部分,方便后面的SQL引用它,比如最典型的列名。通常情况下要在select、insert等语句中反复编写它们,特别是那些字段较多的表更是如此,而在MyBatis中,只需要使用sql元素编写一次便能在其他元素中引用它了。
<resultMap type="role" id="roleMap">
<id property="id" column="id" />
<result property="roleName" column="role_name" />
<result property="note" column="note" />
</resultMap>
<sql id="roleCols">
id,role_name,note
</sql>
<select id="getRole" parameterType="long" resultMap="roleMap">
select
<include refid="roleCols" />
from t_role where id=#{id}
</select>
<insert id="insertRole" parameterType="role">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
select
if (max(id)=null,1,max(id)+3) from t_role
</selectKey>
insert into
t_role(
<include refid="roleCols" />
)
values(#{id},#{roleName},#{note})
</insert>
通过sql元素进行了定义,就可以通过include元素引入到各条SQL中了。这样的代码,在字段多的数据库表可以重复使用,从而减少对其列名的重复编写。
sql元素还支持变量传递,代码如下:
<sql id="roleCols">
${alias}.id,${alias}.role_name,${alias}.note
</sql>
<select id="getRole" parameterType="long" resultMap="roleMap">
select
<include refid="roleCols">
<property name="alias" value="r" />
</include>
from t_role r where id=#{id}
</select>
在include元素中定义了一个命名为alias的变量,其值是SQL中表t_role的别名r,然后sql元素就能够使用这个变量名了。
6 resultMap元素
resultMap的作用是定义映射规则、级联的更新、定制类型转化器等。resultMap定义的主要是一个结果集的映射关系,也就是SQL到Java Bean的映射关系定义,它也支持级联等特性。只是MyBatis现有的版本只支持resultMap查询,不支持更新或者保存,更不必说级联的更新、删除和修改了。
6.1 resultMap元素的构成
resultMap元素的子元素,如下所示:
<resultMap>
<constructor>
<idArg />
<arg />
</constructor>
<id />
<result />
<association />
<collection />
<discriminator>
<case />
</discriminator>
</resultMap>
其中constructor元素用于配置构造方法。一个POJO可能不存在没有参数的构造方法,可以使用constructor进行配置。假设角色类RoleBean不存在没有参数的构造方法,它的构造方法声明为public RoleBean(Integer id, String roleName),那么需要配置结果集,如下所示:
<resultMap ...>
<constructor>
<idArg column="id" javaType="int" />
<arg column="role_name" javaType="string" />
</constructor>
<!-- do something... -->
</resultMap>
这样MyBatis就会使用对应的构造方法来构造POJO了。
id元素标识哪个列是主键,允许多个主键,多个主键则称为联合主键。result是配置POJO到SQL列名的映射关系。id和result元素的属性,如下表所示:
元素名称 | 说明 | 备注 |
---|---|---|
property | 映射到列结果的字段或属性。如果POJO的属性匹配的是存在的且与给定SQL列名(column元素)相同的,那么MyBatis就会映射到POJO上 | 可以使用导航式的字段,比如访问一个学生对象(Student)需要访问学生证(selfcard)的发证日期(issueDate),那么可以写成selfcard.issueDate |
column | 对应的是SQL的列 | —— |
javaType | 配置Java的类型 | 可以是特定的类完全限定名或者MyBatis上下文的别名 |
jdbcType | 配置数据库类型 | 这是一个JDBC的类型,MyBatis已经做了限定,支持大部分常用的数据库类型 |
typeHandler | 类型处理器 | 允许用特定的处理器来覆盖MyBatis默认的处理器。这就要制定jdbcType和javaType相互转化的规则 |
7 级联
级联是一个数据库实体的概念。比如角色就需要存在用户与之对应,这样就有角色用户表,一个角色可能有多个用户,这就是一对多的级联;除此之外,还有一对一的级联,比如身份证和公民是一对一的关系。在MyBatis中还有一种被称为鉴别器的级联,它是一种可以选择具体实现类的级联,比如要查找雇员及其体检表的信息,但是雇员有性别之分,而根据性别的不同,其体检表的项目也会不一样,比如男性体检表可能有前列腺的项目,而女性体检表可能有子宫的项目,那么体检表就应该分为男性和女性两种,从而根据雇员性别区分关联。
级联不是必须的,级联的好处是获取关联数据十分便捷,但是级联过多会增加系统的复杂度,同时降低系统的性能,此增彼减,所以当级联的层级超过3层时,就不要考虑使用级联了,因为这样会造成多个对象的关联,导致系统的耦合、复杂和难以维护。在现实的使用过程中,要根据实际情况判断是否需要使用级联。
7.1 MyBatis中的级联
MyBatis的级联分为3种:
- 鉴别器(discriminator):它是一个根据某些条件决定采用具体实现类级联的方案,比如体检表要根据性别去区分。
- 一对一(association):比如学生证和学生就是一种一对一的级联,雇员和工牌表也是一种一对一的级联。
- 一对多(collection):比如班主任和学生就是一种一对多的级联。
值得注意的是,MyBatis没有多对多级联,因为多对多级联比较复杂,使用困难,而且可以通过两个一对多级联进行替换,所以MyBatis不支持多对多级联了。
7.2 实例
<mapper
namespace="com.hys.mybatis.example3.mapper.EmployeeMapper">
<resultMap type="com.hys.mybatis.example3.pojo.Employee"
id="employee">
<id property="id" column="id" />
<result property="realName" column="real_name" />
<result property="sex" column="sex"
typeHandler="com.hys.mybatis.example3.typehandler.SexTypeHandler" />
<result property="birthday" column="birthday" />
<result property="mobile" column="mobile" />
<result property="email" column="email" />
<result property="position" column="position" />
<result property="note" column="note" />
<association property="workCard" column="id"
select="com.hys.mybatis.example3.mapper.WorkCardMapper.getWorkCardByEmpId" />
<collection property="employeeTaskList" column="id"
select="com.hys.mybatis.example3.mapper.EmployeeTaskMapper.getEmployeeTaskByEmpId" />
<discriminator javaType="long" column="sex">
<case value="1" resultMap="maleHealthFormMapper" />
<case value="2" resultMap="femaleHealthFormMapper" />
</discriminator>
</resultMap>
<resultMap
type="com.hys.mybatis.example3.pojo.FemaleEmployee"
id="femaleHealthFormMapper" extends="employee">
<association property="femaleHealthForm" column="id"
select="com.hys.mybatis.example3.mapper.FemaleHealthFormMapper.getFemaleHealthForm" />
</resultMap>
<resultMap type="com.hys.mybatis.example3.pojo.MaleEmployee"
id="maleHealthFormMapper" extends="employee">
<association property="maleHealthForm" column="id"
select="com.hys.mybatis.example3.mapper.MaleHealthFormMapper.getMaleHealthForm" />
</resultMap>
<select id="getEmployee" parameterType="long"
resultMap="employee">
select id,real_name as
realName,sex,birthday,mobile,email,position,note from t_employee where
id=#{id}
</select>
</mapper>
- association元素:代表着一对一级联的开始。property属性代表映射到POJO属性上。select配置是命名空间+SQL id的形式,这样便可以指向对应Mapper的SQL,MyBatis就会通过对应的SQL将数据查询回来。column代表SQL的列,用作参数传递给select属性制定的SQL,如果是多个参数,则需要用逗号隔开。
- collection元素:一对多级联,其select元素指向SQL,将通过column制定的SQL字段作为参数进行传递,然后将结果返回给雇员POJO的属性employeeTaskList。
- discriminator元素:鉴别器,它的属性column代表使用哪个字段进行鉴别,这里的是sex,而它的子元素case,则用于进行区分,类似于Java的switch...case...语句。而resultMap属性表示采用哪个ResultMap去映射,比如sex=1,则使用maleHealthFormMapper进行映射。
id为employee的resultMap,被maleHealthFormMapper和femaleHealthFormMapper通过extends元素继承。从类的关系而言,它们也是这样的继承关系,而maleHealthFormMapper和femaleHealthFormMapper都会通过association元素来执行对应关联的字段和SQL。
7.3 N+1问题
级联会引发性能问题,比如作为一个雇员的管理者,他只想看到员工信息和员工任务信息,那么体检表和工牌的信息就是多余的,就会使数据库多执行几条毫无意义的SQL。如果需要在雇员信息系统里加入一个关联信息,那么它在默认情况下会执行SQL取出数据,而真实的需求往往只要完成雇员和雇员人物表的级联就可以了,不需要把所有信息都加载进来,因为有些信息并不常用,加载它们会多执行几条毫无用处的SQL,导致数据库资源的损耗和系统性能的下降。
假设现在有N个关联关系完成了级联,那么只要再加入一个关联关系,就变成了N+1个级联,所有的级联SQL都会被执行,显然会有很多并不是我们关心的数据被取出,这样会造成很大的资源浪费,这就是N+1问题,尤其是在哪些需要高性能的互联网系统中,这往往是不被允许的。
为了应对N+1问题,MyBatis提供了延迟加载功能,即再一开始取雇员信息时,并不需要将工牌表、体检表、任务表的记录取出,而是只将雇员信息和雇员任务表的信息取出。当我们通过雇员POJO访问工牌表、体检表和任务表的记录时才通过对应的SQL取出。
7.4 延迟加载
MyBatis支持延迟加载,我们希望一次性把常用的级联数据通过SQL直接查询出来,而对于那些不常用的级联数据不要取出,而是等待要用时才取出,这些不常用的级联数据可以采用了延迟加载的功能。
在MyBatis的setting配置中存在两个元素可以配置级联,如下表所示:
配置项 | 作用 | 配置选项说明 | 默认值 |
---|---|---|---|
lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。在特定关联关系中,可通过设置fetchType属性来覆盖该项的开关状态 | true|false | false |
aggressiveLazyLoading | 当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,则每种属性按需加载 | true|false | 版本3.4.1(包含)之前为true,之后为false |
选项lazyLoadingEnabled决定是否开启延迟加载,而选项aggressiveLazyLoading则控制是否采用层级加载,但是它们都是全局性的配置,并不能解决我们的需求。加载雇员信息时,只加载雇员任务信息,因为层级加载会把工牌信息也加载进来。为了处理这个问题,在MyBatis种使用fetchType属性,它可以处理全局定义无法处理的问题,进行自定义。fetchType出现在级联元素(association、collection,注意,discriminator没有这个属性可配置)中,它存在着两个值:
- eager:获得当前POJO后立即加载对应的数据。
- lazy:获得当前POJO后延迟加载对应的数据。
<collection property="employeeTaskList" column="id"
fetchType="eager"
select="com.hys.mybatis.example3.mapper.EmployeeTaskMapper.getEmployeeTaskByEmpId" />
fetchType属性会忽略全局配置项lazyLoadingEnabled和aggressiveLazyLoading。
8 缓存
在MyBatis中允许使用缓存,缓存一般都放置在可高速读/写的存储器上,比如服务器的内存,它能够有效提高系统的性能。因为数据库在大部分场景下是把存储在磁盘上的数据索引出来。从硬件的角度分析,索引磁盘是一个较为缓慢的过程,读取内存或者高速缓存处理器的速度要比读取磁盘快得多,其速度是读取磁盘的几十倍到上百倍,但是内存和高速缓存处理器的空间有限,所以一般只会把那些常用且命中率高的数据缓存起来,以便将来使用,而不缓存那些不常用且命中率低的数据缓存。因为命中率低,最后还是要在磁盘内查找,并不能有效提高性能。
MyBatis分为一级缓存和二级缓存,同时也可以配置关于缓存的设置。
8.1 一级缓存
一级缓存实在SqlSession上的缓存,二级缓存实在SqlSessionFactory上的缓存。默认情况下,也就是没有任何配置的情况下,MyBatis系统会开启一级缓存,也就是对于SqlSession层面的缓存,这个缓存不需要POJO对象可序列化(实现java.io.Serializable接口)。
SqlSession sqlSession = null;
Logger logger = Logger.getLogger(Test.class);
try {
sqlSession = SqlSessionFactoryUtils.openSqlSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Role role1 = roleMapper.getRole(1L);
logger.info("再获取一次POJO...");
Role role2 = roleMapper.getRole(1L);
sqlSession.commit();
} catch (Exception e) {
logger.info(e);
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
当在同一sqlSession下调用两次getRole方法时,通过查看相关日志可知,实际只有一条SQL被执行,其原因是代码使用了同一个SqlSession对象获取数据。当一个SqlSession第一次通过SQL和参数获取对象后,它就会将其缓存起来,如果下次的SQL和参数都没有发生变化,并且缓存没有超时或者需要刷新时,那么它就会从缓存中获取数据,而不是通过SQL获取了。
SqlSession sqlSession1 = null;
SqlSession sqlSession2 = null;
Logger logger = Logger.getLogger(Test.class);
try {
sqlSession1 = SqlSessionFactoryUtils.openSqlSession();
sqlSession2 = SqlSessionFactoryUtils.openSqlSession();
RoleMapper roleMapper1 = sqlSession1.getMapper(RoleMapper.class);
Role role1 = roleMapper1.getRole(1L);
//需要提交,如果是一级缓存,MyBatis才会缓存对象到SqlSessionFactory层面
sqlSession1.commit();
logger.info("不同sqlSession再获取一次POJO...");
RoleMapper roleMapper2 = sqlSession2.getMapper(RoleMapper.class);
Role role2 = roleMapper2.getRole(1L);
//需要提交,MyBatis才缓存对象到SqlSessionFactory
sqlSession2.commit();
} catch (Exception e) {
logger.info(e);
} finally {
if (sqlSession1 != null) {
sqlSession1.close();
}
if (sqlSession2 != null) {
sqlSession2.close();
}
}
注意commit()方法的使用,如果不进行commit,是不会有一级缓存存在的。运行上述代码后得出的日志可以看出,SQL被执行了两次,这说明了一级缓存是在SqlSession层面的,对于不同的SqlSession对象是不能共享的。
8.2 二级缓存
为了使SqlSession对象之间共享相同的缓存,有时候需要开启二级缓存,开启二级缓存很简单,只要在映射文件(RoleMapper.xml)上加入代码:
<cache />
这个时候MyBatis会序列化和反序列化对应的POJO,也就要求POJO是一个可序列化的对象,那么它就必须实现java.io.Serializable接口。如果没有实现,MyBatis将会抛出异常,导致程序运行错误。
9 存储过程
存储过程是数据库的一个概念,它时数据库预先编译好,放在数据库内存中的一个程序片段,所以具备性能高,可重复使用的特性。它定义了3种类型的参数:输入参数、输出参数、输入输出参数。
- 输入参数:是外界给的存储过程参数,在Java互联网中,也就是互联网系统给它的参数。
- 输出参数:是存储过程经过计算返回给程序的结果参数。
- 输入输出参数:是一开始作为参数传递给存储过程,而存储过程修改后将其返回的参数,比如那些商品的库存就是这样的。
对于返回结果而言,一些常用的简易类型,比如整形、字符型OUT或者INOUT参数是Java程序比较好处理的,而存储过程还可能返回游标类型的参数,这需要我们处理,不过在MyBatis中,这些都可以轻松完成。
9.1 IN和OUT参数存储过程
在Mysql中创建如下存储过程:
CREATE PROCEDURE count_role ( IN p_role_name VARCHAR ( 20 ), OUT count_total INT, OUT exec_date date ) BEGIN
SELECT
count( 1 ) INTO count_total
FROM
t_role
WHERE
role_name LIKE concat( '%', p_role_name, '%' );
SELECT CURRENT_DATE INTO
exec_date;
END;
这样就创建了这个存储过程。为了使用它,要设计一个POJO——PdCountRoleParams,如下:
package com.hys.mybatis.example3.param;
import java.util.Date;
public class PdCountRoleParams {
private String roleName;
private int total;
private Date execDate;
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public Date getExecDate() {
return execDate;
}
public void setExecDate(Date execDate) {
this.execDate = execDate;
}
}
映射XML:
<select id="countRole"
parameterType="com.hys.mybatis.example3.param.PdCountRoleParams"
statementType="CALLABLE">
{call
count_role(#{roleName,mode=IN,jdbcType=VARCHAR},#{total,mode=OUT,jdbcType=INTEGER},#{execDate,mode=OUT,jdbcType=DATE})}
</select>
测试类:
PdCountRoleParams params = new PdCountRoleParams();
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtils.openSqlSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
params.setRoleName("Robert Hou");
roleMapper.countRole(params);
System.out.println(params.getTotal());
System.out.println(params.getExecDate());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
运行结果:
log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
1
Sun Jul 22 08:00:00 CST 2018
9.2 游标的使用
在实际应用中,除了使用简易的输入输出参数,有时候也可能使用游标,MyBatis也对存储过程的游标提供了支持。如果把jdbcType声明为CURSOR,那么它就会使用ResultSet对象处理对应的结果,只要设置映射关系,MyBatis就会把结果集映射出来(具体代码实现可以查看其他文章,本文不再赘述)。
参考资料:[1]杨开振 周吉文 梁华辉 谭茂华.Java EE 互联网轻量级框架整合开发:SSM框架(Spring MVC+Spring+MyBatis)和Redis实现[M].北京:电子工业出版社,2017.7