MyBatis
1. MyBatis简介
1.1 什么是MyBatis?
- MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
- MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
- MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
- MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。
- 2013年11月迁移到Github。
1.2 如何获取MyBatis?
-
maven仓库:https://mvnrepository.com/artifact/org.mybatis/mybatis
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency>
-
github :https://github.com/mybatis/mybatis-3
-
中文官方文档:https://mybatis.org/mybatis-3/zh/index.html
1.3 ORM
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。
2. MyBatis入门
2.1 数据准备
- 在数据库中准备我们需要的测试数据,我们使用mysql
CREATE table `student` (
`s_id` int PRIMARY key auto_increment ,
`s_name` VARCHAR(10) ,
`s_pwd` VARCHAR(10)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
insert into `student`(`s_name` ,`s_pwd`) VALUES ('zhangsan','111')
,('zhangsan','222')
,('zhaoliu','333')
,('tianqi','444')
select * from `student`
2.2 新建项目
- 创建maven项目,在pom中引入相关依赖。
<!-- mysql 驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
2.3 核心配置文件
-
在resources目录下创建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> <settings> <!-- 配置数据库字段名跟属性名小驼峰匹配 --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <!-- 环境:可以配置多个,default:指定采用哪个环境 --> <environments default="development"> <environment id="development"> <!-- 事务管理器,JDBC类型的事务管理器 --> <transactionManager type="JDBC" /> <!-- 数据源,池类型的数据源 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/mybatis-db?useUnicode=true&characterEncoding=UTF-8" /> <property name="username" value="root" /> <property name="password" value="root" /> </dataSource> </environment> </environments> <!-- 指向映射的mapper文件路径 --> <mappers> <mapper resource="com/seven/demo/dao/StudentMapper.xml" /> </mappers> </configuration>
2.4 准备JavaBean
-
准备跟数据库对应的JavaBean类,由于上面配置开启了mapUnderscoreToCamelCase的设置,故此处的属性命名可以使用对应的小驼峰命名规则。
package com.seven.demo.bean; public class User { private int sId ; private String sName = "" ; private String sPwd = "" ; public int getsId() { return sId; } public void setsId(int sId) { this.sId = sId; } public String getsName() { return sName; } public void setsName(String sName) { this.sName = sName; } public String getsPwd() { return sPwd; } public void setsPwd(String sPwd) { this.sPwd = sPwd; } @Override public String toString() { return "User [sId=" + sId + ", sName=" + sName + ", sPwd=" + sPwd + "]"; } }
2.5 Mapper接口
-
编写访问数据库的Mapper接口,以前的DAO在mybatis中叫Mapper。
public interface StudentMapper { public List<User> queryAllUser(); public User queryUserById(int userId); public int insertUser(User user); public int updateUser(User user); public int deleteUser(int userId); }
2.6 Mapper配置
-
在mapper接口同目录中创建对应的配置文件。此配置文件需要在核心配置文件注册。
<?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接口的实现 --> <mapper namespace="com.seven.demo.dao.StudentMapper"> <!-- id:对应接口中的方法名 resultType:方法返回的集合中的javabean类型 --> <select id="queryAllUser" resultType="com.seven.demo.bean.User"> select * from student </select> <!-- #{}表示值,跟方法中的形参列表中的名称一致 --> <select id="queryUserById" parameterType="int" resultType="com.seven.demo.bean.User"> select * from student where s_id = #{userId} </select> <insert id="insertUser" parameterType="com.seven.demo.bean.User"> insert into student (s_name ,s_pwd) values(#{sName},#{sPwd}); </insert> <update id="updateUser" parameterType="com.seven.demo.bean.User"> update student set s_name=#{sName} ,s_pwd=#{sPwd} where s_id=#{sId} </update> <delete id="deleteUser" parameterType="int" > delete from student where s_id = #{userId} </delete> </mapper>
tip:
如果是在idea开发中,不会把资源文件打包,执行会报mapper.xml文件找不到。故需在pom文件中进行如下配置
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
2.7 测试类
2.7.1 查询
public static void main(String[] args) throws IOException {
// 获取核心配置文件对应的输入流
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 通过SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 通过SqlSessionFactory对象获取SqlSession对象
try (SqlSession session = sqlSessionFactory.openSession();){
// 方式1(推荐):通过Mapper来使用
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
List<User> ours = studentMapper.queryAllUser();
System.out.println(ours);
User user = studentMapper.queryUserById(2);
System.out.println(user);
// 方式2:通过接口的全限定名来访问
List<User> list = session.selectList("com.seven.demo.dao.StudentMapper.queryAllUser");
System.out.println(list);
User user2 = session.selectOne("com.seven.demo.dao.StudentMapper.queryUserById",1);
System.out.println(user2);
}
}
2.7.2 新增
public static void main(String[] args) throws IOException {
// 获取核心配置文件对应的输入流
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 通过SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 通过SqlSessionFactory对象获取SqlSession对象
try (SqlSession session = sqlSessionFactory.openSession();){
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
User u = new User();
u.setsName("seven");
u.setsPwd("333");
int i = studentMapper.insertUser(u);
if(i>0){
System.out.println("插入成功。");
}
// 增/删/改操作必须要提交事务才能生效
session.commit();
}
}
- tip: 如果通过openSession方法传入参数true的话
sqlSessionFactory.openSession(true)
,则会自动提交事务。
2.7.3 修改
public static void main(String[] args) throws IOException {
// 获取核心配置文件对应的输入流
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 通过SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 通过SqlSessionFactory对象获取SqlSession对象
try (SqlSession session = sqlSessionFactory.openSession();){
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
User u = new User();
u.setsId(2);
u.setsName("seven");
u.setsPwd("333");
int i = studentMapper.updateUser(u);
if(i>0){
System.out.println("修改成功。");
}
// 增/删/改操作必须要提交事务才能生效
session.commit();
}
}
2.7.4 删除
public static void main(String[] args) throws IOException {
// 获取核心配置文件对应的输入流
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 通过SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 通过SqlSessionFactory对象获取SqlSession对象
try (SqlSession session = sqlSessionFactory.openSession();){
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
int i = studentMapper.deleteUser(3);
if(i>0){
System.out.println("删除成功。");
}
// 增/删/改操作必须要提交事务才能生效
session.commit();
}
}
2.8 生命周期跟作用域
生命周期、作用域是至关重要的,因为错误的使用会导致非常严重的并发问题。
-
SqlSessionFactoryBuilder:这个对象的作用是为了得到SqlSessionFactory对象,一旦得到,则此对象不需要了。因此此对象最好用在临时变量。
-
SqlSessionFactory:可以想象成生产SqlSession的工厂,一旦创建就应该在应用的运行期间一直存在,没有任何理由丢弃或者重新创建。因此此对象最佳作用域是应用区域。最简单的就是单例模式。
-
SqlSession:连接到连接池的一个请求。这个实例并不是线程安全的,因此不能被共享,所以它最佳的作用域是请求作用域或者方法作用域。用完之后记得关闭,不然会造成资源的浪费。
3. MyBatis核心配置
3.1 环境配置(environments)
-
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中。例如,开发、测试和生产环境需要有不同的配置。**尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。**为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。
可以接受环境配置的两个方法签名是:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
如果忽略了环境参数,那么将会加载默认环境,如下所示:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);
environments 元素下可以定义多个environment元素,每个environment都可以id命名,而environments 通过default属性值来定默认操作environment的环境。
-
MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):
设置成JDBC表示使用数据源的事务管理,设置成MANAGED使用容器的事务管理。如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置
-
dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。dataSource元素type="[UNPOOLED|POOLED|JNDI]"。分别表示不使用池/使用池/服务器的JNDI服务来获取数据的连接。
3.2 属性(properties)
-
配置文件中dataSource中连接数据库的一些信息我们可以直接设置,也可以通过属性properties设置。
-
新建外部db.properties文件:
driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mybatis-db?useUnicode=true&characterEncoding=UTF-8 username=root password=root
然后在核心配置文件中引入:
<properties resource="db.properties">
<property name="username" value="root"/>
<property name="password" value="root"/>
</properties>
然后在DataSource中引入这些字段即可。
<dataSource type="POOLED">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
此外,我们还可以在创建SqlSessionFactory对象的时候设置属性:
Properties p = new Properties();
p.put("driver", "com.mysql.jdbc.Driver");
p.put("url", "jdbc:mysql://localhost:3306/mybatis-db?useUnicode=true&characterEncoding=UTF-8");
p.put("username", "root");
p.put("password", "root");
SqlSessionFactory sqlSessionFactory = new qlSessionFactoryBuilder().build(inputStream,p);
此时,这3处地方的属性全部会被读取,java中的设置方式优先级>外部properties文件>xml配置文件中的定义。
3.3 设置(settings)
<!-- 配置数据库字段名跟属性名小驼峰匹配 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 设置日志的实现方式 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 二级全局开关是默认开启的,但为了有好的可读性最好也写上 -->
<setting name="cacheEnabled" value="true"/>
<!-- 获取参数映射的时候可以通过#{0} #{1}..来映射,不推荐 -->
<setting name="useActualParamName" value="false" />
<!-- 打开全局关联查询懒加载的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
3.4 类型别名(typeAliases)
-
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。主要通过配置文件中的typeAliases标签来定义。
-
有2种方式:
第1种是直接定义类型别名
<typeAliases>
<!-- 给com.seven.demo.bean.User类型取个别名user,在映射文件中可以直接使用别名 -->
<typeAlias type="com.seven.demo.bean.User" alias="user" />
</typeAliases>
第2种方式是直接扫描某个包,给所有bean类型自动取别名,规则为类名的首字符小写。
<typeAliases>
<!-- 把com.seven.demo.bean包下的所有bean取名别 -->
<package name="com.seven.demo.bean"/>
</typeAliases>
如果这种默认取名规则我们不需要,可以通过注解来DIY:
//给此bean类型取个类型别名hello,在映射文件中可以直接使用此别名
@Alias("hello")
public class User {
......
}
-
mybatis还为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,具体参见官方文档
https://mybatis.org/mybatis-3/zh/configuration.html#properties
3.6 映射器(mappers)
既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:///
形式的 URL),或类名和包名等。
我们主要用3种方式来引入mapper文件:
-
方式1,使用相对于类路径的资源引用
<mappers> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> <mapper resource="org/mybatis/builder/BlogMapper.xml"/> <mapper resource="org/mybatis/builder/PostMapper.xml"/> </mappers>
此种方式配置灵活,没有限制条件,故推荐此种方式。
-
方式2:通过使用映射器接口实现类的完全限定类名。
<mappers> <mapper class="org.mybatis.builder.AuthorMapper"/> <mapper class="org.mybatis.builder.BlogMapper"/> <mapper class="org.mybatis.builder.PostMapper"/> </mappers>
注意:
-
接口跟Mapper配置文件必须同名。
-
接口跟Mapper配置文件必须在同一包下。
-
-
方式3:将包内的映射器接口实现全部注册为映射器。
<mappers> <package name="org.mybatis.builder"/> </mappers>
注意:
- 接口跟Mapper配置文件必须同名。
- 接口跟Mapper配置文件必须在同一包下。
我们可以综合以上几种方式,mapper接口放入java中,mapper.xml放入resources目录中,但是让2者具有相同的包名跟文件名:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jCV44Odk-1623772288899)(asset\image-20200319232714799.png)]
4. XML映射器
MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。在上面的入门中的Mapper配置文件就是对应的一个XML映射器。我们可以把XML映射器看成DAO接口的实现。
- 每一个Mapper.xml匹配一个mapper接口,匹配的地方在namespace属性中。
<!-- namespace:绑定一个dao/mapper中的接口,相当于mapper接口的实现 -->
<mapper namespace="com.blb.seven.dao.StudentMapper">
......
</mapper>
4.1 SQL标签
- mybatis将SQL语句从JAVA代码中剥离出来,放入到mapper.xml映射器中。
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
共有4中SQL标签:insert、update、delete、select。
在SQL标签中有一些重要的属性需要我们来熟练掌握。
-
id :对应的是mapper接口中的方法名,表示实现哪个方法的业务。
-
parameterType:定义调用方法参数类型,如果简单类型或者基本类型可以不用显示给出,mybatis的类型处理器会自动识别。如果参数过多可以放入map中作为参数传递。
如果方法有多个参数的时候,使用@Param注解取名。也可以通过arg0 ……argN 或者 param1 …… paramN来分别表示参数列表的第几个位置的参数。推荐使用@Param。
-
resultType:指定返回的结果类型。
-
resultMap:对外部 resultMap 的命名引用。 resultType 和 resultMap 之间只能同时使用一个。
<!-- #{}表示值,跟方法中的形参列表中的名称一致 ,parameterType参数类型,resultType结果类型-->
<select id="queryUserById" parameterType="int" resultType="com.seven.demo.bean.User">
select * from student where s_id = #{userId}
</select>
4.2 resultMap
resultMap
元素是 MyBatis 中最重要最强大的元素。主要用来把数据库中的字段跟java中的属性按照我们的意图来映射。当我们数据库字段跟属性不一样时我们可以把这种映射关系在resultMap中来手动一一映射。
<!-- 定义一个resultMap标签,把数据库字段跟type对应的类型的属性进行映射 -->
<resultMap type="com.blb.seven.bean.User" id="stuResultMap">
<id property="id" column="s_id" /><!-- 把s_id字段跟id属性进行映射 -->
<result property="sName" column="s_name" /><!-- 把s_name字段跟sName属性进行映射 -->
</resultMap>
<!-- resultMap的值指向上面定义的resultMap标签的id属性值 -->
<select id="queryUserById" resultMap="stuResultMap" >
select * from student where s_id = #{userId}
</select>
- id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能。
5. 注解SQL
5. 1 注解SQL
之前的SQL我们是在mapper.xml文件中来完成映射的,还有另一种方法来完成语句映射。它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如:
@Select(" select * from student where s_id = #{userId} ")
public User queryUserById(int userId);
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
5.2 字符串替换
默认情况下,使用 #{}
参数语法时,MyBatis 会创建 PreparedStatement
参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中直接插入一个不转义的字符串。 比如 ORDER BY 子句,这时候你可以:ORDER BY ${columnName}
.这样,MyBatis 就不会修改或转义该字符串了。
当 SQL 语句中的元数据(如表名或列名)是动态生成的时候,字符串替换将会非常有用。 举个例子,如果你想 select
一个表任意一列的数据时,不需要这样写:
@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);
@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);
@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);
而是可以只写这样一个方法:
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
这种方式也同样适用于替换表名的情况。
用这种方式接受用户的输入,并用作语句参数是不安全的,会导致潜在的 SQL 注入攻击。因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数。
6. 日志
6.1 STDOUT_LOGGING
- 在核心配置文件中增加如下设置选项则可以打开日志选项:
<setting name="logImpl" value="STDOUT_LOGGING"/>
tip: 注意大小写要完全一样,不能有多的空格。
此时我们再运行代码则会有如下效果[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q114P45W-1623772288900)(asset\image-20200317231533992.png)]
6.2 log4j
Log4j是apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX、Syslog、守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
- 在核心配置文件中增加如下设置选项则可以打开日志选项:
<setting name="logImpl" value="LOG4J"/>
- 引入依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- 在资源目录新建log4j的配置文件(log4j.properties)
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file
#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
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/seven.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=error
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
- 手动使用log4j
Logger logger = Logger.getLogger(StudentMapperTest.class);
logger.debug("log4j: debug ... ");
logger.info("log4j: info ... ");
logger.warn("log4j: warn ... ");
logger.error("log4j: error ... ");
7. 分页
7.1 传统方式
-
Mapper接口中添加方法:
public List<User> queryAllUserLimit(Map<String,Object> map);
-
Mapper.xml映射
<resultMap type="com.blb.seven.bean.User" id="stuResultMap"> <id property="id" column="s_id" /> <result property="sName" column="s_name" /> </resultMap> <select id="queryAllUserLimit" parameterType="map" resultMap="stuResultMap"> select * from student limit #{pageStart} , #{pageSize} </select>
由于方法参数有多个,为了方便我们把参数定义成Map。
-
测试
@Test public void testQueryAllUserLimit() throws IOException{ InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session = sqlSessionFactory.openSession();){ StudentMapper studentMapper = session.getMapper(StudentMapper.class); // 将分页需要的2个参数放入map集合中作为参数传入 Map< String , Object> map = new HashMap< String , Object>(); map.put("pageStart", 0); map.put("pageSize", 3); List<User> users = studentMapper.queryAllUserLimit(map); System.out.println(users); } }
7.2 RowBounds方式(不推荐)
-
Mapper接口中添加方法:
public List<User> queryAllUserLimitRowBounds();
-
Mapper.xml映射
<resultMap type="com.blb.seven.bean.User" id="stuResultMap">
<id property="id" column="s_id" />
<result property="sName" column="s_name" />
</resultMap>
<!-- sql中不需要limit部分 -->
<select id="queryAllUserLimitRowBounds" resultMap="stuResultMap">
select * from student
</select>
-
测试
@Test public void testQueryAllUserLimitRowBounds() throws IOException{ InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session = sqlSessionFactory.openSession();){ // 分页参数都封装到RowBounds对象中 RowBounds rowBounds = new RowBounds(0,3); List<User> users = session.selectList("com.blb.seven.dao.StudentMapper.queryAllUserLimitRowBounds", null, rowBounds); System.out.println(users); } }
7.3 分页插件
MyBatis 分页插件 PageHelper
- 官网:https://pagehelper.github.io/
- github: https://github.com/pagehelper/Mybatis-PageHelper
8. 复杂查询
8.1 数据准备
在数据库中准备2张表(学生表跟老师表)跟一些测试数据:
CREATE TABLE `student` (
`s_id` int(11) NOT NULL AUTO_INCREMENT,
`s_name` varchar(10) DEFAULT NULL,
`s_pwd` varchar(10) DEFAULT NULL,
`t_id` int(11) DEFAULT NULL,
PRIMARY KEY (`s_id`),
KEY `t_f` (`t_id`),
CONSTRAINT `t_f` FOREIGN KEY (`t_id`) REFERENCES `teacher` (`t_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES ('1', 'zhangsan', '111', '1');
INSERT INTO `student` VALUES ('2', 'lisi', '222', '2');
INSERT INTO `student` VALUES ('3', 'wangwu', '333', '3');
INSERT INTO `student` VALUES ('4', 'zhaoliu', '444', '1');
INSERT INTO `student` VALUES ('5', 'tianqi', '555', '2');
INSERT INTO `student` VALUES ('6', 'wangba', '666', '3');
-- ----------------------------
-- Table structure for teacher
-- ----------------------------
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`t_id` int(11) NOT NULL AUTO_INCREMENT,
`t_name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`t_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of teacher
-- ----------------------------
INSERT INTO `teacher` VALUES ('1', 'apple');
INSERT INTO `teacher` VALUES ('2', 'seven');
INSERT INTO `teacher` VALUES ('3', 'lili');
8.1 多对一
8.1.1 实体类
public class Student {
private int id ;
private String sName = "" ;
private String sPwd = "" ;
private Teacher teacher ;
//setter and getter
}
public class Teacher {
private int tId ;
private String tName = "" ;
//setter and getter
}
8.1.2 Mapper接口
public interface StudentMapper {
public List<User> queryStudentWithTeacher();
}
8.1.3 Mapper.xml
- 方式1:
<?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.blb.seven.dao.StudentMapper">
<resultMap type="Student" id="StudentTeacher">
<id property="id" column="s_id" />
<result property="sName" column="s_name" />
<result property="sPwd" column="s_pwd" />
<!-- 如果是一个对象用association,集合用collection -->
<association property="teacher" javaType="Teacher" column="{hello_id=t_id}" select="getTeacher" />
</resultMap>
<select id="queryStudentWithTeacher" resultMap="StudentTeacher">
select * from student
</select>
<select id="getTeacher" resultType="Teacher">
select * from teacher where t_id= #{hello_id}
</select>
</mapper>
tip:
-
首先执行queryStudentWithTeacher方法对应的select,结果Map映射为StudentTeacher。只有teacher这个字段需要处理,把查出来的结果t_id字段放入第2个sql中查询,结果封装成Teacher对象再映射。
-
column="{hello_id=t_id}"表示把查询出来的t_id字段值取名为hello_id,在下面的getTeacher的sql中直接引用。由于这里只有一列,可以简写成column=“t_id”,则getTeacher的sql中的参数可所以给名字。
-
如果有多列需要传递column="{arg1=column1,arg2=column2,…}"。
-
方式2:
<?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.blb.seven.dao.StudentMapper">
<resultMap type="Student" id="StudentTeacher">
<!-- 先从结果封装Student的属性 -->
<id property="id" column="s_id" />
<result property="sName" column="s_name" />
<result property="sPwd" column="s_pwd" />
<!-- 再把Teacher的字段取出配置成teacher属性 -->
<association property="teacher" javaType="Teacher" >
<id property="tId" column="t_id" />
<result property="tName" column="t_name" />
</association>
</resultMap>
<select id="queryStudentWithTeacher" resultMap="StudentTeacher" >
select s_id,s_name,s_pwd,teacher.t_id,t_name from student ,teacher where student.t_id = teacher.t_id ;
</select>
</mapper>
8.1.4 测试
@Test
public void testQueryStudents() throws IOException{
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession();){
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
List<Student> user = studentMapper.queryStudentWithTeacher();
System.out.println(user);
}
}
8.2 一对多
- 一个老师对应N个学生,查询某个老师的所有信息,包括所属的学生信息。
8.2.1 实体类
public class Student {
private int id ;
private String sName = "" ;
private String sPwd = "" ;
// 现在是老师对应学生,这个就直接定义简单类型即可
private int tId ;
//setter and getter
}
public class Teacher {
private int tId ;
private String tName = "" ;
private List<Student> students ;
//setter and getter
}
8.2.2 Mapper接口
public interface StudentMapper {
// 查询某一个老师的所有信息以及所属的学生信息
public Teacher queryTeacher(@Param("t_id") int teacherId);
}
8.2.3 Mapper.xml
- 方式1:
<?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.blb.seven.dao.StudentMapper">
<select id="queryTeacher" resultMap="teacherMap" >
select * from teacher where t_id = #{t_id}
</select>
<resultMap type="Teacher" id="teacherMap" >
<id property="tId" column="t_id"/>
<result property="tName" column="t_name"/>
<!-- javaType是定义类型, ofType是定义集合泛型的类型-->
<collection property="students" column="{hello_id=t_id}" javaType="ArrayList" ofType="Student" select="getStudents" />
</resultMap>
<select id="getStudents" resultMap="StudentMap" >
select * from student where t_id= #{hello_id}
</select>
<!-- 这里定义StudentMap主要是因为属性中叫id,字段名叫s_id -->
<resultMap type="Student" id="StudentMap">
<id property="id" column="s_id" />
</resultMap>
</mapper>
- 方式2:
<?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.blb.seven.dao.StudentMapper">
<resultMap type="Teacher" id="teacherMap">
<id property="tId" column="t_id" />
<result property="tName" column="t_name" />
<!-- 定义类型使用javaType,集合泛型中的类型用ofType -->
<collection property="students" ofType="Student" >
<id property="id" column="s_id" />
<result property="tId" column="t_id" />
<result property="sName" column="s_name" />
<result property="sPwd" column="s_pwd" />
</collection>
</resultMap>
<select id="queryTeacher" resultMap="teacherMap">
select s_id,s_name,s_pwd,teacher.t_id,t_name from student ,teacher
where student.t_id = teacher.t_id and teacher.t_id = #{t_id};
</select>
</mapper>
8.2.4 测试
@Test
public void testQueryTeacher() throws IOException{
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession();){
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
Teacher teacher = studentMapper.queryTeacher(2);
System.out.println(teacher);
}
}
8.3 懒加载
当多表进行关联查询的时候比如student表跟teacher表一起全部查出来是比较耗时的,而且当我们需要查看student信息的时候根本不用去查teacher信息。所以此时,我们使用分步查询+懒加载的优势就体现出来了。
懒加载的意思就是按照需要加载。当查询student信息的时候只执行select * from student ,但当我们使用到里面的teacher信息时才去执行select * from teacher where t_id = #{t_id}。当数据量大的时候这种优势就自体现出来了。
懒加载只对关联查询起作用(一对一、一对多、多对多) 。
默认 equals,clone,hashCode,toString 这些方法触发懒加载,可以在 lazyLoadTriggerMethods的设置中配置。
使用步骤:
- 打开全局懒加载开关,则所有的关联查询都会进行懒加载。
<setting name="lazyLoadingEnabled" value="true"/>
- 在分步查询过程的resultMap映射中设置 fetchType=“lazy” ,这是开关会覆盖上面全局的懒加载开关.
<?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.blb.seven.dao.StudentMapper">
<resultMap type="Student" id="studentTeacherMap">
<id property="id" column="s_id"/>
<result property="sName" column="s_name" />
<result property="sPwd" column="s_pwd" />
<association property="teacher" fetchType="lazy" column="{hello_id=t_id}" javaType="Teacher" select="queryTeacherById" />
</resultMap>
<select id="queryStudentWithTeacher" resultMap="studentTeacherMap">
select * from student
</select>
<select id="queryTeacherById" resultType="Teacher">
select * from teacher where t_id = #{hello_id}
</select>
</mapper>
- 测试1
try (SqlSession session = sqlSessionFactory.openSession();) {
StudentMapper studentMapper = session
.getMapper(StudentMapper.class);
List<Student> ours = studentMapper.queryStudentWithTeacher();
// 这里只打印学生的姓名信息,没有用到teacher信息,故不会去加载获取teacher信息的sql
for (Student user : ours) {
System.out.println(user.getsName() );
}
}
- 结果1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gK1t5rkr-1623772288902)(asset\1585128022809.png)]
- 测试2:把懒加载的开关关闭
- 结果2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HD2Se9q-1623772288903)(asset\1585129373815.png)]
9. 动态SQL
- 要执行的SQL是根据参数的情况来动态拼接而成。
9.1 数据准备
CREATE TABLE `book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT,
`book_name` varchar(20) DEFAULT NULL,
`book_price` double DEFAULT NULL,
`book_author` varchar(20) DEFAULT NULL,
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of book
-- ----------------------------
INSERT INTO `book`(book_name,book_price,book_author) VALUES ( 'Java编程思想', '68', 'zhangsan');
INSERT INTO `book`(book_name,book_price,book_author) VALUES ('网页设计', '46', 'lisi');
INSERT INTO `book`(book_name,book_price,book_author) VALUES ('数据库设计', '28', 'wangwu');
9.2 实体类
public class Book {
private Integer bookId = null;
private String bookName = null ;
private Double bookPrice = null;
private String bookAuthor = null ;
// setter and getter
}
9.3 Mapper接口
public interface BookMapper {
public int insertBook(Book book);
public List<Book> queryBookWithLike(Map map);
}
9.4 Mapper.XML
<mapper namespace="com.blb.seven.dao.BookMapper">
<insert id="insertBook" parameterType="Book">
insert into book(book_name,book_price,book_author) VALUES(#{bookName},#{bookPrice},#{bookAuthor})
</insert>
<select id="queryBookWithLike" resultType="Book">
select * from book where
<!-- if条件判断,如果参数nameKeyWord有内容的话,则拼接标签内的SQL -->
<if test="nameKeyWord != null">
book_name like #{nameKeyWord}
</if>
<!-- if条件判断,如果参数authorKeyWord有内容的话,则拼接标签内的SQL -->
<if test="authorKeyWord != null">
and book_author like #{authorKeyWord}
</if>
</select>
</mapper>
- 这里最终的SQL跟条件(authorKeyWord != null)是否成立有关。
9.5 测试
@Test
public void testqueryBookWithLike() throws IOException{
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession();){
BookMapper bookMapper = session.getMapper(BookMapper.class);
// 参数传递
Map<String,Object> map = new HashMap<String,Object>();
map.put("nameKeyWord", "%计%");
map.put("authorKeyWord", "%w%");
List<Book> bookList = bookMapper.queryBookWithLike(map);
for (Book book : bookList) {
System.out.println(book);
}
}
}
通过上面的案例我们可以看到实际上执行的SQL是根据参数的情况而动态生成的,在Mapper.XML进行了非空验证。而这个非空验证是通过类似JSTL的标签来使用的,非常方便好用。
9.6 if
通过条件的是否成立来决定SQL的拼接,通过上面的案例可以看到当test判断结果为true的时候才将标签内的SQL拼接,否则不进行拼接。
<select id="queryBookWithLike" resultType="Book">
select * from book where book_name like #{nameKeyWord}
<!-- if条件判断,如果参数authorKeyWord有内容的话,则拼接标签内的SQL -->
<if test="authorKeyWord != null">
and book_author like #{authorKeyWord}
</if>
</select>
9.7 choose、when、otherwise
<select id="queryBookWithLike" resultType="Book">
select * from book where
<choose>
<when test="nameKeyWord != null">
and book_name like #{nameKeyWord}
</when>
<when test="authorKeyWord != null ">
and book_author like #{authorKeyWord}
</when>
<otherwise>
and book_price > 30
</otherwise>
</choose>
</select>
@Test
public void testqueryBookWithLike() throws IOException{
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession(true);){
BookMapper bookMapper = session.getMapper(BookMapper.class);
Map<String,Object> map = new HashMap<String,Object>();
// map.put("nameKeyWord", "%计%");
map.put("authorKeyWord", "%w%");
List<Book> bookList = bookMapper.queryBookWithLike(map);
for (Book book : bookList) {
System.out.println(book);
}
}
}
这里的choose、when、otherwise中的意义相同。类似java中的 switch case块。
9.8 where
我们来回顾下9.4中的案例:
<select id="queryBookWithLike" resultType="Book">
select * from book where
<!-- if条件判断,如果参数nameKeyWord有内容的话,则拼接标签内的SQL -->
<if test="nameKeyWord != null">
book_name like #{nameKeyWord}
</if>
<!-- if条件判断,如果参数authorKeyWord有内容的话,则拼接标签内的SQL -->
<if test="authorKeyWord != null">
and book_author like #{authorKeyWord}
</if>
</select>
-
在这个案例中,如果nameKeyWord、authorKeyWord都是null的话,则最终的SQL为select * from book where。最后会多出一个where导致结果错误。
-
如果nameKeyWord为null,而authorKeyWord不为null的话最终SQL为select * from book where and book_author like #{authorKeyWord},这里where后面会多出一个and导致SQL错误。
那么如何优化类似这种情况呢?答案是通过我们的where标签。将上面的SQL优化下:
<select id="queryBookWithLike" resultType="Book">
select * from book
<where>
<!-- if条件判断,如果参数nameKeyWord有内容的话,则拼接标签内的SQL -->
<if test="nameKeyWord != null">
book_name like #{nameKeyWord}
</if>
<!-- if条件判断,如果参数authorKeyWord有内容的话,则拼接标签内的SQL -->
<if test="authorKeyWord != null">
and book_author like #{authorKeyWord}
</if>
</where>
</select>
- 在这里我们把SQL中的where改成了标签,这个where标签就能动态帮我们处理上面遇到的问题:where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
9.9 set
我们再来看下修改操作,按照我们之前的写法如下:
<update id="updateBook" parameterType="Book">
update book set
<if test="bookName!=null">book_name = #{bookName}, </if>
<if test="bookPrice!=null">book_price = #{bookPrice}, </if>
<if test="bookAuthor!=null">book_author = #{bookAuthor} </if>
where book_id = #{bookId}
</update>
@Test
public void testUpdateBook() throws IOException{
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession(true);){
BookMapper bookMapper = session.getMapper(BookMapper.class);
// 传入要修改的book对象
Book book = new Book();
book.setBookId(4);
book.setBookName("MYSQL入门到放弃");
book.setBookPrice(55);
book.setBookAuthor("hello");
int i = bookMapper.updateBook(book);
if(i>0){
System.out.println("修改成功。");
}
}
}
这样的话,我们确实可以修改成功。但是修改的字段是由用户决定的, 如果用户修改的字段不包括作者名呢?也就是去掉book.setBookAuthor(“hello”);这句内容。则拼接的SQL为update book set book_name = ?, book_price = ?, where book_id = ?
,在这个SQL中where前会多出一个,号。不同的修改字段的组成会让你对这个,号的处理异常麻烦,这时我们优化如下:
<update id="updateBook" parameterType="Book">
update book
<set>
<if test="bookName!=null">book_name = #{bookName}, </if>
<if test="bookPrice!=null">book_price = #{bookPrice}, </if>
<if test="bookAuthor!=null">book_author = #{bookAuthor} </if>
</set>
where book_id = #{bookId}
这里我们使用了set标签代替了set,这个set标签会自动帮我们处理麻烦的,号。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。
tip:
- 由于我们是根据字段是否为null来拼接SQL,所以Book的bean中的字符串字段不能初始化空串。
- 也不能有基本数据类型,都改成对应的包装类。
9.10 trim
如果上面的where跟set不太符合我们的需求,我们还可以通过trim来DIY。
trim标签有4个重要的属性:
属性 | 描述 |
---|---|
prefix | 给sql语句拼接的前缀 |
suffix | 给sql语句拼接的后缀 |
prefixOverrides | 去除sql语句前面的关键字或者字符,该关键字或者字符由prefixOverrides属性指定 |
suffixOverrides | 去除sql语句后面的关键字或者字符,该关键字或者字符由suffixOverrides属性指定 |
- 和 where 元素等价的自定义 trim 元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
tip:prefixOverrides中的AND后面跟OR后面的空格不要省略。
- 和 set 元素等价的自定义 trim 元素为:
<trim prefix="SET" suffixOverrides=",">
...
</trim>
9.11 foreach
foreach是动态SQL中比较容易出错的标签。主要用来迭代拼接SQL的。 foreach元素的属性主要有item,index,collection,open,separator,close。 分别表示
-
item: 集合中元素迭代时的别名(必选)。
-
index: 在list和数组中,index是元素的序号,在map中,index是元素的key(可选)。
-
open:foreach代码的开始符号,一般是(和close=")"合用。常用在in(),values()时(可选)。
-
close: foreach代码的关闭符号,一般是)和open="("合用。常用在in(),values()时(可选)。
-
separator:元素之间的分隔符,例如在in()的时候,separator=","会自动在元素中间用“,“隔开,避免手动输入逗号导致sql错误,如in(1,2,)这样。该参数可选。
-
collection: 要做foreach的对象。
我们来看一个案例,加入我们想拼接的SQL为select * from student where s_id in ( 1 ,2 ,3 )时,则我们需要迭代拼接的部分为 ( 1 ,2 ,3 ),这部分的开头open是“(”,结尾close是“)”。1 2 3 是迭代的集合,元素之间是用,号进分割separator。故定义如下:
public List<User> queryUserByIds(List<Integer> idList );
<select id="queryUserByIds" resultType="com.blb.seven.bean.User" >
select * from student where s_id in
<foreach collection="list" open="(" close=")" separator="," item="id" index="i">
#{id}
</foreach>
</select>
UserMapper studentMapper = session.getMapper(UserMapper.class);
List<Integer> idList = new ArrayList<Integer>();
idList.add(1);
idList.add(2);
List<User> list = studentMapper.queryUserByIds(idList);
这里需要重点说明下foreach标签中的collection属性值。
-
当参数列表是单参数且为List类型时,又没有通过@param取名时,collection属性值必须为list。因为mybatis会把list封装成一个map集合,key的值固定为“list”。
-
当参数列表是单参数且为数组类型时,又没有通过@param取名时,collection属性值必须为array。因为mybatis会把list封装成一个map集合,key的值固定为“array”。
-
当参数列表封装到map的时候,则collection属性值没有固定值,直接设置map的key即可。
-
如果不想使用mybaits定义的list或者array则需要我们通过@param来手动取名,也推荐这么做。
-
当参数列表不是单参数的时候,必须通过@param来手动取名。
9.12 SQL片段
当我们SQL中有重复的SQL部分又想重复利用的时候我们可以使用SQL片段。如SQL: select s_id , s_name , s_pwd from student 。我们可以拆分成2部分:
<!-- include标签引用下面的定义SQL片段,refid属性值设置为SQL片段的id值 -->
<select id="queryAllUser" resultType="com.blb.seven.bean.User">
select <include refid="col_sql"/> from student
</select>
<!-- sql片段,这部分SQL可以被重复使用-->
<sql id="col_sql">
s_pwd,s_id ,s_name
</sql>
如果我们想引用的是其他Mapper中的SQL片段的话,则只需要在引入的时候把对应的mapper.xml的 namespace带上即可,如:
<select id="queryAllUser" resultType="com.blb.seven.bean.User">
select <include refid="com.blb.seven.dao.UserMapper.col_sql"/> from student
</select>
10.缓存
10.1 缓存简介
10.1.1 什么是缓存?
-
存在内存中的临时数据。
-
将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上查询。从缓存中查找可以提高查询效率,解决高并发系统的性能问题。
10.1.2 为什么使用缓存?
- 减少跟数据库交互的次数,减少系统开销,提高系统效率。
10.1.3 什么样的数据使用缓存?
- 经常查询并且不经常改变的数据。
10.2 Mybatis缓存
-
mybatis包含了一个非常强大的查询缓存特性,它可以方便的定制和配置缓存,缓存可以极大的提高查询效率。
-
mybatis系统默认定义了两级缓存:一级缓存跟二级缓存。
-
默认情况下,只有一级缓存开启。(SQLSession级别的缓存,也称为本地缓存)
-
二级缓存需要手动开启跟配置,是基于namespace级别的缓存。
-
为了提高扩展性,mybaits还定义了缓存接口,我们可以来自定义二级缓存。
-
10.3 一级缓存
- 一级缓存也叫本地缓存:SqlSession
- 在同一个SqlSession会话期间相同的Mapper查询到的数据会放在本地缓存中。
- 以后需要查询相同的数据,直接从缓存中拿,没有必要再去查询数据库。
测试:
-
开启日志。
<setting name="logImpl" value="STDOUT_LOGGING"/>
-
测试在一个Sqlsession中查询2次相同的记录。
// 必须在同一个SqlSession中。 SqlSession session1 = sqlSessionFactory.openSession(); UserMapper studentMapper = session1.getMapper(UserMapper.class); // 第1次查询走数据库 User user1 = studentMapper.queryUserById(1); System.out.println(user1); System.out.println("================================="); // 第2次访问获取同一个对象时直接从缓存中的user1取出来 User user2 = studentMapper.queryUserById(1); System.out.println(user2); // 由于user2是取的缓存中的user1,所以下面user1跟user2的equals的断言成立。 Assert.assertEquals(user1, user2); // 关闭SqlSession session1.close();
-
查看日志输出来判断。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FjI2iwIQ-1623772288904)(asset\1585064851257.png)]
总结:
- 2次必须是查询相同的数据缓存中才会有这个对象。
- 2次的查询必须是同一个mapper,不同的mapper不会使用缓存。
- 如果2次中间去update/insert/delete数据,即使操作的是其它数据,那么缓存就会被清空,第2次自然也是从数据库中取。
- 我们也可以通过sqlSession.clearCache();来手动清除缓存。
- 一级缓存默认开始,不能关闭。
10.4 二级缓存
- 二级缓存也叫全局缓存,由于一级缓存 作用域太低了,所以诞生了二级缓存。
- 基于namespace级别的缓存,也就是同一个mapper对应一个二级缓存。
- 工作机制
- 一个会话查询的数据先放在当前的一级缓存。
- 当会话关闭的时候一级缓存就不存在了。在关闭的时候会把缓存中的内容移到对应的namespace的二级缓存中。
- 新的会话查询信息就会从二级缓存中获取。
使用步骤:
- 开启全局二级缓存开关
<setting name="cacheEnabled" value="true"/>
tip: 二级全局开关是默认开启的,但为了有好的可读性最好也写上。
- 在要使用二级缓存的mapper.xml中开启
<cache/>
我们也可以加些参数
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true" />
-
eviction:缓存策略
先进先出:按对象进入缓存的顺序来移除它们。
LRU:最近最少使用 ,移除最长时间不被使用的对象。 (默认)
-
flushInterval: 缓存保留时间
-
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
-
readOnly(只读)属性可以被设置为 true 或 false。 默认是false,速度上会慢一些但是会安全。
-
将实体bean进行可序列化
由于这个步骤会将缓存进行序列化,故实体bean需要实现Serializable接口。
public class User implements Serializable { ...... }
-
测试
SqlSession session1 = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession(); UserMapper studentMapper = session1.getMapper(UserMapper.class); UserMapper studentMapper2 = session2.getMapper(UserMapper.class); User user1 = studentMapper.queryUserById(1); System.out.println(user1); // 只有在这个session关闭的时候才会把数据从一级缓存移到二级缓存中,故需要先close再第2次读取 session1.close(); System.out.println("============================="); User user2= studentMapper2.queryUserById(1); System.out.println(user2); session2.close();
-
查看结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lcgfozw6-1623772288906)(asset\1585103596023.png)]
-
补充
- 如果2次查询中进行了insert/update/delete操作则会清空缓存,重新去数据库查询。
10.5 自定义缓存
在mybatis中我们可以自定义缓存,也可以指定第三方缓存。这里我们通过ehcache演示。
-
导包
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.1.0</version> </dependency>
-
在resources目录新建eacache.xml文件,内容如下:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!--diskStore:缓存数据持久化的目录 地址 --> <diskStore path=".temdir/Tmp_EhCache" /> <defaultCache <!-- maxElementsInMemory: 内存中缓存的element的最大数目--> maxElementsInMemory="1000" <!-- maxElementsOnDisk:在磁盘上缓存的element的最大数目,默认值为0,表示不限制 --> maxElementsOnDisk="10000000" <!-- eternal: 设定缓存的elements是否过期。如果为true,则缓存的数据始终有效,如果是false, 要根据timeToIdleSeconds,timeToLiveSeconds判断 --> eternal="false" <!-- overflowToDisk:内存中数据超过限制,是否需要缓存到磁盘上 --> overflowToDisk="false" <!-- diskPersistent:是否在磁盘上持久化,重启jvm后,数据是否有效 --> diskPersistent="true" <!-- timeToIdleSeconds:对象空闲时间,只对eternal属性为false起作用 --> timeToIdleSeconds="120" <!-- timeToLiveSeconds:对象存活时间,只对eternal属性为false起作用 --> timeToLiveSeconds="120" <!-- 磁盘缓存的清理线程运行间隔 --> diskExpiryThreadIntervalSeconds="120" <!-- memoryStoreEvictionPolicy:超过内存空间,向磁盘存储数据的策略, 可选:LRU 最近最少使用 FIFO 先进先出 LFU 最少使用--> memoryStoreEvictionPolicy="LRU" /> </ehcache>
-
在mapper.xml中引入ehcache
<cache type="org.mybatis.caches.ehcache.EhcacheCache" />
-
测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0yDYce64-1623772288907)(asset\1585106110605.png)]
自定义缓存
那如果我们要自己写缓存而不使用第三方的缓存的话,只需要实现org.apache.ibatis.cache.Cache接口并在mapper中引入即可。需要在实现类中定义我们自己想要的缓存策略,真要自己能写出来请用10根头发来换。