- 配置
- 简单查询
- 高级查询
- 代码生成器
- 缓存配置
前言
这里要提到一个词:关注点分离。业务逻辑与数据逻辑相分离。没必要为每一个DAO对象的初始化过程编写大量代码,只需关注其SQL,并将SQL从代码中分离出来,mybatis就是这样一个实现关注点分离思路的框架。
本篇为mybatis学习笔记,设计到mybatis的环境搭建、配置详解、简单应用、代码生成器、高级查询、缓存配置。基本上是全了。
前身是iBatis,以接近JDBC的性能实现了Java代码与SQL语句的分离。但是mybatis有其自己不完美的地方:物理分页、缓存。
mybatis支持声明式缓存,当一条数据被标记为“可缓存”后,首次执行它所获取的数据会被缓存到高速缓存中,后面再次执行直接命中缓存,基于HashMap实现。
配置
配置文件
mybatis-config.xml
mapper.xml
查询
四个基础标签
<select>、<insert>、<update>、<delete>
select
单表
Model类
package com.abc.model;
public class User{
public String name;
public Integer age;
}
查询写法
<select id=”selectUserById”resultType=”com.abc.model.User”>
select name,age from user where id=#{id}
</select>
调用
User user = selectUserById(1001);
第二个例子
public class User{
public String name;
public Integer age;
public Role role;
}
public class Role{
public String roleId;
public String roleName;
}
<select id=”selectUserById”resultType=”com.abc.model.User”>
select name,age,role.Id,role.roleName
from user u
inner join role r on u.id=r.id
where u.id=#{id}
</select>
第二个例子另一种写法
<resultMap id=”userMap” type=”com.abc.model.User”>
<id property=”id” column=”id”/>
<result property=”name” column=”name”/>
<result property=”age” column=”age”>
</resultMap>
<select id=”selectUserById”resultMap=”userMap”>
select name,age
from user u
inner join role r on u.id=r.id
where u.id=#{id}
</select>
多表
当返回值涉及一个model时,和单表一样。如果有多余返回值,可以嵌套model,在写xml写sql时,select u.user_name as “user.userName”
insert
获取主键值
自增方式的数据库:
<insert id="insert" userGenerateKeys="true" keyProperty="id">
insert into ..
</insert>
序列的:
<insert id="insert" userGenerateKeys="true" keyProperty="id">
insert into ..
<selectKey keyColume=="id" resultType="long" keyProperty="id" order="before"> //oracle的序列是先获取的,随意要before。mysql的自增主键是插入后才有的,要改成after。
//mysql SELECT LAST_INSERT_ID()
//oracle SELECT SEQ_ID.nextval from dual
//这里的写法不同数据库不一样
</selectKey>
</insert>
用法
<update id="updateById">
updatre sys_user
set user_name=#{userName},
user_password=#{userPassword},
user_email=#{userEmail},
user_info = #{userInfo}
hear_img = #{headImg,jdbcType=BLOB},
create_time=#{createTime,jdbcType=TIMESTAMP}
where id=#{id}
</update>
在UserMapper中添加接口
int updateById(SysUser sysUser);
测试类:
public void testUpdateById(){
Reader reader = Resource.GetResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().builder(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userMapper.updateById.......//大概流程如此
}
多查询条件,接口需要修改为:
List<SysRole> selectRolesByUserIdAndRoleEnable(@Param("userId")Long userId,@Param("enable")Integer enabled);
如果入参为对象
List<SysRole> selectRolesByUserIdAndRoleEnable(@Param("user")User user,@Param("role")SysRole role);
在xml使用的时候,要用user.userId和role.enable了。和上面查询一样。
delete update略
Mapper动态代理实现
首先声明一个接口
public interface TestMapper{
List<User> selectAll();
}
创建一个代理类
public class MyMapperProxy<T> implements InvocationHandler{
private Class<T> myMapper;
private SqlSession sqlSession;
public MyMapperProxy(Class<T> myMapper,SqlSession sqlSession){
this.myMapper = myMapper;
this.sqlSession = sqlSession;
}
public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
List<T> list = sqlSeesion.selectList(mapperInterface.getCanonicalName()+"."+method.getName());
return list;
}
}
这样就获取了interface的全限定名,所以mybatis是一种约定大于配置的方式简化了操作。
Mybatis注解的用法
Insert注解,不需要返回主键的情况:
@Insert("insert into sys_role(id,name) values(#{id},#{name,jdbcType=String})")
需要返回主键的情况(自增主键)
@Insert("insert into sys_role(name) values(#{name,jdbcType=String})")
@Options(useGeneratedKsys=true,keyProperty="id")
需要返回主键的情况(非自增主键),xml里用的selectKey标签
@Insert("insert into sys_role (name,enable,create_time),values(#{name},#{enable},#{createTime,jdbcType=TIMESTAMP})")
@SelectKey(statement="SELECT LAST_INSERT_ID()",keyProperty="id",resultType=Long.class,before=false)
Update和Delete注解
同上,没什么特殊的。
Provider注解
除了上述增删改查意外,mybatis还提供了四种Provider注解。分别为:@SelectProvider @InsertProvider @UpdateProvider @DeleteProvider。
就是将SQL封装到代码里。
@SelectProvider(type=PrivilegeProvider.class,method="selectById")
SysPrivilege selectById(Long id);
然后PrivilegeProvider类如下
public class PrivilegeProvider{
public String selectById(final Long id){
return new SQL(){
SELECT("id,privilege_name,privilege_url");
FROM("sys_privilege");
WHERE("id=#{id}");
}.toString();
}
}
由于MyBatis注解方式不是主流,仅做了解。
动态SQL
MyBatis强大特性之一便是这一节的东西。MyBatis使用OGNL表达式,用于xml配置中。支持如下几种标签:
<if /> 判断
<choose /> 选择
<where /> 条件
<set /> 更新设值
<trim /> 按规则替换
<foreach />
<bind />
其中where、set、trim这三个标签解决了类似问题,并且where、set都属于trim的一种具体用法。
if标签
有如下查询场景:当用户传入name时,根据name查,当输入的是email时,根据email查。如何用OGNL表达式来写这条sql呢?
<select id="selectByUser" resultType="com.abc.simple.model.user">
select id,
user_name userName,
user_password userPassword,
user_email userEmail,
user_info userInfo
from sys_user
where 1=1
<if test="userName != null and userName!=">
and user_name like concat('%',#{userName},'%') //concat是mysql语法
</if>
<if test="userEmail !=null and userEmail!=">
and user_email = #{userEmail}
</if>
</select> //注意首先判断非空,然后再判断值。两个if标签可同时为真。
if标签有一个必填的属性test,test的属性值一个符合OGNL要求的判断表达式,里面就是一个条件判断语句,支持and or,分组判断,嵌套判断。
update中使用if标签
<update id="updateById">
update sys_user
set
<if test="userName!=null and userName!=">
user_name = #{userName},
</if>
<if test="userPassword!=null and userPassword!=">
user_password = #{userPassword},
</if>
id=#{id}
where id=#{id}
</update> //1、注意逗号 2、注意where条件前的语句id=#{id},这条语句可以最大限度的保证不出错。
insert中使用if标签
<insert id="insert1" useGeneratedKey="true" keyProperty="id">
insert into sys_user(
user_name,user_password,<if test="userEmail !=null and userEmail!=">user_email,</if>userinfo
)values(#{userName},#{userPassword},<if test="userEmail !=null and userEmail!=">#{userEmail},</if>)#{userInfo}
</insert>
字段和值的条件判断要一致。
choose标签
choose的作用就如同if…else,语句为choose when otherwise
<select id=”selectByUser” resultType=”com.abc.model.User”>
select * from user where 1=1
from user
where 1=1
<choose>
<when test=”id!=null”>
and id =#{id}
</when>
<when test=”userName!=null and userName!='' ”>
and userName=#{userName}
<otherwise>
and 1=2
<otherwise>
</choose>
</select>
最后这个1=2是为了当什么都没有输入的时候,返回为空。因为这个条件肯定是false的。
where标签
where标签的作用:如果标签包含的元素中有返回值,那么就插入一个where,如果where后面的字符串以and和or开头,就删除它。
<select id=”selectUser” resultType=”com.abc.model.User”>
select * from user
<where>
<if test=”userName!=null and userName!='' ”>
and user_name like concat('%',#{userName},'%')
</if>
<if>
and user_email = #{userEmail}
</if>
</where>
</select>
set标签
set标签作用:如果该标签包含的元素中有返回值,就插入一个set,如果set后面的字符串是以逗号结尾的,就将逗号去掉。
<update id=”updateUserById”>
update user
<set>
<if test=”userName!=null and userName!='' ”>
user_name=#{userName},
</if>
<if test=”userEmail!=null and userEmail!='' ”>
user_email=#{userEmail},
</if>
<if test=”headImg!=null”>
head_img=#{headImg,jbdcType=BLOB},
</if>
where id=#{id}
</set>
</update>
trim标签
where和set标签的功能都是通过trim来实现的
where对应的是:
<trim prefix=”WHERE”prefixOverrides=”AND |OR ”>
</trim>
set对应的是:
<trim prefix=”SET” suffixOverrides=”,”>
</trim>
prefixOverrides:当trim元素内包含内容时,会把内容中匹配的前缀字符串去掉
suffixOverrides:当trim元素内包含内容时,会把内容中匹配的后缀字符串去掉
foreach标签
批量查询,在SQL中会使用类似 id in (1,2,3)
<select id=”selectByIdList”resultType=”com.abc.model.User”>
select * from user
where id in
<foreach collection=”list”
open=”(”
close=”)”
separator=”,”
item=”id”
index=”i”>
#{id}
</foreach>
</select>
批量插入是SQL-92新增特性,目前支持的数据库有DB2、SQL Server2008、MySQL、SQLite3.7.11以上版本。(2017年11月再版)
INSERT INTO tablename (column-a,column-b,column-c) values (value-a’,value-b’,‘value-c’);
<insert id="insertList">
insert into sys_user(user_name,user_password,user_email)
values
<foreach collection="list" item="user" separator=",">
(
{user.userName},#{user.userPassword},#{user.userEmail}
)
</foreach>
</insert>
foreach插入map类型
<foreach collection="_parameter" item="val" index="key" separator=",">
(
${key}=#{val}
)
</foreach>
注:$取值和#取值的区别。
1.取值会将输入当成字符串,比如输入123,输出"123",可以防sql注入
$取值将输入原样输出,比如输出123,输出123。
bind标签
bind可以绑定变量,避免更换数据库造成的修改sql,当然只是在一定语法程度上。
<if test="userName!=null">
<bind name="nameLike" value="'%'+userName+'%'">
and user_name like #{nameLike}
</if>
多数据支持
提供了一个databaseId的属性值,增删改查、selectKey,sql标签都包含这个属性。
在mybatis-config.xml中配置,并给对应的数据库id起了一个别名。
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver">
<property name="DB2" value="db2">
...
</databaseIdProvider>
然后在写select标签时,加上
<select id=”selectUserById” databaseId=”mysql”></select>
<select id=”selectUserById” databaseId=”oracle”></select>
比如针对oracle和mysql的like查询
<select id="selectByUser" databaseId="mysql" resultType="com.abc.model.user">
select * from sys_user where user_name like concat('%',#{userName},'%')
</select>
<select id="selectByUser" databaseId="oracle" resultType="com.abc.model.user">
select * from sys_user where user_name like '%'||#{userName}||'%'
</select>
就可以根据不同的数据库进行操作,比如mysql有自增主键,oralce有序列。
OGNL语法
a or b
a and b
a==b 或 a eq b
a!=b 或 a neq b
a lt b 小鱼
a lte b 小鱼等鱼 gt大鱼 gte大鱼等鱼
a+b a*b a/b a-b a%b
!a not a
a.method(args) 调用对象方法 比如判断 list.size()>0
a.property 对象属性值 比如user.address.name,比如保证存在这个字段,不然会报错
a[index]按索引取值
@class@method(args)调用静态类方法 比如test="@com.abc.utils.StringUtils@isNotEmpty(userName)"
@class@field调用类的静态字段
MyBatis代码生成器
MyBatis代码生成器又叫MyBatis Generator,缩写MBG,这东西是用来根据数据库来自动生成mapper文件、model文件、dao文件。
运行方式1:下载mybatis-generatorjar文件,然后编写一个xml文件。用
java -jar mybatis-generator-core-1.3.2.jar -configfile generatorConfig.xml -overwrite
运行,还要准备一个数据库连接的jar。
运行方式2:通过maven获取
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.3</version>
</dependency>
然后创建Generator.java类
public static void main(String args[]){
List<String> warnings = new ArrayList<String>();
boolean override = true;
InputStream is = Generator.class.getResourceAsStream("/generator/generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(is);
is.close();
DefaultShellCallBack callback = new DefaultShellCallBack(override);
//创建MBG
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(conifg,callback,warnings);
//执行生成代码
myBatisGenerator.generate(null);
//输出警告信息
for(String warning : warnings){
syso(warning);
}
}
使用java代码好处是,可以在当前项目中找一些自定义类,比如里配置的自定义注释类。
运行方式3:maven plugin方式
运行方式4:eclipse插件方式 安装mybatis-generator的eclipse插件
MBG有丰富的配置可供使用,所以首先来看MBG的xml配置。首先在xml文件中引入MBG的dtd文件。dtd是用来定义标签的。mybatis-generator-config_1_0.dtd
然后所有的配置要写在
<generatorConfiguration>
<properties></properties> 最多配置一个
<classPathEntry></classPathEntry> 选填
<context></context> 至少配置一个
</generatorConfiguration>
MyBatis高级查询
高级结果映射
一对一
<select>
select u.id,
u.user_name userName,
u.user_info userInfo,
r.id "role.id",
r.role_name "role.roleName"
from sys_user u
inner join sys_user_role ur on u.id=ur.user_id
where u.id=#{id}
</select>
User这样写
public User{
@setter @getter public UserRole role;
}
就可以实现嵌套结果映射,mybatis就将查询结果映射到两个类里了。这样写的好处就是降低数据库的访问次数,缺点是要写复杂的sql。
要实现上述功能,mybatis还可以用reusltMap来实现啊。其实这俩是一个东西。
<resultMap>
<result property="role.roleName" column="role_name" jdbcType="String"/>
</resultMap>
如果有十来个属性,那么写起来比较费劲。这时候mybatis的resultMap的继承就起作用了。
将通用的写一个resultMap,需要inner join查其他表的属性,就继承一下。
<resultMap id="userRoleMap" extends="userMap" type="com.abc.model.user">
</result>
在此基础上,继续做修改,resultMap有一个子标签用来配置复杂映射关系
<resultMap id="userRoleMap" extends="userMap" type="com.abc.model.user">
<association property="role" columnPrefix="role_" javaType="com.abc.mode.role">
<result />
</association>
</resultMap>
在inner join的时候,和sys_role相关的要加上role_前缀。
<association resultMap="">功能也可以将association内的result标签独立出来。
<association>还实现了一个重要的功能:嵌套查询的延迟加载。
<association
property="role"
fetchType="lazy"
select="com.abc.mybatis.mapper.RoleMapper.selectRoleById"
column="{id=role_id}"
/>
在RoleMapper.xml中加入
<select id="selectRoleById" resultMap="roleMap">
select * from sys_role where id=#{id}
</select>
然后在mybatis-config.xml中配置
<settings>
<setting name="aggressiveLazyLoading" value="false" />
</settings>
即可。但是注意,在和Sping集成时,要确保只能在Service中使用延迟加载属性,因为SqlSession的生成周期交给了Spring来管理,当对象的生命周期超出了SqlSession的生命周期时,就会报错。在Controller查询会因为SqlSession已经关闭。
当我们想主动触发延迟加载时,mybatis提供了另一个参数来解决lazyLoadingTriggerMethods,当调用默认方法“equals、clone、hashCode、toString”时会触发。
一对多
就是将结果映射成一个List,如下:
@setter @getter List roleList;
<resutMap>
<result />
...
<collection property="roleList" columnPrefix="role_" ofType="com.abc.model.SysRole">
<result property="id" column="id" />
<result property="roleName" column="role_name">
</collection>
</resultMap>
简而言之就是把association变成了collection。
原理:mybatis会根据
主键,来讲查出来的数据,userMap id相同的合并。
利用resultMap嵌套的特性,可以实现多层model的嵌套查询。
collection集合的嵌套查询
association关联的嵌套查询这种方式会执行额外的SQL查询,映射配置会简单很多。下面是collection的嵌套映射。
首先新增PriMapper.xml
<select id="selectPriByRoleId" resultMap="priMap">
select p.* from sys_pri p where role_id=#{id}
</select>
<resultMap id="rolePriListMapSelect" extends="roleMap" type="com.abc.model.Role">
<collection property="priList"
fetchType="lazy"
column="{roleId=id}"
select="com.abc.simple.mapper.PriMapper.selectPriByRoleId" />
</resultMap>
<select id="selectRoleByUserId" resultMap="rolePriListMapSelect">
select r.id,r.role_name,r.role_enable,r.create_time from sys_role r inner join sys_user_role ur on ur.role_id=r.id where ur.user_id=#{userId}
</select>
最顶层用户信息
<resultMap id="userRoleListMapSelect" extends="userMap" type="com.abc.model.User">
<collection property="roleList"
fetchType="lazy"
select="com.abc.simple.mapper.RoleMapper.selectRoleByUserId"
column="{userId=id}" />
</resultMap>
<select id="selectAllUserAndRolesSelect" resultMap="userRoleListMapSelect">
select u.id,
u.user_name,
u.user_info,
u.create_time
from sys_user u where u.id=#{id}
</select>
这样就实现了多层collection嵌套循环
单元测试
@Test
public void testSelectAllUserAndRolesSelect(){
SqlSession sqlSession = getSession();
//省略try catch
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
SysUser user = userMapper.selectAllUserAndRolesSelect(1L);
syso(user.getUserName);
//开始延迟加载
foreach(SysRole role : user.getRoleList()){
syso(role.getRoleName);
//继续延迟加载
for(SysPri pri : role.getPriList()){
syso(pri.getPriName());
}
}
}
鉴别器映射
有时一个单独数据库查询会返回很多不同类型的数据,类似Java中的switch语句。这个功能很强大。
比如当我们查询一个分类的科目时,如果该分类的状态为不可用状态,那么就不去查他的科目了。下面这个例子逻辑一样。
<resultMap id="rolePriListMapChoose" type="com.abc.model.SysRole">
<discriminator column="enable" javaType="int">
<case value="1" resultMap="rolePriListMapSelect" />
<case value="0" resultMap="roleMap" />
</discriminator>
</resultMap>
根据enbale的值,来映射不同的resultMap。
存储过程
<select id="selectUserById" statementType="CALLABLE" useCache="false">
{
call select_user_by_id(
#{id,mode=IN},
#{userName,mode=OUT,jdbcType=VARCHAR},
#{userInfo,mode=OUT,jdbcType=VARCHAR},
#{createTime,mode=OUT,jdbcType=TIMESTAMP},
#{headImg,mode=OUT,jdbcType=BLOB,javaType=_byte[]}
)}
</select>
使用mybatis调用存储过程,不支持mybatis二级缓存,所以直接把缓存useCache=“false”,一定要注意基本类型的映射。比如上面的headImg。这个存储过程没有返回值,使用出参的方式获得了,相当于c#中的
out user
使用出参方式获取返回值,必须保证所有出参在JavaBean中都存在。因为JavaBean对象中不存在出参对应的setter方法。
下面一个存储过程场景:分页查询
需要返回两个值:一个是List 一个是int total。返回值用resultMap接,total用上述的方式接。
插入、删除也是
<insert id="insert" statementType="CALLABLE">
{call insert_user(
#{user.id,mode=OUT,jdbcType=BIGINT},
#{user.userName,mode=IN},
#{user.headImg,mode=IN,jdbcType=BLOB},
#{roleIds,mode=IN}
)}
</insert>
<delete id="delete" statementType="CALLABLE">
{call delete_user(
{id,mode=IN}
)}
</delete>
oracle带游标的存储过程
游标其实对应多个sql语句。创建两个参数的游标:
create or replace procedure SELECT_COUNTRY(ref_cur1 out sys_refcursor,ref_cur2 out sys_refcursor)
is begin
open ref_cur1 for select * from country where id<3;
open ref_cur1 for select * from country where id>=3;
end SELECT_COUNTRY;
CountryMapper.xml写法
<select id="selectCountry" statementType="CALLABLE" useCache="false">
{
call SELECT_COUNTRY(
#{list1,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=BaseResultMap},
#{list2,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=BaseResultMap},
)
}
</select>
对枚举值的映射
MyBatis在遇到枚举值时,默认使用TypeHandler处理,这个处理器会将枚举值转换为字符串类型的字面值并使用,对Enable而言便是disable和enable字符串
所以不能使用默认的,使用另一个EnumOrdinalTypeHandler,这个处理器使用枚举的索引进行处理,在mybatis-config.xml中添加
<typeHandlers>
<typeHandler javaType="com.abc.type.Enable这个是枚举类" handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
</typeHandlers>
还可以实现自己的类型处理器,继承实现TypeHandler接口。1.6对Java 8日期(JSR-310)的支持,在pom.xml中引入依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.2</version>
</dependency>
MyBatis缓存配置
一级缓存
在一个SqlSession中,如果查询的方法和参数都一样,那么第二次查的就不会从数据库里查了,第一次查询的结果会以Map形式存在缓存里。这时候如果对第一次查询的结果进行修改操作,也会影响第二次查询的结果。因为这俩的引用完全一样。如何避免这种情况?在查询语句上新增一个属性
<select id="selectById" flushCache="true" resultMap="userMap">
select * from user where id =#{id}
</select>
flushCache="true"会保证每次查询都从数据库里查,这时候如果查两次,那么返回的实例对象的引用就不同了。要避免这么做。还有一种刷新缓存的方式,就是执行insert update delete的时候,会自动清空一级缓存。如果在一个SqlSession中,先查id=1的User1,再查一次id=1的User2,那么User1和User2是同一个东西。如果先查id=1的User1,然后随便删除或更新一个id=2的User,然后再查id=1的User2,那么User1和User2就不是同一个引用了。因为缓存被清空了。
刚才说到,是在一个SqlSession中。如果打开了两个SqlSession,那么第一个SqlSession查出来的User和第二个SqlSession查出来的User对象,尽管值一样,但是是不同的引用对象。
这就是MyBatis的一级缓存。
二级缓存
我们常说的MyBatis缓存其实是值的它的二级缓存,因为一级缓存是默认不可被控制的。
MyBatis二级缓存非常强大。在存在于比SqlSession范围更大的SqlSessionFactory生命周期中。
二级缓存配置
mybatis-config.xml中添加配置,开启二级缓存
<settings>
<setting name="cacheEnabled" value="true" />
</settings>
xml中添加缓存配置
保证上面缓存全局开启的情况下进行,在UserMapper.xml中追加cache标签即可
<mapper namespace="com.abc.mapper.UserMapper">
<cache />
</mapper>
添加这句话有什么效果呢?
1、所有SELECT语句会被缓存
2、所有insert update delete会刷新缓存
3、使用LRU算法来回收 least recently used
4、根据时间表,缓存不会以任何时间来刷新,可以设置
5、缓存会存储集合或对象的1024个引用
6、缓存是可读可写的
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readonly="true"
/>
这个配置更为高级,创建了一个FIFO缓存,每60秒刷新一次,存储512个引用,只读的。
cache的eviction回收策略属性值如下:
LRU 最近最少使用
FIFO 按进入缓存的顺序
SOFT 移除基于GC规则的软引用
WEAK 移除基于GC规则的弱引用
接口里配置
在使用注解方式时,如果想对注解方法启用二级缓存,还需要在Mapper接口中建配置。如果Mapper接口也存在对应的zml映射文件,两者同时开启缓存时,还需要特殊配置。只在Mapper接口中配置的话,在UserMapper的interface中
@CacheNamespace(
eviction=FifoCache.class,
flushInterval=60000,
size=512,
readWrite=true
)
如果接口不存在使用注解式的方法,可以只在xml中配置,否则要在
@CacheNamespaceRef(UserMapper.class)
public interface UserMapper{
}
因为想让接口方法和xml文件使用相同的缓存,就要这样。然后在UserMapper.xml中添加
<cache-ref namespace="com.abc.mapper.UserMapper"/>
这样配置后,xml就会引用Mapper接口中配置的二级缓存,同样可以避免同时配置二级缓存导致的冲突。
MyBatis中很少同时使用Mapper接口注解方式和xml映射文件,所以参照缓存不是为了解决这个问题而设计的。参照缓存除了能够通过引用减少配置外,主要作用是用来解决脏读。
使用二级缓存
如果缓存配置的是只读的,那么无所谓,从缓存里读的对象就是同一个实例。
如果配置成可读可写的,mybatis使用SerializedCache来序列化可读可写缓存类。被序列化的对象要实现Serializable接口。
二级缓存什么时候会有数据呢?看下面的流程
1、开启SqlSession
2、查询id=1的用户
3、再次查询id=1的用户
这时候2和3查出来的是一个
4、关闭SqlSession
5、开启新的SqlSession
6、查询id=1的用户
7、再次查询id=1的用户
这时候虽然6和2查出来的不是一个引用,但是6并没有去查数据库,而是去二级缓存里查了。因为是可读可写的情况,6查出来的和7查出来的都是经过反序列化的,不是同一个实例。但6和7都没有去数据库查,都到二级缓存里去查了。二级缓存是当这个查询所在的SqlSession执行close()方法后,才将数据刷到二级缓存里。
这里可能会有疑问,如果我在第2步查出来数据后,修改其信息,那么后来查出来的不就都出问题了吗?Yes,所以不要做无意义的修改,避免人为增加脏数据。
MyBatis是通过Map来实现缓存的。少量数据可以,大量数据怎么办?
可以通过其他工具或框架来保存。
集成EhCache缓存
EhCache是一个纯Java进程内缓存,快速、简单、多种策略、内存和磁盘两级、可通过RMI实现分布式。该项目由MyBatis官方提供,叫ehcache-cache。https://github.com/mybatis/ehcache-cache
依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
集成Redis
MyBatis还提供了二级缓存的Redis支持,命名为redis-cache。https://github.com/mybatis/redis-cache。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
集成基本上的套路都是引用依赖,在src/main/resources新增配置文件,在mybatis-config.xml中配置缓存标签。
脏数据
二级缓存虽然能提高效率,但是也容易产生脏数据,由于多标联查时。要掌握避免脏数据的技巧。比如参照缓存。
二级缓存适用场景
1、以查询为主,少的增删改
2、绝大多数单表操作
3、按照业务对表进行分组时,如果关联表比较少,可以使用参照缓存。
在使用二级缓存时,一定要考虑脏数据对系统的影响,在任何情况下,都可以考虑在业务层使用可控制的缓存来代替二级缓存。