mybatis
关于MyBatis的增删改查实现 在MyBatis系列(一)中涉及了如何创建一个入门级 的mybatis测试。
但是只涉及了查询单行数据,接下来 我把它补全。
上次采用的是MySQL数据库 ,这次我采用的是Oracle。从官网下到了Oracle6的一个jar
.
环境:
eclipse 2019-12
Oracle 11g
apache tomcat 9.0.3
mybatis 3.5.4.jar
Oracle.jar
在Oracle中建个表 create table (stuno number,stuname varchar(20),stuage number,graname varchar(20));
,
随变插入几行数据,此处就不放图了🈚。
在eclipse里构建这个类 此处省略代码,详见MyBatis系列(一)。
映射 新建一个studentMapper.xml
(当然是要映射了🍔)。
新建conf.xml
(选择数据库以及映射关系)。
这里关于一些细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <configuration > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="oracle.jdbc.driver.OracleDriver" /> <property name ="url" value ="jdbc:oracle:thin:@127.0.0.1:1521:ORCL" /> <property name ="username" value ="scott" /> <property name ="password" value ="tiger" /> </dataSource > </environment > <environment id ="cloud" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="com.mysql.cj.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://eshyee,top:3306/test?useSSL=false& serverTimezone=UTC" /> <property name ="username" value ="我的用户名" /> <property name ="password" value ="我的密码" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="top/eshyee/entity/studentMapper.xml" /> </mappers > </configuration >
关于Mapper.xml
的新增代码,无非就是增删改查✨
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id ="queryStudentBystuNum" resultType ="top.eshyee.entity.Student" parameterType ="int" > select * from student where stuno= #{stuno} </select > <select id ="queryAllStudent" resultType ="top.eshyee.entity.Student" > select * from student </select > <insert id ="addStudent" parameterType ="top.eshyee.entity.Student" > insert into student(stuno,stuname,stuage,graname) values(#{stuNo},#{stuName},#{stuAge},#{graName}) </insert > <update id ="upDateStuByNo" parameterType ="top.eshyee.entity.Student" > update student set stuname=#{stuName},stuage=#{stuAge},graname=#{graName} where stuno=#{stuNo} </update > <delete id ="deleteStuByNo" parameterType ="int" > delete from student where stuno=#{stuno} </delete >
因为查之前说了,这里就拿改学生信息来做个小测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void updateStuByNo () throws IOException { Reader reader = Resources.getResourceAsReader("conf.xml" ); SqlSessionFactory sessionfactory = new SqlSessionFactoryBuilder ().build(reader); SqlSession session = sessionfactory.openSession(); String statement = "top.eshyee.entity.studentMapper.upDateStuByNo" ; Student stu=new Student (2 ,"小牛" ,33 ,"二年级" ); int count = session.update(statement, stu); session.commit(); System.out.println("修改成功," + count + "行受影响" ); session.close(); } public static void main (String[] args) throws IOException { selectAll(); updateStuByNo(); selectAll(); }
测试,得到结果,注意要提交。
mapper动态代理(MyBatis接口开发) 原则:约定优于配置 配置方式:
abc.xml
硬编码方式
abc.java
约定:默认值就是myProject
与传统配置的不同之处:省略掉statement,即根据约定直接定位出sql语句。
约定:
接口和mapper一一对应:namespace的value=接口的全类名
构建接口 所以说新建一个接口,根据前一个篇的mapper.xml的文件基础上改,注:我这里是把接口建在了mapper包下
1 2 3 4 5 Student queryStudentBystuNum (int stuno) ; List<Student> queryAllStudent () ; int addStudent (Student student) ;int upDateStuByNo (Student student) ;int deleteStuByNo (int stuno) ;
注意这里的接口要遵循上面的约定。
更改xml文件 再mapper.xml file中,namespace也应该改成接口的名字
1 <mapper namespace ="top.eshyee.mapper.StudentMapper" >
接下来,就是修改测试类,曾经使用的statement
的地方,目前就不用使用了。
使用接口 这次拿删除学生举例,首先要从session
中获取到这个接口,使用getMapper
方法。然后从这个接口中要我刚刚写的那个删除的方法deleteStuByNo()
,这里我写死删除no为3的 倒霉孩子 ✨
1 2 3 4 5 6 7 8 Reader reader = Resources.getResourceAsReader("conf.xml" );SqlSessionFactory sessionfactory = new SqlSessionFactoryBuilder ().build(reader);SqlSession session = sessionfactory.openSession();StudentMapper stuMapper = session.getMapper(StudentMapper.class);int count = stuMapper.deleteStuByNo(3 );session.commit(); System.out.println("删除成功," + count + "行受影响" ); session.close();
测试一下 3号学生被删除并想你抛了一行log🌚
总结 约定相对于配置来说,出错率更少了,因为不用写statement,而是通过调用接口去实现xml的id的调用,对于写statement的String容易手残的老哥来说,是不错的选择。
关于一些优化🍔
数据库连接配置 在config.xml
中如果配置了很多的东西,这时在想去更改数据库配置的一些参数显得尤为麻烦,这是可以新建一个properties
来写一些kv对,再在config中使用<properties>
标签来传值貌似就会简化一部分操作。
因此在src
下新建一个db.properties文件。如下配置:
1 2 3 4 driver =oracle.jdbc.driver.OracleDriver url =jdbc:oracle:thin:@127.0.0.1:1521:ORCL username =scott password =tiger
在config.xml
中添加
1 <properties resource ="db.properties" > </properties >
数据源中配置:
1 2 3 4 <property name ="driver" value ="${driver}" /> <property name ="url" value ="${url}" /> <property name ="username" value ="${username}" /> <property name ="password" value ="${password}" />
如此,每次想对数据源做更改的时候只需改变kv对即可。
mybatis的全局参数设置更改 谨慎更改👨🔧
1 2 3 <settings > <setting name ="" value ="" /> </settings >
设置别名 top.eshyee.entity.Student
太长了🤦♀️,想要改别名。
(大小写不敏感)
选择两种方式:
设置单个别名 1 2 3 <typeAliases > <typeAlias type ="top.eshyee.entity.Student" alias ="student" /> </typeAliases >
批量设置别名 1 2 3 <typeAliases > <package name ="top.eshyee.entity" /> </typeAliases >
还有一些内置别名。
小节 怎么方便怎么来呗🏊♀️
关于类型处理器和resultmap
类型处理器(类型转换器)
MyBatis自带一些常见的类型处理器
也可以自定义Mybatis类型处理器
JAVA 数据类型 –数据库(数据类型)
比如:
实体类 Student :Boolean stuSex true:男 /false:女
表中Student : number stuSex 1:男 / 0:女
自定义类型转换器 假设 我要在Student表里面新建一个性别列 男生用1 表示 女生用0表示(number类型),
此时实体类中我男生用的true 女生用的false(Boolean,仅仅是举个例子)。
数据类型不匹配此时数据类型不匹配。
创建类型转换器 需要实现TypeHandler接口 此接口有一个实现类 BaseTypeHandler
因此实现转换器有两种方法 实现 接口TypeHandler 和 继承BaseTypeHandler(简单)
所以这里采用后者,去extend
this method。
准备工作 新建一个Boolean的属性 叫做stuSex 。
private Boolean stuSex
形成get set方法
转换器 新建一个转换器继承BaseTypeHandler,编译器会自动生成三个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public Boolean getNullableResult (ResultSet rs, String columnName) throws SQLException { return rs.getInt(columnName)==1 ?true :false ; } public Boolean getNullableResult (ResultSet rs, int columnIndex) throws SQLException { return rs.getInt(columnIndex)==1 ?true :false ; } public Boolean getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { return cs.getInt(columnIndex)==1 ?true :false ; } public void setNonNullParameter (PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException { if (parameter) { ps.setInt(i, 1 ); }else { ps.setInt(i, 0 ); } }
其中,
ps:PreparedStatement对象 i:PreparedStatement对象操作的参数的位置 parameter:Java值 jdbcType:jdbc操作的数据类型
这里用到了三元运算符,想起了之前一个写个人网站用到的一个三元运算符🤦♂️(更)。
conf配置 在config中加上转换器
1 2 3 <typeHandlers > <typeHandler handler ="转换器类名" javaType ="Boolean" jdbcType ="INTEGER" /> </typeHandlers >
这样就算是好了 测试一下
mapper配置 使用了转换器的查询 1如果类中属性和表中的字段类型都能合理识别(String-varchar2),则可以使用resultType resultType=”top.eshyee.entity.Student” 否则(boolean-integer)使用resultMap 2如果类中的属性名和表中的字段名都能够合理识别(stuNo-stuno)则可以使用resultType 否则(id-stuno)使用resultMap
1 2 3 4 5 6 7 8 9 10 <select id ="queryStudentBystuNumConverter" resultMap ="studentResult" parameterType ="int" > select * from student where stuno= #{stuno} </select > <resultMap type ="top.eshyee.entity.Student" id ="studentResult" > <id property ="stuNo" column ="stuno" /> <result property ="stuName" column ="stuname" /> <result property ="stuAge" column ="stuage" /> <result property ="graName" column ="graname" /> <result property ="stuSex" column ="stusex" javaType ="boolean" jdbcType ="INTEGER" /> </resultMap >
resultMap resultMap可以实现两个功能: 1 类型转换 2 属性-字段名之间的映射关系
分为主键id和非主键result 如果实体类中的属性跟数据库中那个的字段名叫法不一样,从下面的映射关系中也可以更改 。
这样就好了,可以在test.java
中测试一下了。🥐
小结 以前用过别的方法来转换数据库数据与java数据 但是这个更系统一些吧。
输入参数和输出参数 输入参数parameterType 简单类型 ${}#{}比较
#{任意值}或者${value}(标识符只能是value)
#{}会自动给String加上’’(自动类型转换)
${}会原样输出,但是适合于动态排序(动态字段)
#{}可以防止sql注入
${}不可以
${}#{}相同之处
都可以获取对象的值,(嵌套类型对象)
对象类型 #{属性名}或者${属性名}(对象的属性名例如stuNo)
1 2 3 4 <select id ="queryStudentByAddress" resultType ="top.eshyee.entity.Student" parameterType ="address" > select stuno,stuname,stuage from student where homeaddress=#{homeAddress} or schooladdress='${schoolAddress}' </select >
嵌套类型输入参数为级联属性 1 2 3 4 5 6 7 <select id ="queryStudentByAddress" resultType ="top.eshyee.entity.Student" parameterType ="Student" > select stuno,stuname,stuage from student where homeaddress=#{address.homeAddress} or schooladdress='${address.schoolAddress}' </select > <select id ="queryStudentBystuNameOrAge" resultType ="top.eshyee.entity.Student" parameterType ="Student" > select stuno,stuname,stuage from student where stuname like '%${stuName}%' or stuage=#{stuAge} </select >
传入为hashmap 1 2 3 4 <select id ="queryStudentBystuNameOrAgewithHashmap" resultType ="top.eshyee.entity.Student" parameterType ="HashMap" > select stuno,stuname,stuage from student where stuname like '%${stuName}%' or stuage=#{stuAge} </select >
调用存储过程 存储过程的传入参数在mybatis中用map来传递(Hashmap) CALLABLE设置sql 的调用方式是存储过程 输出参数通过map的get方法获取
查询某个年级的所有学生总数 数据库里 1 2 3 4 5 6 create or replace procedure queryCountByGradeWithProcadure(gName in varchar ,scount out number ) as begin select count (1 ) into scount from student where graname= gname; end ; /
mapper中 1 2 3 4 5 <select id ="queryCountByGradeWithPrecadure" statementType ="CALLABLE" parameterType ="HashMap" > {CALL queryCountByGradeWithProcadure( #{gName,jdbcType=VARCHAR,mode=IN}, #{sCount,jdbcType=INTEGER,mode=OUT} )} </select >
根据学号删除学生 数据库里 1 2 3 4 5 6 create or replace procedure deleteStuBystunoWithProcedure(sno in number)as begin delete from student where stuno= sno;end ;/
mapper中 1 2 3 <delete id ="deletestuByStunoWithPrecadure" statementType ="CALLABLE" parameterType ="HashMap" > {CALL deleteStuBystunoWithProcedure(#{sno,jdbcType=INTEGER,mode=IN})} </delete >
输出参数 resultType 简单类型,实体对象类型,实体对象类型的集合之前有提到过,此处不再赘述。
输出为HashMap 通过别名作为map的key
1 2 3 <select id ="queryStudentWithHash" resultType ="HashMap" > select stuno "no",stuname "name" from student </select >
resultMap 解决实体类型属性与数据表字段名不一致(前面有用到)
也可以使用HashMap+resultType
关于动态sql if 按姓名和年龄查询
1 2 3 4 5 6 7 8 9 10 11 <select id ="qStuByNOrAWithSQLTag" parameterType ="top.eshyee.entity.Student" resultType ="top.eshyee.entity.Student" > select stuno ,stuname from student where 1=1 <if test ="stuName!=null and stuName!= ''" > student有stuname属性且不为null and stuname=#{stuName} </if > <if test ="stuAge!=null and stuAge!=0" > student有stunage属性且不为null and stuage=#{stuAge} </if > </select >
这里and会出问题
where where:处理第一个and
1 2 3 4 5 6 7 8 9 10 11 <select id ="qStuByNOrAWithSQLTag" parameterType ="top.eshyee.entity.Student" resultType ="top.eshyee.entity.Student" > select stuno ,stuname from student <where > <if test ="stuName!=null and stuName!= ''" > and stuname=#{stuName} </if > <if test ="stuAge!=null and stuAge!=0" > and stuage=#{stuAge} </if > </where > </select >
foreach 查询学号为1 2 4的学生学号信息
迭代的类型:数组、对象数组、集合、属性
数组 数组固定写法:array 这是约定
1 2 3 4 5 6 7 8 9 10 <select id ="queryStuwithNoWitharray" resultType ="top.eshyee.entity.Student" parameterType ="int[]" > select * from student <where > <if test ="array!=null and array.length>0" > <foreach collection ="array" open ="and stuno in(" close =")" item ="stuNo" separator ="," > #{stuNo} </foreach > </if > </where > </select >
放入对象的属性中 1 2 3 4 5 6 7 8 9 10 <select id ="queryStuwithNoInGra" resultType ="top.eshyee.entity.Student" parameterType ="top.eshyee.entity.Grade" > select * from student <where > <if test ="stuNos!=null and stuNos.size>0" > <foreach collection ="stuNos" open ="and stuno in(" close =")" item ="stuNo" separator ="," > #{stuNo} </foreach > </if > </where > </select >
集合 1 2 3 4 5 6 7 8 9 10 <select id ="queryStuwithNowithlist" resultType ="top.eshyee.entity.Student" parameterType ="top.eshyee.entity.Grade" > select * from student <where > <if test ="list!=null and list.size>0" > <foreach collection ="list" open ="and stuno in(" close =")" item ="stuNo" separator ="," > #{stuNo} </foreach > </if > </where > </select >
对象数组 必须使用Object[]
1 2 3 4 5 6 7 8 <select id ="queryStuwithNowithObjArr" resultType ="top.eshyee.entity.Student" parameterType ="Object[]" > select * from student <include refid ="objectArraStuno" > </include > </select >
sql片段 重复使用的提取出来
1 2 3 4 5 6 7 8 9 <sql id ="objectArraStuno" > <where > <if test ="array!=null and array.length>0" > <foreach collection ="array" open ="and stuno in(" close =")" item ="student" separator ="," > #{student.stuNo} </foreach > </if > </where > </sql >
关联查询 关于关联查询主要重点是配置好mapper
一对一 a.业务扩展类
核心:用resultType指定的类的属性包含多表查询的所有字段
b. resultMap
通过属性成员将2个类建立起联系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <select id ="qSByNWithO2O" resultType ="top.eshyee.entity.StudentBussiness" parameterType ="int" > select s.*,c.* from student s inner join studentcard c on s.cardid =c.cardid where s.stuno=#{stuNo} </select > <select id ="qSByNWithMapO2O" resultMap ="student_card_map" parameterType ="int" > select s.*,c.* from student s inner join studentcard c on s.cardid =c.cardid where s.stuno=#{stuNo} </select > <resultMap type ="student" id ="student_card_map" > <id property ="stuNo" column ="stuNo" /> <result property ="stuName" column ="stuName" /> <result property ="stuAge" column ="stuAge" /> <result property ="graName" column ="graName" /> <result property ="stuSex" column ="stuSex" /> <association property ="card" javaType ="StudentCard" > <id property ="cardId" column ="cardId" /> <result property ="cardInfo" column ="cardInfo" /> </association > </resultMap >
一对多(多对一) (MyBatis:多对一,多对多的本质就是一对多的变化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <select id ="qCAS" resultMap ="class_student_classid" parameterType ="int" > select c.*,s.* from student s inner join studentclass c on c.classid=s.classid where c.classid=#{classId} </select > <resultMap type ="top.eshyee.entity.StudentClass" id ="class_student_classid" > <id property ="classId" column ="classId" /> <result property ="className" column ="className" /> <collection property ="student" ofType ="top.eshyee.entity.Student" > <id property ="stuNo" column ="stuNo" /> <result property ="stuName" column ="stuName" /> <result property ="stuAge" column ="stuAge" /> <result property ="graName" column ="graName" /> <result property ="stuSex" column ="stuSex" /> </collection > </resultMap >
多对多 多对多 可由两个多对一等方法实现!
log4j和延迟加载 在之前下载的mybatis包中找到log4j并导入到项目
开启日志 如果不指定,Mybatis就会根据以下顺序寻找日志 SLF4J →Apache Commons Logging →Log4j 2→Log4j → JDK logging 日志级别: DEBUG< INFO< WARN< ERROR 如果设置为info, 则只显示info及以上级别的信息; 建议: 在开发时设置debug,在运行时设置为info或以上。
1 2 3 <settings > <setting name ="logImpl" value ="LOG4J" /> </settings >
每次运行就会出现诸如此类的debug
1 2 3 4 5 6 7 DEBUG [main] - PooledDataSource forcefully closed/removed all connections. DEBUG [main] - PooledDataSource forcefully closed/removed all connections. DEBUG [main] - PooledDataSource forcefully closed/removed all connections. DEBUG [main] - PooledDataSource forcefully closed/removed all connections. DEBUG [main] - Opening JDBC Connection DEBUG [main] - Created connection 961160488. DEBUG [main] - Setting autocommit to false on JDBC Connection [oracle.jdbc.driver.T4CConnection@394a2528]
配置延迟加载 嵌套查询多用于延迟加载的设计
具体如下
一对一:查学生和学生卡信息 在xml中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <select id ="qSByNWithMapO2OLazy" resultMap ="student_card_Lazyload_map" parameterType ="int" > select * from student </select > <resultMap type ="student" id ="student_card_Lazyload_map" > <id property ="stuNo" column ="stuNo" /> <result property ="stuName" column ="stuName" /> <result property ="stuAge" column ="stuAge" /> <result property ="graName" column ="graName" /> <result property ="stuSex" column ="stuSex" /> <association property ="card" javaType ="StudentCard" select ="top.eshyee.mapper.StudentCardMapper.querycardbyid" column ="cardid" > </association > </resultMap >
新建Cardmapper
1 2 3 4 5 <select id ="querycardbyid" parameterType ="int" resultType ="top.eshyee.entity.StudentCard" > select * from studentCard where cardid=#{cardId} </select >
一对多:查班级和学生信息 新建班级mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 <select id ="qCASLazy" resultMap ="class_student_classid_lazy" parameterType ="int" > select c.* from studentclass c </select > <resultMap type ="top.eshyee.entity.StudentClass" id ="class_student_classid_lazy" > <id property ="classId" column ="classId" /> <result property ="className" column ="className" /> <collection property ="student" ofType ="top.eshyee.entity.Student" select ="top.eshyee.mapper.StudentMapper.queryStuByClassId" column ="classId" > </collection > </resultMap >
在studentmapper中
1 2 3 4 <select id ="queryStuByClassId" parameterType ="int" resultType ="top.eshyee.entity.Student" > select * from student where classId=#{classId} </select >
注 一对多和一对一的延迟加载配置方法相同
mybatis 缓存 查询缓存 一级缓存:同一个SqlSession对象
MyBatis默认开启一级缓存,如果用同样的SqlSession对象查询相同的数据,则只会在第一次查询时向数据库发送SQL语句,并将查询的结果放入到SQLSESSION中(作为缓存在) ;后续再次查询该同样的对象时则直接从缓存中查询该对象即可(即省略了数据库的访问)。
二级缓存
Mybatis自带二级缓存: [同一个namespace]生成的mapper对象
MyBati s默认情况没有开启二级缓存,需要手工打开。
conf.xml中
1 2 <!-- 开启二级缓存 --> <setting name="cacheEnable" value="true"/>
mapper中
异常提示:NotSerializableException可知,MyBatis的二级缓存是将对象放入硬盘文件中
序列化:内存->硬盘
反序列化:硬盘->内存
准备缓存的对象,必须实现了序列化接口( 如果开启的缓存Namespace="top.eshyee.mapper.StudentMapper"
可知序列化对象为Student,因此需要将Student序列化(序 列化Student类,以及Student的级联属性、和父类)
触发将对象写入二级缓存的时机: SqlSession对象的close()方法
回顾: namespace的值 就是接口的全类名(包名.类名) ,通过接口可以产生代理对象(Mapper对象)
–>namespace决定了Mapper对象的产生
结论:只要产生的xxxMapper对象来自于同一个namespace,则这些对象共享二级缓存。
如果是同一个SqISession对象进行多次相同的查询,则直接进入一级缓存查询;
如果不是同一个SqISession对象进行多次相同的查询(但是均来自同一个namespace)则进入二级缓存查询
如果一个namespace, 如果有多个xxMapper. xml的namespace值相同, 则通过这些xxxMapper. xml产生的xxMapper,仍然共享二级缓存。
禁用:
在需要禁用的select标签中的usecache
属性改为false
清理:与清理一级缓存的方法相同,一般执行增删改时会清理掉缓存 ,设计的原因是为了防止脏数据
commit();
commit会清理一级和二级缓存;但是清理二级缓存时,不能是查询自身的commit()
改标签
在select标签中设置flushCache= "true"
命中率: 1 :0% 0.0
2:50% 0.5
3:2/3 0.666
4:3/4 0.75
三方提供的二级缓存:
ehicache、memcache
要想整合三方提供的二级缓存(或者自定义二级缓存) ,必须实现org.apache.ibatis.cache.Cache
接口,该接口的默认实现类是PerpetualCache
整合ehcache二级缓存:
Ehcache-core.jar mybatis- Ehcache.jar slf4j-api.jar
编写ehcache配置文件Ehcache.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <ehcache xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation ="ehcache.xsd" updateCheck ="true" monitoring ="autodetect" dynamicConfig ="true" > <diskStore path ="D:\Ehcache" /> <defaultCache maxElementsInMemory ="1000" maxElementsOnDisk ="1000000" eternal ="false" overflowToDisk ="false" timeToIdleSeconds ="100" timeToLiveSeconds ="100" diskExpiryThreadIntervalSeconds ="120" memoryStoreEvictionPolicy ="LRU" > </defaultCache > </ehcache >
配置mapper
1 2 3 4 <cache type = "org.mybatis.caches.ehcache.EhcacheCache" > <property name = "maxELementsInMemory" value = "2000" /> 可以覆盖掉默认值 </cache >