MyBatis 学习笔记
什么是 MyBatis?
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
环境搭建
Maven模块导入
pom.xml
<!-- mysql-connector-java -->
<!-- mybatis -->
<!-- junit -->
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
配置文件
mybatis-config.xml
<configuration>
<!-- 加载类路径下的属性文件 -->
<properties resource="db.properties"/>
<!-- 默认连接环境配置 -->
<environments default="mysql_developer">
<!-- 连接环境信息 -->
<environment id="mysql_developer">
<!-- mybatis使用jdbc事务管理方式 -->
<transactionManager type="jdbc"/>
<!-- 使用连接池的方式来获取连接 -->
<dataSource type="pooled">
<!-- 配置与数据库交互的4个必要属性 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3307/lpxz_blog?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong" />
<property name="username" value="test" />
<property name="password" value="1234" />
</dataSource>
</environment>
</environments>
</configuration>
编写 Mybatis 工具类获取 sqlSession
MybatisUtil.java
/**** imports ****/
public class MybatisUtil {
private static final ThreadLocal<SqlSession> threadLocal = new ThreadLocal<>();
private static final SqlSessionFactory sqlSessionFactory;
/* 加载 mybatis-config.xml 配置文件 */
static {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 禁止外界通过 new 方法创建
*/
private MybatisUtil() {
}
/**
* 获取 SqlSession
*/
public static SqlSession getSqlSession() {
// 从当前线程中获取 SqlSession 对象
SqlSession sqlSession = threadLocal.get();
// 如果 SqlSession 对象为空
if (sqlSession == null) {
// 在SqlSessionFactory 非空的情况下,获取 SqlSession 对象
sqlSession = sqlSessionFactory.openSession();
// 将 SqlSession 对象与当前线程绑定在⼀起
threadLocal.set(sqlSession);
}
// 返回 SqlSession 对象
return sqlSession;
}
/**
* 关闭SqlSession与当前线程分开
*/
public static void closeSqlSession() {
// 从当前线程中获取 SqlSession 对象
SqlSession sqlSession = threadLocal.get();
// 如果 SqlSession 对象非空
if (sqlSession != null) {
// 关闭 SqlSession 对象
sqlSession.close();
// 分开当前线程与 SqlSession 对象的关系,目的是让 GC 尽早回收
threadLocal.remove();
}
}
/**
* 测试
*/
public static void main(String[] args) {
Connection conn = MybatisUtil.getSqlSession().getConnection();
System.out.println(conn != null ? "连接成功" : "连接失败");
}
}
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}
在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。
Dao 层
UserInfoDao.java
public interface UserInfoDao {
List<UserInfo> getUserInfoList();
}
接口实现类
UserInfoMapper.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">
<!-- namespace=绑定一个对应的 Dao/Mapper 接口 -->
<mapper namespace="com.lpxz.lpxzblog.dao.UserInfoDao">
<select id="getUserInfoList" resultType="com.lpxz.lpxzblog.entity.UserInfo">
select * from user_info
</select>
</mapper>
测试类
UserInfoDaoTest.java
@Test
public void findUserInfoById() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
UserInfoDao dao = sqlSession.getMapper(UserInfoDao.class);
UserInfo userInfo = dao.getUserInfoById(1); // 参数为 id
System.out.println(userInfo);
// 提交并关闭 SqlSession
sqlSession.commit();
sqlSession.close();
}
@Test
public void getUserInfoList() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
UserInfoDao dao = sqlSession.getMapper(UserInfoDao.class);
List<UserInfo> userInfoList = dao.getUserInfoList();
System.out.println(userInfoList);
// 提交并关闭 SqlSession
sqlSession.commit();
sqlSession.close();
}
CRUD (Create, Retrieve, Update, Delete)
namespace
指定了类路径下的 Dao/Mapper文件
select, insert, update, delete
id对应的 namespace 中的方法名
resultType sql语句执行的返回值类型
parameterType 参数类型
Map 传递参数
模糊查询(like “%%”)
Configuration
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
类型别名(typeAliases)
方法一 可以自定义别名
<typeAliases>
<typeAlias type="com.lpxz.lpxzblog.entity.UserInfo" alias="UserInfo"/>
</typeAliases>
方法二 通过注解实现别名:@Alias(“${definedName}”)
<typeAliases>
<package name="com.lpxz.lpxzblog.entity"/>
</typeAliases>
resultMap
结果集映射
UserInfo.java
@Data
@TableName("user_info") // @TableName中的值对应着表名
public class UserInfo {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer password; // 将 pwd 改为 passsword
}
UserInfoMapper.xml
<select id="getUserInfoList" resultMap="userMap">
select * from user_info
</select>
<!-- 结果集映射 -->
<resultMap id="userMap" type="userInfo">
<!-- column->数据库中的字段 property->实体类中的属性 -->
<result column="id" property="id"></result>
<result column="name" property="name"></result>
<result column="pwd" property="password"></result>
</resultMap>
复杂的使用还没有讲
日志工厂
如果一个数据库操作出现了异常,我们需要排错,日志是最好的助手。
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
Log4j
通过修改配置文件,控制每一条日志的输出格式,定义每一条日志信息的级别,不需要修改应用的代码。
日志可以输出到控制台、文件、GUI 组件
mybatis-config.xml
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
pom.xml
<!-- 日志 Log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j-version}</version>
</dependency>
log4j.properties
# 日志等级
log4j.rootLogger=DEBUG,console,file
# 控制台输出
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Threshold=DEBUG
#log4j.appender.console.ImmediateFlush=true
log4j.appender.console.Target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
# 文件输出
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/lpxzLog.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
# 日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
使用
logger.info("info");
logger.debug("debug");
logger.error("error!");
分页
为什么要分页?
- 减少数据的处理量
Limit 分页
SELECT * from user limit {startIndex}, {pageSize}; -- 开始位置和页面容量
SELECT * from user limit {n}; -- [0, n]
<mapper>
<!-- 分页 -->
<select id="getUserByLimit" parameterType="map" resultType="UserInfo">
select * from user_info limit #{startIndex}, #{pageSize}
</select>
</mapper>
UserInfoMapperTest.java
/**
* 分页
*/
@Test
public void getUserInfoByLimit() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("startIndex", 0);
map.put("pageSize", 2);
List<UserInfo> userInfoList = mapper.getUserByLimit(map);
for (UserInfo userInfo : userInfoList) {
System.out.println(userInfo);
}
sqlSession.close();
}
RouBounds 分页
面向对象方法实现分页,仅作了解
使用注解开发
AOP 面向接口编程思想
UserInfoMapper.java
// 查询全部用户信息 注解方式
@Select("select * from user_info")
List<UserInfo> getUserInfoListAOP();
mybatis-config.xml
<mappers>
<!-- 绑定接口 -->
<mapper class="com.lpxz.lpxzblog.dao.UserInfoMapper"/>
</mappers>
关于 @Param
@Param 注解用于给方法参数起一个名字。以下是总结的使用原则:
- 在方法只接受一个参数的情况下,可以不使用 @Param。
- 在方法接受多个参数的情况下,建议一定要使用 @Param注解给参数命名。
- 如果参数是 JavaBean , 则不能使用 @Param。
- 不使用 @Param 注解时,参数只能有一个,并且是 Java Bean。
# 与 $ 的区别
#{} 的作用主要是替换预编译语句(PrepareStatement)中的占位符? 【推荐使用】
INSERT INTO user (name) VALUES (#{name}); INSERT INTO user (name) VALUES (?);
${} 的作用是直接进行字符串替换
INSERT INTO user (name) VALUES ('${name}'); INSERT INTO user (name) VALUES ('kuangshen');
Lombok
@Data @AllArgsConstructor @NoArgsConstructor
…
多对一的处理
多对一的理解:
- 多个学生对应一个老师
搭建测试环境
创建实体类
@Data
//GET,SET,ToString,有参,无参构造
public class Teacher {
private int id;
private String name;
}
@Data
public class Student {
private int id;
private String name;
//多个学生可以是同一个老师,即多对一
private Teacher teacher;
}
编写实体类对应的Mapper接口
public interface StudentMapper {
}
public interface TeacherMapper {
}
编写Mapper接口对应的 mapper.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="com.kuang.mapper.StudentMapper"/>
<?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.kuang.mapper.TeacherMapper"/>
按查询嵌套处理
StudentMapper.java
// 获取所有学生及对应老师的信息
List<Student> getStudents();
StudentMapper.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="com.kuang.mapper.StudentMapper">
<select id="getStudents" resultMap="StudentTeacher">
select * from student
</select>
<resultMap id="StudentTeacher" type="Student">
<!-- 对象:association 集合:collection -->
<!--association关联属性 property属性名 javaType属性类型 column在多的一方的表中的列名-->
<association property="teacher" column="tid" javaType="Teacher" select="getTeacherById"/>
</resultMap>
<select id="getTeacherById" resultType="Teacher">
select * from teacher where id = #{id}
</select>
</mapper>
StudentMapperTest.java
@Test
public void testGetStudents() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.getStudents();
for (Student student : students) {
System.out.println(student);
}
sqlSession.commit();
sqlSession.close();
}
按结果嵌套处理
StudentMapper.java
List<Student> getStudents2();
StudentMapper.xml
<select id="getStudents2" resultMap="StudentTeacher2" >
select s.id sid, s.name sname , t.name tname
from student s,teacher t
where s.tid = t.id
</select>
<resultMap id="StudentTeacher2" type="Student">
<id property="id" column="sid"/>
<result property="name" column="sname"/>
<!--关联对象property 关联对象在Student实体类中的属性-->
<association property="teacher" javaType="Teacher">
<result property="name" column="tname"/>
</association>
</resultMap>
StudentMapperTest.java
@Test
public void testGetStudents2() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.getStudents2();
for (Student student : students) {
System.out.println(
"学生名:" + student.getName()
+ "\t老师:" + student.getTeacher().getName());
}
}
按照查询进行嵌套处理就像SQL中的子查询
按照结果进行嵌套处理就像SQL中的联表查询
一对多的处理
修改实体类
@Data
public class Teacher {
private int id;
private String name;
private List<Student> studentList;
}
@Data
public class Student {
private int id;
private String name;
private Teacher teacher;
}
按照结果嵌套处理
TeacherMapper.java
Teacher getTeacherById(@Param("tId") int id);
TeacherMapper.xml
<select id="getTeacherById" resultMap="TeacherStudent">
select s.id sId, s.name sName, t.name tName, t.id tId
from student s, teacher t
where s.tId = t.id and t.id = #{tId}
</select>
<resultMap id="TeacherStudent" type="Teacher">
<result property="id" column="tId"/>
<result property="name" column="tName"/>
<!-- 集合:collection
javaType="" 指定属性的类型
集合中的泛型信息,使用 ofType 获取
-->
<collection property="studentList" ofType="Student">
<result property="id" column="sId"/>
<result property="name" column="sName"/>
<result property="tId" column="tId"/>
</collection>
</resultMap>
TeacherMapperTest.java
@Test
public void testGetTeacherById() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacherById(1);
System.out.println(teacher);
sqlSession.commit();
sqlSession.close();
}
按照查询嵌套处理
TeacherMapper.java
Teacher getTeacherById2(@Param("tId") int id);
TeacherMapper.xml
<select id="getTeacherById2" resultMap="TeacherStudent2">
select * from teacher where id = #{tId}
</select>
<resultMap id="TeacherStudent2" type="Teacher">
<collection property="studentList" ofType="Student" select="getStudentByTeacher" column="id"/>
</resultMap>
<select id="getStudentByTeacher" resultType="Student">
select * from student where tId = #{tId}
</select>
TeacherMapperTest.java
@Test
public void testGetTeacherById2() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacherById2(1);
System.out.println(teacher);
sqlSession.commit();
sqlSession.close();
}
关联 - association 多对一
集合 - collection 一对多
javaType 用来指定实体类中属性的类型
ofType 用来指定映射到 List 或者集合中的 POJO 类型,泛型中的约束类型
动态 SQL
动态 SQL:指根据不同的条件生成不同的 SQL 语句
if
*Mapper.xml
<select id="queryBlogIf" parameterType="map" resultType="Blog">
select * from blog
<where>
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</where>
</select>
choose
*Mapper.xml
<select id="queryBlogChoose" resultType="Blog">
select * from blog
<where>
<choose>
<when test="title != null">
title = #{title}
</when>
<when test="author != null">
and title = #{author}
</when>
<otherwise>
and views = #{views}
</otherwise>
</choose>
</where>
</select>
第一个之后的 when, otherwise 标签中的语句前不加 and 会报错
set
BlogMapper.xml
<update id="updateBlog" parameterType="Blog">
update blog
<set>
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author}
</if>
</set>
where id = #{id}
</update>
if 标签中语句末不加逗号会报错
SQL 片段
有时候可能某个 sql 语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。
提取 SQL 片段:
<sql id="if-title-author">
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</sql>
引用 SQL 片段:
<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from blog
<where>
<include refid="if-title-author"/>
</where>
</select>
Foreach
将数据库中前三个数据的 id 修改为 1,2,3;
需求:查询 Blog 表中 id 分别为 1,2,3 的博客信息
BlogMapper.java
List<Blog> queryBlogForeach(Map map);
*Mapper.xml
<select id="queryBlogForeach" parameterType="map" resultType="blog">
select * from blog
<where>
<!--
collection:指定输入对象中的集合属性
item:每次遍历生成的对象
open:开始遍历时的拼接字符串
close:结束时拼接的字符串
separator:遍历对象之间需要拼接的字符串
select * from blog where 1=1 and (id=1 or id=2 or id=3)
-->
<foreach collection="ids" item="id" open="and (" close=")" separator="or">
id = #{id}
</foreach>
</where>
</select>
BlogMapperTest.java
@Test
public void testQueryBlogForeach() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
HashMap map = new HashMap();
List<String> ids = new ArrayList<>();
ids.add("1");
ids.add("2");
ids.add("3");
map.put("ids", ids);
List<Blog> blogs = mapper.queryBlogForeach(map);
System.out.println(blogs);
sqlSession.commit();
sqlSession.close();
}
缓存
Mybatis 缓存
MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启(SqlSession 级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,他是基于 namespace 级别的缓存
- 为了提高扩展性,MyBatis 定义了缓存接口 Cache。我们可以通过实现 Cache 接口来自定义二级缓存
一级缓存
一级缓存也叫本地缓存:
- 与数据库同一次会话期间查询到的数据会放在本地缓存中。
- 以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库;
测试
1、在 Mybatis 中加入日志,方便测试结果
2、编写接口方法
//根据id查询用户
User queryUserById(@Param("id") int id);
3、接口对应的Mapper文件
<select id="queryUserById" resultType="user">
select * from user where id = #{id}
</select>
4、测试
@Test
public void testQueryUserById() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.queryUserById(1);
System.out.println(user);
User user2 = mapper.queryUserById(1);
System.out.println(user2);
System.out.println(user == user2);
session.close();
}
5、结果分析
一级缓存失效的四种情况
一级缓存是 SqlSession 级别的缓存,是一直开启的,我们关闭不了它;
一级缓存失效情况:没有使用到当前的一级缓存,效果就是,还需要再向数据库中发起一次查询请求!
1、sqlSession不同
@Test
public void testQueryUserById(){
SqlSession session = MybatisUtils.getSession();
SqlSession session2 = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user = mapper.queryUserById(1);
System.out.println(user);
User user2 = mapper2.queryUserById(1);
System.out.println(user2);
System.out.println(user==user2);
session.close();
session2.close();
}
观察结果:发现发送了两条 SQL 语句!
结论:每个 sqlSession 中的缓存相互独立
2、sqlSession 相同,查询条件不同
@Test
public void testQueryUserById(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
UserMapper mapper2 = session.getMapper(UserMapper.class);
User user = mapper.queryUserById(1);
System.out.println(user);
User user2 = mapper2.queryUserById(2);
System.out.println(user2);
System.out.println(user==user2);
session.close();
}
观察结果:发现发送了两条 SQL 语句!很正常的理解
结论:当前缓存中,不存在这个数据
3、sqlSession 相同,两次查询之间执行了增删改操作!
增加方法
//修改用户
int updateUser(Map map);
编写SQL
<update id="updateUser" parameterType="map">
update user set name = #{name} where id = #{id}
</update>
测试
@Test
public void testQueryUserById(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.queryUserById(1);
System.out.println(user);
HashMap map = new HashMap();
map.put("name","kuangshen");
map.put("id",4);
mapper.updateUser(map);
User user2 = mapper.queryUserById(1);
System.out.println(user2);
System.out.println(user==user2);
session.close();
}
观察结果:查询在中间执行了增删改操作后,重新执行了
结论:因为增删改操作可能会对当前数据产生影响
4、sqlSession 相同,手动清除一级缓存
@Test
public void testQueryUserById(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.queryUserById(1);
System.out.println(user);
session.clearCache();//手动清除缓存
User user2 = mapper.queryUserById(1);
System.out.println(user2);
System.out.println(user==user2);
session.close();
}
一级缓存就是一个 map
二级缓存
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
基于 namespace 级别的缓存,一个名称空间,对应一个二级缓存;
工作机制
- 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
- 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
- 新的会话查询信息,就可以从二级缓存中获取内容;
- 不同的mapper查出的数据会放在自己对应的缓存(map)中;
使用步骤
1、开启全局缓存 mybatis-config.xml
<setting name="cacheEnabled" value="true"/>
2、去每个mapper.xml中配置使用二级缓存,这个配置非常简单;*Mapper.xml
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<!-- 这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。 -->
3、代码测试
- 所有的实体类先实现序列化接口
- 测试代码
@Test
public void testQueryUserById(){
SqlSession session = MybatisUtils.getSession();
SqlSession session2 = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user = mapper.queryUserById(1);
System.out.println(user);
session.close();
User user2 = mapper2.queryUserById(1);
System.out.println(user2);
System.out.println(user==user2);
session2.close();
}
结论
- 只要开启了二级缓存,我们在同一个Mapper中的查询,可以在二级缓存中拿到数据
- 查出的数据都会被默认先放在一级缓存中
- 只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中
缓存原理图

EhCache
第三方缓存实现–EhCache: 查看百度百科
Ehcache是一种广泛使用的java分布式缓存,用于通用缓存;
要在应用程序中使用Ehcache,需要引入依赖的jar包
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.1.0</version>
</dependency>
在mapper.xml中使用对应的缓存即可
<mapper namespace = “org.acme.FooMapper” >
<cache type = “org.mybatis.caches.ehcache.EhcacheCache” />
</mapper>
编写 ehcache.xml
文件,如果在加载时未找到 /ehcache.xml 资源或出现问题,则将使用默认配置。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--
diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
user.home – 用户主目录
user.dir – 用户当前工作目录
java.io.tmpdir – 默认临时文件路径
-->
<diskStore path="./tmpdir/Tmp_EhCache"/>
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="cloud_user"
eternal="false"
maxElementsInMemory="5000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="1800"
memoryStoreEvictionPolicy="LRU"/>
<!--
defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
-->
<!--
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
</ehcache>
Tips
1) Mybatis-Plus 查询 Mysql datetime 类型时,相差8小时
解决方案:https://blog.csdn.net/a549654065/article/details/88664077