前言
由于使用jdbc
的方式去操作数据库的复杂,所以诞生了Mybatis
这款框架,简化了很多连接、结果集映射等操作,可以使得开发者更加专注于SQL
的编写,同时Mybatis
有很多设计提升了性能,关键的点应该是延迟加载和二级缓存,更多官方的优点和学习使用可以参考官方文档:https://mybatis.org/mybatis-3/zh_CN/getting-started.html
springboot
针对mybatis
有很多的集成,但是学习时,并没有使用springboot
的自动配置,需要从更详细的配置了解起。
创建第一个mybatis
程序
-
添加相关依赖
<dependencies> <!--mysql驱动连接--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.14</version> </dependency> <!--单元测试,方便测试--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies>
-
编写
mybatis-config.xml
配置类配置类名称从官方文档中确定为
mybatis-config.xml
,放置在resources
下<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--配置数据库环境--> <environments default="development"> <environment id="development"> <!--事务配置--> <transactionManager type="JDBC"/> <!--数据源连接配置--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!--指定mapper对于的xml位置--> <mappers> <mapper resource="my/mapper/UserMapper.xml"/> </mappers> </configuration>
-
编写获取
SqlSession
的工具类public class MybatisUtils { private static SqlSessionFactory sqlSessionFactory; /** * 为什么使用静态代码块? * 静态代码块会在项目启动时就加载 */ static { try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } /** * 获取SqlSession * * @return */ public static SqlSession getSqlSession() { return sqlSessionFactory.openSession(); } }
-
创建实体类、表、接口、
xml
文件-
实体类
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="my.mapper.UserMapper"> <select id="getAllUsers" resultType="my.domain.User"> select * from user </select> </mapper>
-
mapper
/** * @author songj */ public interface UserMapper { /** * 获取所有用户 * * @return */ List<User> getAllUsers(); }
-
xml配置:
其中的
namespace
对应mapper
的路径<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="my.mapper.UserMapper"> <select id="getAllUsers" resultType="my.domain.User"> select * from user </select> </mapper>
-
-
避免资源导出失败的问题
之前学习
maven
时,就遇到<resources>
标签,当时的作用是 配置文件映射pom.xml
的自定义属性,现在的作用是,一些存放在路径下的资源在打包运行时,没有生成到target
文件夹下,在运行时找不到对应文件,这时候需要配置以下内容解决<build> <!--防止资源导出失败的问题--> <resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> </resource> </resources> </build>
-
测试
从上面的理解可以知道
SqlSession
是每次请求就会产生的public class UserMapperTest { @Test public void test01() { SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> allUsers = mapper.getAllUsers(); for (User allUser : allUsers) { System.out.println(allUser.toString()); } } }
CRUD
- select
- insert
- update
- delete
- 万能map
- 模糊查询
配置详解
mybatis-config.xml
下有很多的配置选项,其中有很多需要重点学习的,必要时需要结合官方文档学习
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)
environments(环境配置)
最核心的配置,可以指定多个不同的数据源,每个数据源都应该有对应的SqlSessionFactory
有多个需要注意点:
- 事务的配置,一般默认是JDBC的配置,但是也有其他的
dataSource
选择POOLED
,其使用了连接池,能更快的获取连接,并保证连接资源不会被浪费
官方文档的提示:
- 默认使用的环境 ID(比如:default=“development”)。
- 每个 environment 元素定义的环境 ID(比如:id=“development”)。
- 事务管理器的配置(比如:type=“JDBC”)。
- 数据源的配置(比如:type=“POOLED”)。
<!--配置数据库环境-->
<environments default="development">
<!--开发环境-->
<environment id="development">
<!--事务配置-->
<transactionManager type="JDBC"/>
<!--数据源连接配置-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<!--测试环境-->
<environment id="test">
<!--事务配置-->
<transactionManager type="JDBC"/>
<!--数据源连接配置-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
properties(属性)
类似于学习mvn
的自定义属性,它能够使mybatis-config.xml
动态的加载项目中的一些配置文件,比如读取db.properties
来配置多个数据源
resources
下的配置文件db.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=123456
mybatis-config.xml
的properties
配置
<properties resource="db.properties"/>
<!--配置数据库环境-->
<environments default="development">
<!--开发环境-->
<environment id="development">
<!--事务配置-->
<transactionManager type="JDBC"/>
<!--数据源连接配置-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<!--测试环境-->
<environment id="test">
<!--事务配置-->
<transactionManager type="JDBC"/>
<!--数据源连接配置-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
mapper(映射器)
当编写了xxMapper.xml
文件时,需要和对应的mapper
接口做映射,并在mybatis-config.xml
配置文件中做配置,否则是不能获取到对应接口的。常见写法有很多,但是常用的就是resource
的较多
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
typeAliases(类型别名)
该配置主要是为了简化编写xml时,使用全路径的实体类,减少了代码,个人感觉比较鸡肋。实际开发还是写全路径的较多
<typeAliases>
<typeAlias type="my.domain.User" alias="user"/>
</typeAliases>
此时编写时就可以简化
<update id="updateUser" parameterType="user">
update user set name = #{name}, age = #{age} where id = #{id}
</update>
还可以使用@Alias(“user”)为实体类取别名,了解即可
settings(设置)
一些关于mybatis
的详细配置
生命周期和作用域(重点理解)
关于mybatis
的几个核心的概念:
-
SqlSessionFactoryBuilder
:这个类可以被实例化、使用和丢弃,一旦创建了
SqlSessionFactory
,就不再需要它了。 因此SqlSessionFactoryBuilder
实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用SqlSessionFactoryBuilder
来创建多个SqlSessionFactory
实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。 -
SqlSessionFactory
SqlSessionFactory
一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用SqlSessionFactory
的最佳实践是在应用运行期间不要重复创建多次,多次重建SqlSessionFactory
被视为一种代码“坏习惯”。因此SqlSessionFactory
的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。 -
SqlSession
每个线程都应该有它自己的
SqlSession
实例。SqlSession
的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将SqlSession
实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将SqlSession
实例的引用放在任何类型的托管作用域中,比如Servlet
框架中的HttpSession
。 如果你现在正在使用一种 Web 框架,考虑将SqlSession
放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个SqlSession
,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。
ps:由上面的这几个官方文档的定义可以了解到
SqlSessionFactoryBuilder
(单个,创建完就销毁)–>SqlSessionFactory
(单个,伴随程序的整个生命周期) -->SqlSession
(多个,每次此请求就会创建,在执行结束后要销毁SqlSession
)
ResultMap
结果集映射(重点)
resultMap算是mybatis中比较核心的内容了,在xml中设置返回结果的映射,有基础的用法,也有高级的用法
-
数据库列名和实体类的属性名称对不上
其实可以使用给字段取别名的方式,但是更加高级点的,就可以使用
resultMap
做相应的映射<resultMap id="userResultMap" type="my.domain.User"> <result column="name" property="name"/> <result column="age" property="old"/> </resultMap> <select id="getUsersByResultMap" resultMap="userResultMap"> select id,name,age from user </select>
-
高级映射
日志工厂
上面提到了在mybatis-config.xml
中有很多的配置,其中有一项是关于mybatis的日志选择的
重点讲两种配置STDOUT_LOGGING
和log4j
-
STDOUT_LOGGING
:mybatis-config.xml
中的配置<settings> <!--配置日志 stdout_logging--> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>
控制台会打印出
sql
执行的详细信息Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter. PooledDataSource forcefully closed/removed all connections. PooledDataSource forcefully closed/removed all connections. PooledDataSource forcefully closed/removed all connections. PooledDataSource forcefully closed/removed all connections. Opening JDBC Connection Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary. Created connection 752684363. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cdd0d4b] ==> Preparing: select id,name,age from user ==> Parameters: <== Columns: id, name, age <== Row: 2, kkk, 19 <== Row: 3, 王五, 32 <== Row: 4, 赵六, 21 <== Row: 5, 777, 22 <== Total: 4 User{id=2, name='kkk', age='19'} User{id=3, name='王五', age='32'} User{id=4, name='赵六', age='21'} User{id=5, name='777', age='22'} Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cdd0d4b] Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cdd0d4b] Returned connection 752684363 to pool.
-
log4j
是一款常用的日志框架,不仅仅mybatis
可以配置使用log4j
,在项目开发中也需要使用到log4j
不同于
STDOUT_LOGGING
可以直接使用,在使用log4j
时需要导入依赖和相应的配置-
导入依赖
<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
-
导入配置
是
log4j
的配置,有很多详细的配置需要了解### 设置### log4j.rootLogger = debug,stdout,D,E ### 输出信息到控制抬 ### log4j.appender.stdout = org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target = System.out log4j.appender.stdout.layout = org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n ### 输出DEBUG 级别以上的日志到=/home/duqi/logs/debug.log ### log4j.appender.D = org.apache.log4j.DailyRollingFileAppender log4j.appender.D.File = /home/duqi/logs/debug.log log4j.appender.D.Append = true log4j.appender.D.Threshold = DEBUG log4j.appender.D.layout = org.apache.log4j.PatternLayout log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n ### 输出ERROR 级别以上的日志到=/home/admin/logs/error.log ### log4j.appender.E = org.apache.log4j.DailyRollingFileAppender log4j.appender.E.File =/home/admin/logs/error.log log4j.appender.E.Append = true log4j.appender.E.Threshold = ERROR log4j.appender.E.layout = org.apache.log4j.PatternLayout log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
-
mybatis-config.xml
的配置<settings> <!--配置日志--> <setting name="logImpl" value="LOG4J"/> </settings>
-
简单使用
public class Log4jTest { static Logger logger = Logger.getLogger(Log4jTest.class); @Test public void logTest() { logger.info("这是一条info信息"); logger.debug("这是一条debug信息"); logger.error("这是一条error信息"); } }
-
分页
从学习mysql到mybatis等,多次涉及到了分页的查询,主要的功能其实也是减小单次查询数据库的压力,有以下几类实现方式:
-
使用sql的方式实现(mysql可用)
select * from table limit startIndex,pageSize; select * from table limtt pageSize offset startIndex;
-
使用RowBounds分页(仅限于mybatis的SqlSession下可用,不建议使用)
-
使用基于mybatis封装的插件(pageHelper),企业开发中经常使用到,后续可以自行练习
关于pageSize和pageNo的理解:
提到分页肯定离不开这两个参数,由limit的语法,其实可以理解到该怎么设置参数的值
startIndex = (pageNo - 1) * pageSize
ps:上面的几种都可以使用,但是最原生的,也是其他插件的基础,就是使用sql,不论插件再更新,写的再厉害,也离不开limit
使用注解开发
发现任何框架最终都会使用到注解和反射来简化开发,mybatis
也是这样,注解能够简化简单的sql语句编写,不必创建xml文件,但是相对比较复杂的sql还是应该使用xml,同时xml能够降低耦合,并且能够使用resultMap
进行结果集的映射。虽然不建议使用注解开发,但是还是需要学习该种写法,并且还是能够简化xml的编写
-
mybatis-config.xml配置文件指定接口
<!--指定mapper对于的xml位置--> <mappers> <!--指定xml文件--> <mapper resource="my/mapper/UserMapper.xml"/> <!--指定mapper文件,使用注解进行开发--> <mapper class="my.mapper.UserByAnnotationsMapper"/> </mappers>
-
设置自动提交事务
之前对于增删改都需要手动提交,这明显是不方便的,所以在创建SqlSession时,可以选择自动提交事务,这样在完成增删改后,就会自动提交事务。
// SqlSession openSession(boolean autoCommit); sqlSessionFactory.openSession(true);
-
编写接口
-
select
@Select("select * from user") List<User> getAllUser();
-
insert
@Insert("insert into user (name,age) values (#{name},#{old})") int addUser(User user);
-
update
-
delete
-
@Param
注解的使用
给基本数据类型和String重命名使用的,当多多个参数,需要使用@param指明传入sql的变量名称
mapper中:
public User selectUser(@Param("userName") String name,@Param("password") String pwd);
xml中:
<select id="selectUser" resultMap="User">
select * from user where user_name = #{userName} and user_password=#{password}
</select>
ResultMap
高级映射(重点)
之前针对resultmap的映射的使用是很简单的:实体类属性和表字段不同。但是在实际开发中有更复杂的映射关系
多对一
简单的例子就是学生和班级的关系:
对于班级来讲:是一对多的关系
对于学生来讲:是多对一的关系
-
创建表
-- 学生表 CREATE TABLE `student` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `c_id` int DEFAULT NULL COMMENT '所属班级id', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; -- 班级表 CREATE TABLE `class` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '班级名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-
创建实体类
student:
public class Student { private int id; private String name; // 学生所属班级的类 private MyClass aClass; }
class:
public class MyClass { private int id; private String name; }
ps:简化了get和set方法,以及构造方法
-
查询学生和对应班级的信息
-
使用子查询
通过在
<association>
标签中写select
属性即可嵌套子查询,关于column
可以是多个条件的,参考官方文档<resultMap id="stuClass" type="my.domain.Student"> <result property="id" column="id"/> <result property="name" column="name"/> <!--当映射是一个实现类时,使用association--> <association property="cid" column="cid" javaType="my.domain.MyClass" select="getClassById" /> </resultMap> <select id="getAllStudents" resultMap="stuClass"> select * from student </select> <!--根据id查询班级--> <select id="getClassById" resultType="my.domain.MyClass"> select * from class where id = #{id} </select>
-
使用连表查询
与上面的区别是,在写sql时就使用了连表查询,将属性中的实体类的值已经查出来了,只需要做相应的映射。也使用
<association>
指明实体类的映射关系<resultMap id="stuClass2" type="my.domain.Student"> <result property="id" column="sid"/> <result property="name" column="sname"/> <association property="cid" column="cid" javaType="my.domain.MyClass"> <result property="name" column="cname"/> </association> </resultMap> <select id="getAllStudents2" resultMap="stuClass2"> select t1.id as sid ,t1.name as sname,t2.name as cname from student as t1, class as t2 where t1.cid = t2.id </select>
-
注意点:
- 在创建实体类时,一定要写上无参构造方法,否则在实体类中包含其他实体类时,就会发生异常:argument type mismatch
- 子查询的方式的性能是更差的,但是后续会有延迟加载来优化
一对多
-
创建实体类
student:
public class Student { private int id; private String name; private int cId; }
class:
public class MyClass { private int id; private String name; private List<Student> students; }
ps:与上面的多对一不同,是以班级的角度,要查询班级下的所有学生的信息,所以使用了list
-
查询班级下的所有学生
-
嵌套子查询
<select id="getStudentByClass" resultMap="stuByClass"> select * from class where id = #{id} </select> <select id="getStudents" resultType="my.domain.Student"> select * from student where cid = #{id} </select> <resultMap id="stuByClass" type="my.domain.MyClass"> <id property="id" column="id"/> <result property="name" column="name"/> <!-- column指班级id将作为查询条件给select中,javaType指外层集合的类型,ofType指集合内部元素的类型 --> <collection property="students" column="id" javaType="ArrayList" ofType="my.domain.Student" select="getStudents"/> </resultMap>
-
使用连表查询
<select id="getStudentByClass2" resultMap="stuByClass2"> SELECT s.id sid, s.NAME sname, s.cid scid, c.id cid, c.NAME cname FROM student AS s, class AS c WHERE s.cid = c.id AND c.id = #{id} </select> <resultMap id="stuByClass2" type="my.domain.MyClass"> <id property="id" column="cid"/> <result property="name" column="cname"/> <!-- 指定属性值和集合内部的元素类型即可 --> <collection property="students" ofType="my.domain.Student"> <id property="id" column="sid"/> <result property="name" column="sname"/> <result property="cId" column="scid"/> </collection> </resultMap>
-
动态sql
这部分内容,在之前的开发中经常用到,简单来说就是mybatis通过标签的方式进行条件判断、循环等拼接sql语句,支持动态的构建sql语句,具体的使用方式可以参考官方文档,它写的很详细了
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
缓存(重点)
一级缓存
是mybatis
默认开启的缓存,是不可关闭的缓存,一级缓存是SqlSession
级别的缓存。由上面的流程可以知道,用户的每次连接请求都会创建SqlSession
会话。那么在从创建到关闭SqlSession
的这个过程中,重复查询一个SQL
时,第一次查询结果就会保存在一级缓存中,第二次查询就会从一级缓存中获取了,不会再查询数据库了。
什么时候缓存失效?
在两次查询的过程中发生增删改操作时,就会让一级缓存失效
案例:
-
不执行增删改:
@Test public void TestSqlSessionCache() { // 测试一级缓存 SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 第一次获取user User user1 = mapper.getUserById(2); System.out.println(user1); // 第二次获取user User user2 = mapper.getUserById(2); System.out.println(user2); System.out.println(user1 == user2); sqlSession.close(); }
结果:
由结果可以看到,两次查询只执行了一次
sql
,并且两次查询出来的对象相等,说明第二次查询是从一级缓存中获取的。[DEBUG] 2024-08-14 20:36:02,485 method:org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:454) Created connection 198499365. [DEBUG] 2024-08-14 20:36:02,495 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Preparing: select * from user where id = ? [DEBUG] 2024-08-14 20:36:02,541 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Parameters: 2(Integer) [DEBUG] 2024-08-14 20:36:02,586 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) <== Total: 1 User{id=2, name='kkk', age='19'} User{id=2, name='kkk', age='19'} true [DEBUG] 2024-08-14 20:36:02,590 method:org.apache.ibatis.transaction.jdbc.JdbcTransaction.close(JdbcTransaction.java:97) Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@bd4dc25] [DEBUG] 2024-08-14 20:36:02,590 method:org.apache.ibatis.datasource.pooled.PooledDataSource.pushConnection(PooledDataSource.java:409) Returned connection 198499365 to pool.
-
执行增删改,导致一级缓存失效
@Test public void TestSqlSessionCache() { // 测试一级缓存 SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 第一次获取user User user1 = mapper.getUserById(2); System.out.println(user1); // 执行修改 mapper.updateUser(new User(2, "xxx", "33")); // 第二次获取user User user2 = mapper.getUserById(2); System.out.println(user2); System.out.println(user1 == user2); sqlSession.close(); }
结果:执行两次查询
user
的sql
,并且两次结果不相等,说明增删改导致了一级缓存失效[DEBUG] 2024-08-14 20:40:35,653 method:org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:454) Created connection 198499365. [DEBUG] 2024-08-14 20:40:35,657 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Preparing: select * from user where id = ? [DEBUG] 2024-08-14 20:40:35,688 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Parameters: 2(Integer) [DEBUG] 2024-08-14 20:40:35,718 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) <== Total: 1 User{id=2, name='kkk', age='19'} [DEBUG] 2024-08-14 20:40:35,721 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Preparing: update user set name = ?, age = ? where id = ? [DEBUG] 2024-08-14 20:40:35,721 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Parameters: xxx(String), 33(String), 2(Integer) [DEBUG] 2024-08-14 20:40:35,841 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) <== Updates: 1 [DEBUG] 2024-08-14 20:40:35,842 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Preparing: select * from user where id = ? [DEBUG] 2024-08-14 20:40:35,842 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Parameters: 2(Integer) [DEBUG] 2024-08-14 20:40:35,845 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) <== Total: 1 User{id=2, name='xxx', age='33'} false
二级缓存
由上面的流程可以知道SqlSession
可以获取多个mapper。所有二级缓存是mapper
级别的。一级缓存会在会话结束后销毁,当开启二级缓存后,一级缓存销毁前会将数据保存在二级缓存中。一级缓存是默认开启的,而二级缓存需要手动配置。
-
开启二级缓存配置
<settings> <!--开启二级缓存--> <setting name="cacheEnabled" value="true"/> </settings>
-
指定mapper开启二级缓存
<mapper namespace="my.mapper.UserMapper"> <cache/> <!--userCache 可以关闭这个接口的缓存--> <select id="getUserById" parameterType="int" resultType="my.domain.User" useCache="true"> select * from user where id = #{id} </select> </mapper>
-
测试
@Test public void TestSqlSessionCache() { // 测试二级缓存 // 第一次获取user SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user1 = mapper.getUserById(2); System.out.println(user1); sqlSession.close(); // 新建会话获取user SqlSession sqlSession2 = MybatisUtils.getSqlSession(); UserMapper mapper2= sqlSession2.getMapper(UserMapper.class); User user2 = mapper2.getUserById(2); System.out.println(user2); sqlSession2.close(); System.out.println(user1 == user2); }
结果:只执行了一次
sql
,所以数据保存在了二级缓存中[DEBUG] 2024-08-14 20:57:11,022 method:org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:454) Created connection 550302731. [DEBUG] 2024-08-14 20:57:11,031 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Preparing: select * from user where id = ? [DEBUG] 2024-08-14 20:57:11,070 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) ==> Parameters: 2(Integer) [DEBUG] 2024-08-14 20:57:11,105 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:135) <== Total: 1 User{id=2, name='xxx', age='33'} [DEBUG] 2024-08-14 20:57:11,112 method:org.apache.ibatis.transaction.jdbc.JdbcTransaction.close(JdbcTransaction.java:97) Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@20ccf40b] [DEBUG] 2024-08-14 20:57:11,113 method:org.apache.ibatis.datasource.pooled.PooledDataSource.pushConnection(PooledDataSource.java:409) Returned connection 550302731 to pool. [WARN ] 2024-08-14 20:57:11,114 method:org.apache.ibatis.io.SerialFilterChecker.check(SerialFilterChecker.java:45) As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66 [DEBUG] 2024-08-14 20:57:11,118 method:org.apache.ibatis.cache.decorators.LoggingCache.getObject(LoggingCache.java:60) Cache Hit Ratio [my.mapper.UserMapper]: 0.5 User{id=2, name='xxx', age='33'} false
ps:有很多相关配置,参考文档
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
缓存执行流程
先获取二级缓存,再获取一级缓存,最后获取数据库
指定Ehcache缓存(了解)
延迟加载(重点)
之前学习resultmap
的高级映射时,当使用嵌套子查询的方法查询类对象属性时,就会有性能的问题。所以mybatis
做了这方面的优化,初次查询不会加载该类,当涉及到时才会查询,减少了数据库的连接次数
- 开启配置
<settings>
<!--开启全局延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当延迟加载启用时,指定当触发加载时是否执行目标对象的初始化 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
-
mapper中指定是否开启延迟加载
关键点在:
fetchType="lazy"
<resultMap id="stuClass" type="my.domain.Student"> <result property="id" column="id"/> <result property="name" column="name"/> <!--当映射是一个实现类时,使用association--> <association property="cid" column="cid" javaType="my.domain.MyClass" select="getClassById" fetchType="lazy"/> </resultMap> <select id="getAllStudents" resultMap="stuClass"> select * from student </select> <!--根据id查询班级--> <select id="getClassById" resultType="my.domain.MyClass"> select * from class where id = #{id} </select>
在上述配置中,我们通过在<association>
标签中设置fetchType="lazy"
来指定关联的MyClass
对象应用延迟加载。当我们查询Student
对象时,MyBatis
不会立即查询MyClass
对象,而是在我们首次访问MyClass
对象的Student
属性时才会查询。
注意:在配置延迟加载时,确保开启了MyBatis
的全局配置中的lazyLoadingEnabled
选项,并且关闭或者正确配置aggressiveLazyLoading
选项,以确保延迟加载机制能够按预期工作。
最后补一个mybatis-config
的配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<settings>
<!--配置日志-->
<setting name="logImpl" value="LOG4J"/>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
<!--开启全局延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当延迟加载启用时,指定当触发加载时是否执行目标对象的初始化 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<typeAliases>
<typeAlias type="my.domain.User" alias="user"/>
</typeAliases>
<!--配置数据库环境-->
<environments default="development">
<!--开发环境-->
<environment id="development">
<!--事务配置-->
<transactionManager type="JDBC"/>
<!--数据源连接配置-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<!--测试环境-->
<environment id="test">
<!--事务配置-->
<transactionManager type="JDBC"/>
<!--数据源连接配置-->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--指定mapper对于的xml位置-->
<mappers>
<!--指定xml文件-->
<mapper resource="my/mapper/UserMapper.xml"/>
<!--指定mapper文件,使用注解进行开发-->
<mapper class="my.mapper.UserByAnnotationsMapper"/>
<mapper resource="my/mapper/StudentMapper.xml"/>
<mapper resource="my/mapper/MyClassMapper.xml"/>
</mappers>
</configuration>