SSM-MyBatis(四)-二级缓存、dao层实现
MyBatis的两级缓存
- 缓存:是指为了减少数据库直接访问次数、提高访问效率,而临时存储在内存中的数据.
- 数据库是存储在硬盘上的文件,每次查询都进行了IO操作,效率较低
- 适合存放到缓存中的数据:经常查询、不经常改变、数据的正确性对最终结果影响不大
- 举例一些不适合放到缓存中的数据:商品库存、股票 黄金的价格、汇率
- MyBatis的缓存分为一级缓存、二级缓存,用于缓存select的结果
一级缓存
- 一级缓存是存放到了SqlSession对象中
- 同一个SqlSession的select共享缓存
- 所以当关闭SqlSession时,缓存也就失效了
- 执行insert、update、delete、commit等方法时,会自动清理一级缓存
- 由于在很多时候,每次查询用的都是不同的SqlSession,所以一级缓存的命中率并不高
-
一级缓存代码举例:
public void firstLevel() { try (SqlSession session = MyBatises.openSession()) { Job job1 = session.selectOne("job.get", 1); System.out.println(job1); Job job2 = session.selectOne("job.get", 1); System.out.println(job2); //job1与job2打印一样 } //不同session,关闭SqlSession时,缓存也就失效了 try (SqlSession session = MyBatises.openSession()) { Job job3 = session.selectOne("job.get", 1); System.out.println(job3); //job1与job3打印不一样,不同的session } //执行insert、update、delete、commit等方法时,会自动清理一级缓存 try (SqlSession session = MyBatises.openSession()) { Job job4 = session.selectOne("job.get", 1); System.out.println(job4); //会清除一级缓存 session.update("job.update", null); Job job5 = session.selectOne("job.get", 1); System.out.println(job5); //job4与job5打印不一样 } }
二级缓存
- 为了提高缓存的命中率,可以考虑开启MyBatis的二级缓存,它是namespace(mapper)级别的缓存
- 同一个namespace下的select共享缓存
- 默认情况,namespace下update、insert、delete执行成功时,会自动清理二级缓存
- 当调用SqlSession的close方法时,会将查询结果放进二级缓存
开启二级缓存
-
在mybatis-config.xml中配置
<setting name="cache Enabled" value="true"/>
-
在映射文件的mapper中添加cache标签,默认会缓存映射文件中的所有select的结果
//job.xml <mapper namespace="job"> <cache readOnly="false"/> <sql id="sqlListAll"> SELECT * FROM job </sql> <select id="list" resultType="zh.bean.Job"> <include refid="sqlListAll"/> </select> <select id="get" parameterType="int" resultType="zh.bean.Job"> <include refid="sqlListAll"/> WHERE id = #{id} </select> <update id="update" parameterType="zh.bean.Job"> UPDATE job SET name = #{name}, duty = #{duty} WHERE id = #{id} </update> </mapper>
- cache标签的常用属性有
- size:缓存多少个存储结果(单个对象或一个列表都属于一个引用) 的引用,默认值是1024
- eviction:当缓存数量超过size时的清除策略。可选值有LRU(默认值)、FIFO、SOFT、WEAK
- LRU – 最近最少使用:移除最长时间不被使用的对象。
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
- WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
- flush Interval:每隔多少毫秒清除一次缓存, 默认不会定时清除缓存
- readOnly: true代表缓存的是对原对象的引用,false代表缓存的是原对象序列化后的拷贝对象
- 所以false时要求bean实现Serializable接口。默认值是false
- readOnly为true,代表:只读,二级缓存中是:原对象,不安全,会修改原来对象的值
-
readOnly为false,代表:可读写,二级缓存中是:原对象序列化后的结果(相当于深拷贝),安全,不会修改原来对象的值
package zh.bean; import java.io.Serializable; //bean实现Serializable接口 public class Job implements Serializable { private Integer id; private String name; private String duty; //get、set方法,略 }
- 代码举例:
//Job.xml中 <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
-
测试代码举例
public void secondLevel() { try (SqlSession session = MyBatises.openSession()) { Job job1 = session.selectOne("job.get", 1); job1.setName("ZH666"); System.out.println(job1 + "_" + job1.getName()); } try (SqlSession session = MyBatises.openSession()) { Job job2 = session.selectOne("job.get", 1); System.out.println(job2 + "_" + job2.getName()); } //readOnly为true,则job1与job2为同一个对象 //readOnly为false,则job1与job2不是同一个对象,但是job2的name也为ZH666 //原因:只有session在调用close的时候才会将数据放入缓存,所以job1.setName("ZH666");是在放入缓存之前设置的,因此job2从缓存中复制出的name也为ZH666 }
- 总结:放入二级缓存的时机就是session调用close方法时
userCache、flushCache
- 可以通过设置userCache属性来决定某个select是否需要开启二级缓存
- 注意:前提条件是全局必须开启二级缓存,而且映射文件的mapper中添加cache标签。
<select id="get" useCache="false" parameterType="int" resultType="zh.bean.Job"> <include refid="sqlListAll"/> WHERE id = #{id} </select>
-
可以通过设置flushCache属性来决定某个操作之后,是否需要清除缓存
<update id="update" flushCache="false" parameterType="zh.bean.Job"> UPDATE job SET name = #{name}, duty = #{duty} WHERE id = #{id} </update>
使用MyBatis实现dao层
- 使用MyBatis实现dao层的几种方式
- 方法一: 自定义dao实现类impl,在实现中通过SqlSession实现API(使用XML)
- 方法二:只定义dao接口类,SqlSession的getMapper方法生成dao的代理对象(使用XML)impl,然后通过daoimpl直接调用API(daoimpl不需要手动封装SqlSession调用API的代码)
- 方法三:只定义dao接口类,SqlSession的getMapper方法生成dao的代理对象 (使用注解)impl。
- 目前注解的功能并没有XML强大,所以也可以XML+注解混合使用
初始化项目
-
创建bean对象skill
//com.zh.bean.Skill public class Skill extends Bean { private Integer id; private Date createdTime; private String name; private Integer level; //get/set方法略 public Skill() {} }
- 引入druid第三方连接池
- pom.xml添加依赖
-
添加durid.properties文件
dev.driverClass=com.mysql.jdbc.Driver dev.url=jdbc:mysql://localhost:3306/test-mybatis dev.username=root dev.password=root dev.initialSize=5 dev.maxActive=10 dev.maxWait=5000
- mybatis封装
- pom.xml引入mybatis
-
配置全局文件mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="druid.properties" /> <settings> <!-- name与value的内容在mysql官方可以查询:开启驼峰命名自动映射,比如从数据库列名my_age映射到Bean属性名myAge --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <!-- 别名 --> <typeAliases> <!-- 一旦设置了别名,它是不区分大小写的 --> <typeAlias type="com.zh.bean.Skill" alias="skill" /> <typeAlias type="com.zh.common.DruidDataSourceFactory" alias="druid" /> </typeAliases> <!-- 设置使用什么环境--> <environments default="development"> <!-- 开发环境(调试阶段) --> <environment id="development"> <!-- 采用JDBC的事务管理方法 --> <transactionManager type="JDBC" /> <dataSource type="DRUID"> <property name="driverClass" value="${dev.driverClass}"/> <property name="url" value="${dev.url}"/> <property name="username" value="${dev.username}"/> <property name="password" value="${dev.password}"/> <property name="initialSize" value="${dev.initialSize}"/> <property name="maxActive" value="${dev.maxActive}"/> <property name="maxWait" value="${dev.maxWait}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/skill.xml"/> </mappers> </configuration>
-
创建UnpooledDataSourceFactory子类,设置mybatis的数据源为durid
//com.zh.common.DruidDataSourceFactory package com.zh.common; import com.alibaba.druid.pool.DruidDataSource; import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory; public class DruidDataSourceFactory extends UnpooledDataSourceFactory { public DruidDataSourceFactory() { this.dataSource = new DruidDataSource(); } }
-
封装SqlSession
//com.zh.util.MyBatises public class MyBatises { private static SqlSessionFactory factory; static { try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml")) { // 创建一个工厂 factory = new SqlSessionFactoryBuilder().build(reader); } catch (Exception e) { e.printStackTrace(); } } public static SqlSession openSession(boolean autoCommit) { return factory.openSession(autoCommit); } public static SqlSession openSession() { return factory.openSession(); } }
方法一:自定义dao实现类
-
添加mapper:skill.xml,添加增删该改查的xml
<?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="skill"> <sql id="sqlList">SELECT * FROM skill</sql> <select id="get" parameterType="int" resultType="Skill"> <include refid="sqlList"/> WHERE id = #{id} </select> <select id="list" resultType="Skill"> <include refid="sqlList"/> </select> <update id="update" parameterType="Skill"> UPDATE skill SET name = #{name}, level = #{level} WHERE id = #{id} </update> <insert id="save" useGeneratedKeys="true" keyProperty="id" parameterType="Skill"> INSERT INTO skill(name, level) VALUES(#{name}, #{level}) </insert> <delete id="remove" parameterType="int"> DELETE FROM skill WHERE id = #{id} </delete> </mapper>
- 新建dao层(dao层分为接口+实现)
-
接口SkillDao
package com.zh.dao; import com.zh.bean.Skill; import java.util.List; public interface SkillDao { boolean save(Skill skill); boolean update(Skill skill); boolean remove(Integer id); Skill get(Integer id); List<Skill> list(); }
-
SkillDao接口实现SkillDaoImpl
package com.zh.dao.impl; import com.zh.bean.Skill; import com.zh.dao.SkillDao; import com.zh.util.MyBatises; import org.apache.ibatis.session.SqlSession; import java.util.List; public class SkillDaoImpl implements SkillDao { @Override public boolean save(Skill skill) { try (SqlSession session = MyBatises.openSession(true)) { return session.insert("skill.save", skill) > 0; } } @Override public boolean update(Skill skill) { try (SqlSession session = MyBatises.openSession(true)) { return session.update("skill.update", skill) > 0; } } @Override public boolean remove(Integer id) { try (SqlSession session = MyBatises.openSession(true)) { return session.delete("skill.remove", id) > 0; } } @Override public Skill get(Integer id) { try (SqlSession session = MyBatises.openSession()) { return session.selectOne("skill.get", id); } } @Override public List<Skill> list() { try (SqlSession session = MyBatises.openSession()) { return session.selectList("skill.list"); } } }
-
-
测试代码SkillTest
@Test public void get() { SkillDao dao = new SkillDaoImpl(); System.out.println(dao.get(1)); } @Test public void list() { SkillDao dao = new SkillDaoImpl(); System.out.println(dao.list()); } @Test public void save() { SkillDao dao = new SkillDaoImpl(); Assert.assertTrue(dao.save(new Skill("mj888", 100))); } @Test public void update() { SkillDao dao = new SkillDaoImpl(); Skill skill = new Skill("666", 99); skill.setId(80); Assert.assertTrue(dao.update(skill)); } @Test public void remove() { SkillDao dao = new SkillDaoImpl(); Assert.assertTrue(dao.remove(80)); }
- 缺点: SkillDaoImpl代码过于累赘,如果有n个impl,那么不同的仅仅是“skill.list”等语句
方法二:getMapper+XML实现dao层
- 有2个配置要求
- mapper的namespace必须是dao接口类的全类名
- mapper中select、update、insert、delete的id值必须和dao的方法名一致
- 如果update、insert、delete方法的返回值是Boolean类型
- 代理对象内部是影响记录数大于0就返回true
- 参考源码:org.apache.ibatis.binding.MapperMethod.rowCountResult方法
- 通过SqlSession的getMapper方法生成对应dao接口的实现类-代理类,然后用代理类调用api
代码举例
- 删除dao的实现SkillDaoImpl
- 改造skil.xml
- namespace修改为:
com.mj.dao.SkillDao
- 将里面对应的sql操作语句的id变为SkillDao接口对应的方法名
<mapper namespace="com.zh.dao.SkillDao"> <sql id="sqlList">SELECT * FROM skill</sql> <update id="update" parameterType="Skill"> UPDATE skill SET name = #{name}, level = #{level} WHERE id = #{id} </update> <insert id="save" useGeneratedKeys="true" keyProperty="id" parameterType="Skill"> INSERT INTO skill(name, level) VALUES(#{name}, #{level}) <!-- <selectKey keyProperty="id" resultType="int" order="AFTER">--> <!-- SELECT LAST_INSERT_ID()--> <!-- </selectKey>--> </insert> <delete id="remove" parameterType="int"> DELETE FROM skill WHERE id = #{id} </delete> <!--constructor: 指定调用有参数的构造方法,默认调用无参的构造方法--> <resultMap id="rmGet" type="Skill"> <constructor> <!--专门用来指定主键id映射--> <!--<idArg></idArg>--> <arg name="level" column="level"/> <arg name="name" column="name"/> </constructor> </resultMap> <!-- 该句的本质是先sql查询数据,然后创建一个Skill对象,将数据映射给对象属性 通常创建对象是直接调用Skill类的无参构造函数,但是也可以指定调用其他构造函数 指定调用哪个构造函数通过<constructor>标签 --> <select id="get" parameterType="int" resultMap="rmGet"> <include refid="sqlList"/> WHERE id = #{id} </select> <select id="list" resultType="Skill"> <include refid="sqlList"/> </select> </mapper>
-
注意:如果使用指定构造函数,bean对象的构造函数参数要添加
@param
修饰public Skill(@Param("name") String name, @Param("level") Integer level) { this.name = name; this.level = level; }
- namespace修改为:
-
测试代码SkillTest
@Test public void get() { try (SqlSession session = MyBatises.openSession()) { // 根据SkillDao接口,生成SkillDao的代理对象 SkillDao dao = session.getMapper(SkillDao.class); System.out.println(dao.get(1)); } } @Test public void list() { try (SqlSession session = MyBatises.openSession()) { // 生成SkillDao的代理对象 SkillDao dao = session.getMapper(SkillDao.class); System.out.println(dao.list()); } } @Test public void save() { try (SqlSession session = MyBatises.openSession(true)) { // 生成SkillDao的代理对象 SkillDao dao = session.getMapper(SkillDao.class); Assert.assertTrue(dao.save(new Skill("mj888", 100))); } } @Test public void update() { try (SqlSession session = MyBatises.openSession(true)) { // 生成SkillDao的代理对象 SkillDao dao = session.getMapper(SkillDao.class); Skill skill = new Skill("666", 99); skill.setId(80); Assert.assertTrue(dao.update(skill)); } } @Test public void remove() { try (SqlSession session = MyBatises.openSession(true)) { // 生成SkillDao的代理对象 SkillDao dao = session.getMapper(SkillDao.class); Assert.assertTrue(dao.remove(78)); } }
方法三:getMapper+注解实现dao层
- 首先要在mybatis-config.xml中配置dao的位置
- 方法一:
<mapper class="dao的全类名"/>
- 方法二:
<packagename="dao所在的包"/>
- 方法一:
-
常用注解
@Select、@Insert、@Update、@Delete、@Selectedkey @Param:设置参数名,@Options:设置其他属性值,@CacheNamespace:对应<cache> @Results、@ResultMap:对应<resultMap>,@Result:对应<id>、<result> @One对应<association>,@Many对应<collection> @ConstructorArgs:对应<constructor>,@Arg:对应<idArg>、<arg>
- 其他知识
- 可以使用
<script>
嵌套其他XML标签中的内容 - 参考资料:https://mybatis.org/mybatis-3/zh/java-api.html
- 可以使用
- 纯注解的优缺点
- 优点:sql语句直接写在代码中,简洁而且容易理解
- 缺点:项目编译后直接成为字节码class文件,无法手动修改。xml可在服务器手动修改sql语句
- 通常可以用xml与注解混合使用
代码举例
- 删除mappers以及skil.xml
-
在mybatis-config.xml文件中的mapper修改如下:
<mapper class="com.zh.dao.SkillDao"/>
-
在SkillDao中使用注解
//设置二级缓存,readWrite等价于readOnly //@CacheNamespace(flushInterval = 600000, size = 512, readWrite = true) public interface SkillDao { @Insert("INSERT INTO skill(name, level) VALUES (#{name}, #{level})") //设置id属性,方法1: //@SelectKey(statement = "SELECT LAST_INSERT_ID()", // keyProperty = "id", before = false, resultType = Integer.class) //方法2 //@Options(useGeneratedKeys = true, keyProperty = "id") boolean save(Skill skill); @Update("UPDATE skill SET name = #{name}, level = #{level} WHERE id = #{id}") boolean update(Skill skill); @Delete("DELETE FROM skill WHERE id = #{id}") boolean remove(Integer id); @Select("SELECT * FROM skill WHERE id = #{id}") Skill get(Integer id); @Select("SELECT * FROM skill") List<Skill> list(); //设置参数名称:传参 @Select("SELECT * FROM skill LIMIT #{start}, #{size}") List<Skill> listByStartAndSize(@Param("start") int start, @Param("size") int size); //批量保存,script标签内部可以引用xml中直接使用的标签内容 //因为注解没有foreach语句 @Insert("<script>" + "INSERT INTO skill(name, level) VALUES" + "<foreach collection=\"list\" item=\"skill\" separator=\",\">" + "(#{skill.name}, #{skill.level})" + "</foreach>" + "</script>" ) boolean batchSave(@Param("skills") List<Skill> skills); }
-
test测试
//其余跟方法二一样 //新增 @Test public void list2() { try (SqlSession session = MyBatises.openSession()) { // 生成SkillDao的代理对象 SkillDao dao = session.getMapper(SkillDao.class); List<Skill> skills = dao.listByStartAndSize(0, 10); for (Skill skill : skills) { System.out.println(skill); } } }
复杂数据结构的注释使用
- 当bean对象属性有对象类型、集合类型如何使用注释?
- 仍然引入多表关系使用的复杂数据:一个人有n张银行卡、一张身份证、n分职业的场景
- bean中添加Person、Job、IdCard、BandCard
- dao文件中添加对应的接口文件
-
PersonDao文件:
public interface PersonDao { @Select("SELECT * FROM person WHERE id = #{id}") //@Results相当于<resultMap>,@Result:对应<id>、<result> @Results(id = "get", value = { //id=ture:代表是id标签,默认false,等价于 <id property="id" column="id"/> @Result(property = "id", column = "id", id = true), @Result(property = "name", column = "name"), /* 身份证 */ //column是给select传参数 @Result( property = "idCard", column = "id", one = @One(fetchType = FetchType.LAZY, select = "com.zh.dao.IdCardDao.getByPerson") ), /* 银行卡 */ @Result( property = "bankCards", column = "id", many = @Many(fetchType = FetchType.LAZY, select = "com.zh.dao.BankCardDao.listByPerson") ), /* 工作 */ @Result( property = "jobs", column = "id", many = @Many(fetchType = FetchType.LAZY, select = "com.zh.dao.JobDao.listByPerson") ) }) Person get(Integer id); @Select("SELECT * FROM person") /* 引用id为get的@Results,ResultMap代表返回数据为集合,每个元素的映射方式 */ @ResultMap("get") List<Person> list(); //xml与注解混合使用:查询sql在Person.xml中 Person testGet(); // 演示标签使用 // @insert("<script>" + // // ) boolean batchSave(@Param("skills") List<Skill> skills); }
-
其他dao文件:
public interface BankCardDao { //通过id查询银行卡信息 @Select("SELECT * FROM bank_card WHERE person_id = #{personId}") List<BankCard> listByPerson(Integer personId); } public interface IdCardDao { //通过id查询身份证信息 @Select("SELECT * FROM id_card WHERE person_id = #{personId}") IdCard getByPerson(Integer personId); } public interface JobDao { //通过id查询工作信息 @Select( "SELECT j.* FROM job j " + "JOIN person_job pj ON j.id = pj.job_id AND pj.person_id = #{personId}" ) List<Job> listByPerson(Integer personId); }
-
-
混合注解、xml开发,新建一个person.xml
<mapper namespace="com.zh.dao.PersonDao"> <select id="testGet" resultType="Person"> SELECT * FROM person WHERE id = 1 </select> </mapper>
-
核心配置mybatis-config.xml文件
<configuration> <!-- 这样写就相当于下面注释的内容--> <properties resource="druid.properties" /> <!--其他设置 --> <settings> <!-- name与value的内容在mysql官方可以查询:开启驼峰命名自动映射,比如从数据库列名my_age映射到Bean属性名myAge --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <!-- 别名 --> <typeAliases> <!-- 一旦设置了别名,它是不区分大小写的 --> <typeAlias type="com.zh.bean.Skill" alias="skill" /> <typeAlias type="com.zh.bean.Person" alias="Person" /> <typeAlias type="com.zh.common.DruidDataSourceFactory" alias="druid" /> </typeAliases> <!-- 设置使用什么环境--> <environments default="development"> <!-- 开发环境(调试阶段) --> <environment id="development"> <!-- 采用JDBC的事务管理方法 --> <transactionManager type="JDBC" /> <dataSource type="DRUID"> <property name="driverClass" value="${dev.driverClass}"/> <property name="url" value="${dev.url}"/> <property name="username" value="${dev.username}"/> <property name="password" value="${dev.password}"/> <property name="initialSize" value="${dev.initialSize}"/> <property name="maxActive" value="${dev.maxActive}"/> <property name="maxWait" value="${dev.maxWait}"/> </dataSource> </environment> </environments> <mappers> <mapper class="com.zh.dao.SkillDao"/> <!--与<mapper resource="mappers/person.xml"/>重复导入--> <!-- <mapper class="com.zh.dao.PersonDao"/>--> <mapper class="com.zh.dao.IdCardDao"/> <mapper class="com.zh.dao.JobDao"/> <mapper class="com.zh.dao.BankCardDao"/> <!--xml与注解,混合使用,注意:不能重复导入:这个包含了导入PersonDao --> <mapper resource="mappers/person.xml"/> <!--可以通过这种方式,导出该包下所有的dao --> <!--<package name="com.zh.dao"/>--> </mappers> </configuration>
其他知识点
- 去除编译的时候SSL警告
-
在duirid.proprties文件数据库连接地址修改如下:(后面添加userSSL)
dev.url=jdbc:mysql://localhost:3306/test-mybatis?useSSL=false
-