目录
一、简介
1、历史
- MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目从Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下,iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github
- iBatis一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBatis提供的持久层框架包括SQL Maps和Data Access Objects(DAO)
- 如果打开mybatis的jar包,还可以在包名中看到
ibatis
的身影,如下:
2、特性
- MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架
- MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集
- MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录
- MyBatis 是一个 半自动的ORM(Object Relation Mapping)框架
3、和其他持久层框架对比
JDBC:
- SQL 夹杂在Java代码中耦合度高,导致硬编码内伤
- 维护不易且实际开发需求中 SQL 有变化,频繁修改的情况多见
- 代码冗长,开发效率低
Hibernate 和 JPA:
- 操作简便,开发效率高
- 程序中的长难复杂 SQL 需要绕过框架
- 内部自动生产的 SQL,不容易做特殊优化
- 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。
- 反射操作太多,导致数据库性能下降
Mybatis:
- 轻量级,性能出色
- SQL 和 Java 编码分开,功能边界清晰。Java代码专注业务、SQL语句专注数据
- 开发效率稍逊于HIbernate,但是完全能够接受
二、开发环境
1、Spring + Mybatis
1.1、依赖
<!-- MyBatis框架依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.1</version>
</dependency>
<!-- MyBatis与Spring整合依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
1.2、applicationContext-datasource.xml配置
- 红线:指定maybtis使用的数据库配置信息
- 黄线:指定核心配置文件的位置,避免mybatis找不到核心配置文件
- 黑线:指定mapper接口以及xml文件的位置,避免mybatis找不到Mapper接口和xml文件
1.3、核心配置文件
在下面这个目录中会详细说明核心配置文件,因此我们下面只将配置内容进行截图:
1.4、基本demo
https://gitee.com/mkdxm61/mybatis-spring-study-demo
2、SpringBoot + Mybatis
2.1、依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
2.2、application.properties配置
常用配置说明:
# MyBatis
mybatis:
# 常用配置1:搜索指定包别名,然后在xml文件中直接使用实体类名称即可(不区分实体类名称的大小写)
typeAliasesPackage: com.ruoyi.**.domain
# 常用配置2:配置mapper的扫描,找到所有的mapper.xml映射文件;即如果mapper的xml文件和bean实体类不在同一个目录下,那就需要指定一下mapper的xml文件的位置
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 常用配置3:加载全局的配置文件,即加载核心配置文件
configLocation: classpath:mybatis/mybatis-config.xml
常用配置1截图说明:
常用配置2截图说明:
常用配置3截图说明:
2.3、核心配置文件
在下面这个目录中会详细说明核心配置文件,因此我们下面只将配置内容进行截图:
2.4、使用@MapperScan注解扫描Mapper接口
可以直接使用@MapperScan注解扫描Mapper接口,这是方法1,替代方案是在每个Mapper接口上添加@Mapper接口,这是方法2,相比来说,肯定方法1更胜一筹,下面我们来截图方法1的用法:
2.5、基本demo
https://gitee.com/mkdxm61/mybatis-springboot-study-demo
三、核心配置文件
1、用途说明
习惯上命名为mybatis-config.xml,这个文件名仅仅只是建议,并非强制要求。将来整合Spring之后,这个配置文件可以省略,所以大家操作时可以直接复制、粘贴。
核心配置文件主要用于配置连接数据库的环境以及MyBatis的全局配置信息,但是在spring或者springboot中可以以其他方式配置数据库连接,所以不一定需要在配置文件中说明
核心配置文件存放的位置是src/main/resources目录下
2、标签顺序
核心配置文件中的标签必须按照固定的顺序(有的标签可以不写,但顺序一定不能乱),这是mybatis限定的,不按照顺序来就会报错,标签顺序如下:
properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers
3、写法举例
<?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文件,此时就可以${属性名}的方式访问属性值,一般用来配置数据库连接信息-->
<properties resource="jdbc.properties"></properties>
<!-- ***重要*** -->
<settings>
<!--将表中字段的下划线自动转换为驼峰,比如表中字段是emp_name,而Java实体类中是empName,如果想让两者对应起来,然后在xml中resultType直接使用实体类名称,这就需要开启该设置-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--开启延迟加载,通过get方法用到某属性时在执行该级别的sql语句,否则不执行(一个功能可以由很多个sql语句组合完成,延迟加载可以让暂时不需要用到的sql语句不执行)-->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- logImpl的value取值可以是:SLF4J | LOG4J(3.5.9 起废弃) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING -->
<!--指定 MyBatis 所用日志的具体实现,未指定时将自动查找,本次使用log4j-->
<setting name="logImpl" value="LOG4J"/>
<!--打印sql语句和执行结果到控制台,如果不设置,将不会打印信息到控制台-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- ***重要*** -->
<typeAliases>
<!--
1、单个实体类位置说明设置(用于说明实体类所在位置之后,可以直接在xml文件中使用实体类别名,而不用使用实体类全限定名称了,在真实开发中一般不使用这种方式):
typeAlias:设置某个具体的类型的别名
属性:
type:需要设置别名的类型的全类名
alias:设置此类型的别名,且别名不区分大小写。若不设置此属性,该类型拥有默认的别名,即类名
-->
<!--<typeAlias type="com.atguigu.mybatis.bean.User"></typeAlias>-->
<!--<typeAlias type="com.atguigu.mybatis.bean.User" alias="user">
</typeAlias>-->
<!--
2、多个实体类位置说明:
以包为单位,设置改包下所有的类型都拥有默认的别名,即类名且不区分大小写
-->
<package name="com.atguigu.mybatis.bean"/>
</typeAliases>
<!-- ***不重要*** -->
<!--
真实使用中一般不在此处指定数据库连接信息和事务情况,而是会在xml文件中进行详细配置
environments:设置多个连接数据库的环境
属性:
default:设置默认使用的环境的id
-->
<environments default="mysql_test">
<!--
environment:设置具体的连接数据库的环境信息
属性:
id:设置环境的唯一标识,可通过environments标签中的default设置某一个环境的id,表示默认使用的环境
-->
<environment id="mysql_test">
<!--
transactionManager:设置事务管理方式
属性:
type:设置事务管理方式,type="JDBC|MANAGED"
type="JDBC":设置当前环境的事务管理都必须手动处理
type="MANAGED":设置事务被管理,例如spring中的AOP
-->
<transactionManager type="JDBC"/>
<!--
dataSource:设置数据源
属性:
type:设置数据源的类型,type="POOLED|UNPOOLED|JNDI"
type="POOLED":使用数据库连接池,即会将创建的连接进行缓存,下次使用可以从缓存中直接获取,不需要重新创建
type="UNPOOLED":不使用数据库连接池,即每次使用连接都需要重新创建
type="JNDI":调用上下文中的数据源
-->
<dataSource type="POOLED">
<!--设置驱动类的全类名-->
<property name="driver" value="${jdbc.driver}"/>
<!--设置连接数据库的连接地址-->
<property name="url" value="${jdbc.url}"/>
<!--设置连接数据库的用户名-->
<property name="username" value="${jdbc.username}"/>
<!--设置连接数据库的密码-->
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- ***重要*** -->
<!--引入映射文件-->
<mappers>
<!-- 如果Mapper接口和xml文件不在同一个目录下面,那么就需要指明xml文件的位置 -->
<!-- 单个xml文件位置说明(实际中不用): -->
<!-- 由于mappers目录放在resource下面,所以写法如下 -->
<!-- <mapper resource="mappers/UserMapper.xml"/> -->
<!--
多个xml文件位置说明(实际中使用):
以包为单位,将包下所有的映射文件引入核心配置文件
注意:
1. 此方式必须保证mapper接口和mapper映射文件必须在相同的包下,虽然位置不同,mapper接口在java目录下面,而xml文件在resource目录下面,但是我们可以让两者的包名相同
2. mapper接口要和mapper映射文件的名字一致
-->
<package name="com.atguigu.mybatis.mapper"/>
</mappers>
</configuration>
4、Spring和SpringBoot使用时的对比说明
-
将实体类位置告诉mybatis,然后在xml中使用实体类别名:需要避免在xml文件中的type或者resultType属性上使用实体类的全限定类名,毕竟这太长了,比如:
<resultMap id="BaseResultMap" type="com.eloancn.p2p.model.loan.BidInfo" >
或者<select id="selectBidInfoListByLoanId" resultType="com.eloancn.p2p.model.loan.BidInfo">
,我们真正想在xml文件中使用的是实体类简单名称,比如:<resultMap id="BaseResultMap" type="BidInfo" >
或者<select id="selectBidInfoListByLoanId" resultType="BidInfo">
;
1、对于在Spring中的实现方法,可以设置核心配置文件中使用typeAliases
标签中的package
标签
2、对于SpringBoot中的实现方法,直接在application.yml中使用mybatis.typeAliasesPackage
设置即可
两者对比截图如下:
-
当xml文件和mapper不在同一个目录的同一个层级的时候,设置xml文件的位置:如果在同一个位置,只要指定Mapper接口的位置,mybatis自然会去同一位置查找同名称的xml文件,但是如果把xml文件放在resource下面,那么我们需要指定xml文件的位置,对于spring和springboot中的指定方式区别如下:
-
扫描Mapper接口:
四、java代码
1、传递参数值写法
总结
无论哪种情况,参数全部添加@Param注解,并在注解中添加value属性值,在xml中直接使用属性值即可
1.1、普通单参(不加@Param注解)
说明:
- 如果mapper接口中的方法参数是单个参数,那么xml文件中可以使用
${XXX}
和#{XXX}
获取参数的值(XXX代表任意的名称(最好见名识意)),注意使用${XXX}
需要手动加单引号
Mapper方法写法:
User getUserByUsername(String username);
xml方法写法:
<!--1、#{XXX}写法,此后直接说成#{}写法-->
<select id="getUserByUsername" resultType="User">
select * from t_user where username = #{username}
</select>
<!--2、${XXX}写法,此后直接说成${}写法-->
<select id="getUserByUsername" resultType="User">
select * from t_user where username = '${username}'
</select>
1.2、普通多参(不加@Param注解)
说明:
-
若mapper接口中的方法参数为多个时,此时MyBatis会自动将这些参数放在一个map集合中
- 以arg0,arg1…为键,以参数为值;
- 以param1,param2…为键,以参数为值;
-
因此只需要通过
${}
和#{}
访问map集合的键就可以获取相对应的值,注意${}
需要手动加单引号。 -
使用arg或者param都行,要注意的是,arg是从arg0开始的,param是从param1开始的
Mapper方法写法:
User checkLogin(String username,String password);
xml方法写法:
<!--1、#{}写法-->
<select id="checkLogin" resultType="User">
select * from t_user where username = #{arg0} and password = #{arg1}
</select>
<!--2、${}写法-->
<select id="checkLogin" resultType="User">
select * from t_user where username = '${param1}' and password = '${param2}'
</select>
1.3、map集合(不加@Param注解)
说明:
- 若mapper接口中的方法需要的参数为多个时,此时可以手动创建map集合,然后将这些数据放在map中,那么只需要通过
${}
和#{}
访问map集合的键就可以获取相对应的值,注意${}
需要手动加单引号
Mapper方法写法:
User checkLoginByMap(Map<String,Object> map);
xml方法写法:
<!--1、#{}写法-->
<select id="checkLoginByMap" resultType="User">
select * from t_user where username = #{username} and password = #{password}
</select>
<!--2、${}写法-->
<select id="checkLoginByMap" resultType="User">
select * from t_user where username = '${username}' and password = '${password}'
</select>
调用Mapper方法的代码写法:
@Test
public void checkLoginByMap() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("usermane","admin");
map.put("password","123456");
User user = mapper.checkLoginByMap(map);
System.out.println(user);
}
1.4、实体类对象单参(不加@Param注解)
说明:
- 若mapper接口中的方法参数为实体类对象时此时可以使用
${}
和#{}
,通过访问实体类对象中的属性名获取属性值,注意${}
需要手动加单引号
Mapper方法写法:
int insertUser(User user);
xml方法写法:
<!--1、#{}写法-->
<insert id="insertUser">
insert into t_user values(null,#{username},#{password},#{age},#{sex},#{email})
</insert>
<!--2、${}写法-->
<insert id="insertUser">
insert into t_user values(null,'${username}','${password}','${age}','${sex}','${email}')
</insert>
调用Mapper方法的代码写法:
@Test
public void insertUser() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
User user = new User(null,"Tom","123456",12,"男","123@321.com");
mapper.insertUser(user);
}
1.5、使用@Param标识参数
说明:
- 可以通过@Param注解标识mapper接口中的方法参数,此时,会将这些参数放在map集合中
- 以@Param注解的value属性值为键,以参数为值;
- 以param1,param2…为键,以参数为值;
- 只需要通过
${}
和#{}
访问map集合的键就可以获取相对应的值,注意${}
需要手动加单引号
Mapper方法写法:
User CheckLoginByParam(@Param("username") String username, @Param("password") String password);
xml方法写法:
<!--1、#{}写法-->
<select id="CheckLoginByParam" resultType="User">
select * from t_user where username = #{username} and password = #{password}
</select>
<!--2、${}写法-->
<select id="CheckLoginByParam" resultType="User">
select * from t_user where username = '${username}' and password = '${password}'
</select>
调用Mapper方法的代码写法:
@Test
public void checkLoginByParam() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
mapper.CheckLoginByParam("admin","123456");
}
2、接收参数值写法
2.1、单个类
说明:
- 最多能查询到1条数据,如果返回多条数据就会出现TooManyResultsException异常
Mapper方法写法:
/**
* 根据用户id查询用户信息
* @param id
* @return
*/
User getUserById(@Param("id") int id);
xml方法写法:
<select id="getUserById" resultType="User">
select * from t_user where id = #{id}
</select>
2.2、类集合
说明:
- 无论返回一条数据,还是多条数据,都可以使用集合来接收
Mapper方法写法:
/**
* 查询所有用户信息
* @return
*/
List<User> getUserList();
xml方法写法:
<select id="getUserList" resultType="User">
select * from t_user
</select>
2.3、普通单值(基本数据类型 / 基本数据类型包装类 / String)
Mapper方法写法:
/**
* 查询用户的总记录数
* @return
* 在MyBatis中,对于Java中常用的类型都设置了类型别名
* 例如:java.lang.Integer-->int|integer
* 例如:int-->_int|_integer
* 例如:Map-->map,List-->list
*/
int getCount();
xml方法写法:
<!-- resultType属性值对应int类型 -->
<select id="getCount" resultType="_integer">
select count(id) from t_user
</select>
2.4、Map集合(接收一条数据)
说明:
- 返回值是一条数据的查询结果,不在乎该条数据是否对应实体类
Mapper方法写法:
/**
* 根据用户id查询用户信息为map集合
* @param id
* @return
*/
Map<String, Object> getUserToMap(@Param("id") int id);
xml方法写法:
<!--
查询结果举例:
{password=123456, sex=男, id=1, age=23, username=admin}
-->
<select id="getUserToMap" resultType="map">
select * from t_user where id = #{id}
</select>
2.5、Map集合(接收多条数据)
说明:
- 返回值是一条或者多条数据的查询结果,不在乎这些数据是否对应实体类
2.5.1、方法1(使用List<Map<String, Object>>接收返回值)
Mapper方法写法:
/**
* 查询所有用户信息为map集合
* @return
* 将表中的数据以map集合的方式查询,一条数据对应一个map;若有多条数据,就会产生多个map集合,此时可以将这些map放在一个list集合中获取
*/
List<Map<String, Object>> getAllUserToMap();
xml方法写法:
<!--
查询结果举例:
[{password=123456, sex=男, id=1, age=23, username=admin},
{password=123456, sex=男, id=2, age=23, username=张三},
{password=123456, sex=男, id=3, age=23, username=张三}]
-->
<select id="getAllUserToMap" resultType="map">
select * from t_user
</select>
2.5.2、方法2(使用Map<String, Object>接收返回值)
Mapper方法写法:
/**
* 查询所有用户信息为map集合
* @return
* 将表中的数据以map集合的方式查询,一条数据对应一个map;若有多条数据,就会产生多个map集合,并且最终要以一个map的方式返回数据,此时需要通过@MapKey注解设置map集合的键,而值对应每条数据所组成的map集合
*/
@MapKey("id")
Map<String, Object> getAllUserToMap();
xml方法写法:
<!--
查询结果举例:
{
1={password=123456, sex=男, id=1, age=23, username=admin},
2={password=123456, sex=男, id=2, age=23, username=张三},
3={password=123456, sex=男, id=3, age=23, username=张三}
}
-->
<select id="getAllUserToMap" resultType="map">
select * from t_user
</select>
3、@Param详解
@Param注解采用代理模式来完成Map结构参数的赋值,至于存在argX和paramX之分,这全是mybatis内部代码拼接下标得到,但是在使用过程中一般不会采用,所以建议全部使用@Param注解标识参数,然后在xml中直接使用@param注解中的value属性值即可,此处不再赘述,有兴趣的直接去看 【尚硅谷】2022版MyBatis零基础入门教程(细致全面,快速上手)—29_@Param源码解析
五、xml文件
1、resultType和resultMap区别
-
resultType
使用情况:- sql执行结果和类中属性完全对应
- sql执行结果和类无法对应,但是通过全局设置使用驼峰命名法,之后sql执行结果和类完全对应
使用说明:
- 可以是类的全限定名称
- 如果向mybatis说明了实体类的包名位置,可以写成实体类简写类名,说明方法在Spring和SpringBoot使用时的对比说明中的第1条,并且不仅仅是表对应的实体类,还可以是Vo类等
-
resultMap
使用情况:- sql执行结果是 基本数据类型 / 基本数据类型保证类 / String
- sql执行结果和类无法关联,即使全局设置了驼峰命名法也无法关联
使用说明:
- 可以是 基本数据类型 / 基本数据类型保证类 / String的全限定名称
- 可以是 基本数据类型 / 基本数据类型保证类 / String 对应的类型别名(请看:xml文件》默认类型别名写法)
- 可以是xml标签中resultMap标签的id属性值
2、默认类型别名写法
下面是一些为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格。
别名 | 映射的类型 |
---|---|
_byte | byte |
_char (since 3.5.10) | char |
_character (since 3.5.10) | char |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
char (since 3.5.10) | Character |
character (since 3.5.10) | Character |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
biginteger | BigInteger |
object | Object |
date[] | Date[] |
decimal[] | BigDecimal[] |
bigdecimal[] | BigDecimal[] |
biginteger[] | BigInteger[] |
object[] | Object[] |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
举例: 如果resultType
属性值原来是java.lang.Integer
类型,那么可以写成integer
3、#{}和${}的使用场景
-
MyBatis获取参数值的两种方式:
${}
和#{}
,由于使用${}
存在sql注入的风险,所以优先使用#{}
-
${}
的本质就是字符串拼接,#{}
的本质就是占位符赋值 -
${}
不会自动添加单引号,而#{}
会自动添加单引号 -
${}
可以被包裹在单引号中使用,而#{}
被包裹在单引号中不能被识别,否则将被当做?
-
当对字符串类型或日期类型的字段进行赋值时,对于${}来说需要手动加单引号,而#{}可以自动添加单引号,这也正是上面的印证
-
大部分情况下使用#{},小部分情况需要使用${},特殊情况请看
特殊情况写法
4、parameterType、jdbcType一定要写吗?
不需要写,mybatis会进行自动推断
5、自定义映射resultMap写法
5.1、resultMap介绍
- resultMap参数说明(作用:设置自定义映射)
- 属性:
- id:表示自定义映射的唯一标识,不能重复
- type:查询的数据要映射的实体类的类型
- 子标签:
- id:设置主键的映射关系
- result:设置普通字段的映射关系
- 子标签属性:
- property:设置映射关系中实体类中的属性名
- column:设置映射关系中表中的字段名
- 属性:
- 使用resultMap需要列出全部属性:若字段名和实体类中的属性名不一致,则可以通过resultMap设置自定义映射,即使字段名和属性名一致的属性也要映射,也就是全部属性都要列出来
- 可以使用驼峰命名法来解决数据库字段名和实体类名称不一致的情况,用来避免使用resutlMap标签:若字段名和实体类中的属性名不一致,但是字段名符合数据库的规则(使用_),实体类中的属性名符合Java的规则(使用驼峰)。此时也可通过以下两种方式处理字段名和实体类中的属性的映射关系
-
可以通过为字段起别名的方式,保证和实体类中的属性名保持一致
-
可以在MyBatis的
核心配置文件
中的setting标签中,设置一个全局配置信息mapUnderscoreToCamelCase,可以在查询表中数据时,自动将_类型的字段名转换为驼峰,例如:字段名user_name,设置了mapUnderscoreToCamelCase,此时字段名就会转换为userName。
-
5.2、一对一映射处理
说明: 一对多映射的场景、共有类、共有接口和下面相同,因此在一对多映射中不在赘述
5.2.1、场景说明
一个部门可以有多个员工,一个员工只属于一个部门
5.2.2、sql语句
-- 部门表
DROP TABLE IF EXISTS `t_dept`;
CREATE TABLE `t_dept` (
`id` int(5) NOT NULL,
`dept_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
INSERT INTO `t_dept` VALUES (1, '销售部');
INSERT INTO `t_dept` VALUES (2, '技术部');
INSERT INTO `t_dept` VALUES (3, '运维部');
-- 员工表
DROP TABLE IF EXISTS `t_emp`;
CREATE TABLE `t_emp` (
`id` int(11) NOT NULL,
`emp_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`dept_id` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
INSERT INTO `t_emp` VALUES (1, '小明', 1);
INSERT INTO `t_emp` VALUES (2, '小黄', 1);
INSERT INTO `t_emp` VALUES (3, '小亮', 1);
INSERT INTO `t_emp` VALUES (4, '小白', 2);
5.2.3、共有类 / 接口
实体类:
// 部门(需要在pom.xml文件中添加lombok依赖)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Dept {
public Integer id;
public String deptName;
}
// 员工(需要在pom.xml文件中添加lombok依赖)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Emp {
public Integer id;
public String empName;
public Integer deptId;
}
实体类对应的VO类:
// 部门Vo类(需要在pom.xml文件中添加lombok依赖)
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptVo extends Dept {
public List<Emp> emps;
@Override
public String toString() {
return "DeptVo{" +
"emps=" + emps +
", id=" + id +
", deptName='" + deptName + '\'' +
'}';
}
}
// 员工Vo类(需要在pom.xml文件中添加lombok依赖)
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmpVo extends Emp {
public Dept dept;
@Override
public String toString() {
return "EmpVo{" +
"dept=" + dept +
", id=" + id +
", empName='" + empName + '\'' +
", deptId=" + deptId +
'}';
}
}
Mapper接口:
// 部门Mapper接口:
public interface DeptMapper {
/**
* 查询所有部门信息,包括部门中的所有员工
* @return 部门Vo集合
*/
List<DeptVo> getAll();
/**
* 根据部门id查询部门信息(说明:为分步查询准备)
* @param id 部门id
* @return 部门信息
*/
Dept getById(@Param("id") String id);
}
// 员工Mapper接口:
public interface EmpMapper {
/**
* 查询所有员工信息,包括员工所属的部门信息
* @return 员工Vo集合
*/
List<EmpVo> getAll();
/**
* 根据部门id找出部门中的所有员工
* @param deptId 部门id
* @return 当前部门id中的所有员工
*/
List<Emp> findByDeptId(@Param("deptId") String deptId);
}
5.2.4、使用级联方式处理映射关系
EmpMapper.xml:
<resultMap id="empVo" type="com.atguigu.demo.vo.EmpVo">
<id property="id" column="id" />
<result property="empName" column="emp_name" />
<result property="deptId" column="dept_id"/>
<!--
此处为级联写法,其中dept是EmpVo类中的的属性名称,
属性类型是Dept类型,另外id和deptName都是Dept类的属性名称
-->
<result property="dept.id" column="dept_id"/>
<result property="dept.deptName" column="dept_name"/>
</resultMap>
<select id="getAll" resultMap="empVo">
<!--
避免用户id和部门id重复,
而部门id存在于员工表中,所以我们只需要获取部门表的名称即可
-->
select e.*, d.dept_name from t_emp e left join t_dept d on e.dept_id = d.id
</select>
结果如下:
[EmpVo{dept=Dept(id=1, deptName=销售部), id=1, empName='小明', deptId=1}, EmpVo{dept=Dept(id=1, deptName=销售部), id=2, empName='小黄', deptId=1}, EmpVo{dept=Dept(id=1, deptName=销售部), id=3, empName='小亮', deptId=1}, EmpVo{dept=Dept(id=2, deptName=技术部), id=4, empName='小白', deptId=2}]
5.2.5、使用association标签处理映射关系
EmpMapper.xml:
<resultMap id="empVo" type="com.atguigu.demo.vo.EmpVo">
<id property="id" column="id" />
<result property="empName" column="emp_name" />
<result property="deptId" column="dept_id"/>
<!--
property参数值dept代表EmpVo类中的属性名称,
javaType参数值Dept是实体类别名,用来指明属性类型,因为我们已经把实体类位置告诉了mybatis,
所以可以这样使用,详细解释可以看:
Spring和SpringBoot使用时的对比说明中的第1条
-->
<association property="dept" javaType="Dept">
<id property="id" column="dept_id"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
<select id="getAll" resultMap="empVo">
<!--
避免用户id和部门id重复,
而部门id存在于员工表中,所以我们只需要获取部门表的名称即可
-->
select e.*, d.dept_name from t_emp e left join t_dept d on e.dept_id = d.id
</select>
结果如下:
[EmpVo{dept=Dept(id=1, deptName=销售部), id=1, empName='小明', deptId=1}, EmpVo{dept=Dept(id=1, deptName=销售部), id=2, empName='小黄', deptId=1}, EmpVo{dept=Dept(id=1, deptName=销售部), id=3, empName='小亮', deptId=1}, EmpVo{dept=Dept(id=2, deptName=技术部), id=4, empName='小白', deptId=2}]
5.2.6、分步查询(特别说明:执行多次select查询操作)
EmpMapper.xml:
<!-- 第一步 -->
<resultMap id="empVo" type="com.atguigu.demo.vo.EmpVo">
<id property="id" column="id" />
<result property="empName" column="emp_name" />
<result property="deptId" column="dept_id"/>
<!--
property参数值dept代表EmpVo类中的属性名称,
select参数值代表Mapper接口中方法的全限定名称,该方法对应的xml文件写法请看下面的DeptMapper.xml,
column参数值代表该sql语句执行结果中的列名dept_id,我们需要将该列值 当做 上述方法的参数值
-->
<association property="dept" select="com.atguigu.demo.mapper.DeptMapper.getById" column="dept_id"/>
</resultMap>
<select id="getAll" resultMap="empVo">
select * from t_emp
</select>
DeptMapper.xml:
<!-- 第二步 -->
<!-- Mapper接口方法:Dept getById(@Param("id") String id); -->
<select id="getById" resultType="Dept">
select * from t_dept where id = #{id}
</select>
结果如下:
[EmpVo{dept=Dept(id=1, deptName=销售部), id=1, empName='小明', deptId=1}, EmpVo{dept=Dept(id=1, deptName=销售部), id=2, empName='小黄', deptId=1}, EmpVo{dept=Dept(id=1, deptName=销售部), id=3, empName='小亮', deptId=1}, EmpVo{dept=Dept(id=2, deptName=技术部), id=4, empName='小白', deptId=2}]
5.3、一对多映射处理(TODO)
5.3.1、使用collection处理映射关系
DeptMapper.xml:
<resultMap id="deptVo" type="com.atguigu.demo.vo.DeptVo">
<id property="id" column="id"/>
<result property="deptName" column="dept_name"/>
<!--
property="emps"是DeptVo中的Emp集合属性名称
ofType="Emp"是com.atguigu.demo.domain.Emp的别名,
用来指明属性类型,因为我们已经把实体类位置告诉了mybatis,
所以可以这样使用,详细解释可以看:
Spring和SpringBoot使用时的对比说明中的第1条
-->
<collection property="emps" ofType="Emp">
<!--下面是Emp中属性和列名的对应关系-->
<id property="id" column="emp_id"/>
<result property="empName" column="emp_name"/>
<result property="deptId" column="emp_dept_id"/>
</collection>
</resultMap>
<select id="getAll" resultMap="deptVo">
select d.*, e.id emp_id, e.emp_name, e.dept_id emp_dept_id from t_dept d left join t_emp e on d.id = e.dept_id
</select>
结果如下:
[DeptVo{emps=[Emp(id=1, empName=小明, deptId=1), Emp(id=2, empName=小黄, deptId=1), Emp(id=3, empName=小亮, deptId=1)], id=1, deptName='销售部'}, DeptVo{emps=[Emp(id=4, empName=小白, deptId=2)], id=2, deptName='技术部'}, DeptVo{emps=[], id=3, deptName='运维部'}]
5.3.2、分步查询(特别说明:执行多次select查询操作)
DeptMapper.xml:
<!-- 第一步: -->
<resultMap id="deptVo" type="com.atguigu.demo.vo.DeptVo">
<id property="id" column="id"/>
<result property="deptName" column="dept_name"/>
<!--
property="emps"是DeptVo中的Emp集合属性名称
select参数值是findByDeptId方法的全限定名称
column="id"是当前部门的id,我们需要把该id值传递给findByDeptId方法参数,用来查询部门员工信息集合
-->
<collection property="emps" select="com.atguigu.demo.mapper.EmpMapper.findByDeptId" column="id"/>
</resultMap>
<select id="getAll" resultMap="deptVo">
select * from t_dept
</select>
EmpMapper.xml:
<!-- 第二步: -->
<!-- Mapper接口方法:List<Emp> findByDeptId(@Param("deptId") String deptId); -->
<select id="findByDeptId" resultType="Emp">
select * from t_emp where dept_id = #{deptId}
</select>
结果如下:
[DeptVo{emps=[Emp(id=1, empName=null, deptId=null), Emp(id=2, empName=null, deptId=null), Emp(id=3, empName=null, deptId=null)], id=1, deptName='销售部'}, DeptVo{emps=[Emp(id=4, empName=null, deptId=null)], id=2, deptName='技术部'}, DeptVo{emps=[], id=3, deptName='运维部'}]
5.4、分步查询—开启延迟加载
-
分步查询的优点:可以实现延迟加载,并且延迟加载只有在分布查询的情况下才有用,开启延迟加载必须要在核心配置文件中设置全局配置信息(默认不开启延迟加载,并且会执行所有步骤中的sql语句)相关的两个配置如下
-
lazyLoadingEnabled:延迟加载全局开关,默认为false,需要在核心配置文件中配置,相关配置截图在下面。如果不考虑association和collection标签中的fetchType属性,当lazyLoadingEnabled为true时,所有关联对象都会延迟加载,也就是只有通过get方法调用的时候才执行对应sql语句,否则不执行sql语句,减少资源浪费
-
fetchType:懒加载开关,它是xml文件中association和collection标签的属性,默认为lazy(延迟加载),只有当lazyLoadingEnabled设置为true的时候才有意义,否则毫无意义,相关配置方法截图如下;当lazyLoadingEnabled为true时,association和collection中的fetchType属性用来设置当前的分步查询是否使用延迟加载,取值可以是"lazy(延迟加载)|eager(立即加载)",默认lazy(延迟加载)
-
6、基本增删改查写法
说明: 增删改操作
的默认返回值是受影响的条数,并且也只能是它,既然只能是它,那就不用写resultType或者resultMap属性了
6.1、增
<!--Mapper接口中方法写法:int insertUser();-->
<insert id="insertUser">
insert into t_user values(null,'admin','123456',23,'男','12345@qq.com')
</insert>
6.2、删
<!--int deleteUser();-->
<delete id="deleteUser">
delete from t_user where id = 6
</delete>
6.3、改
<!--int updateUser();-->
<update id="updateUser">
update t_user set username = '张三' where id = 5
</update>
6.4、查询单个类
<!--User getUserById();-->
<select id="getUserById" resultType="com.atguigu.mybatis.bean.User">
select * from t_user where id = 2
</select>
6.5、查询类集合
<!--List<User> getUserList();-->
<select id="getUserList" resultType="com.atguigu.mybatis.bean.User">
select * from t_user
</select>
- 查询的标签select必须设置属性
resultType
或resultMap
,用于设置实体类和数据库表的映射关系- resultType:自动映射,用于属性名和表中字段名一致的情况(或者设置全局使用驼峰命名法之后能对应也行)
- resultMap:自定义映射,用于一对多 / 多对一 / 字段名和属性名不一致的情况
- 当查询的数据为多条时,不能使用实体类作为返回值,只能使用集合,否则会抛出异常TooManyResultsException;但是若查询的数据只有一条,可以使用实体类或集合作为返回值
7、多种查询写法
直接看 java代码
》接收参数值写法
8、特殊情况写法
8.1、模糊查询
Mapper方法写法:
/**
* 根据用户名进行模糊查询
* @param username
* @return java.util.List<com.atguigu.mybatis.pojo.User>
* @date 2022/2/26 21:56
*/
List<User> getUserByLike(@Param("username") String username);
xml方法写法:
<select id="getUserByLike" resultType="User">
<!-- 方法1(不推荐):使用${},存在sql注入的风险 -->
<!--select * from t_user where username like '%${username}%'-->
<!-- 方法2(推荐) -->
<!--select * from t_user where username like concat('%',#{username},'%')-->
<!-- 方法3(推荐) -->
select * from t_user where username like "%"#{username}"%"
</select>
8.2、批量删除
说明:
- 只能使用
${}
,如果使用#{}
,则解析后的sql语句为delete from t_user where id in ('1,2,3')
,这样是将1,2,3
看做是一个整体,只有id为1,2,3
的数据会被删除。正确的语句应该是delete from t_user where id in (1,2,3)
,或者delete from t_user where id in ('1','2','3')
,所以我们可以使用String.join(拼接符, ',')
/StringUtils.join(集合, ',')
将参数值用英文逗号拼写在一起,然后在传入xml
文件中
Mapper方法写法:
/**
* 根据id批量删除
* @param ids
* @return int
* @date 2022/2/26 22:06
*/
int deleteMore(@Param("ids") String ids);
xml方法写法:
<delete id="deleteMore">
delete from t_user where id in (${ids})
</delete>
调用Mapper方法的代码写法:
//测试类
@Test
public void deleteMore() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
SQLMapper mapper = sqlSession.getMapper(SQLMapper.class);
int result = mapper.deleteMore("1,2,3,8");
System.out.println(result);
}
8.3、动态设置表名
说明:
- 只能使用
${}
,因为表名不能加单引号
Mapper方法写法:
/**
* 查询指定表中的数据
* @param tableName
* @return java.util.List<com.atguigu.mybatis.pojo.User>
* @date 2022/2/27 14:41
*/
List<User> getUserByTable(@Param("tableName") String tableName);
xml方法写法:
<select id="getUserByTable" resultType="User">
select * from ${tableName}
</select>
8.4、插入数据到数据库,自动填充自增主键值到对象主键值中
说明:
- 只有主键自增的情况才能这样用,对于使用java代码生成主键的情况,可以直接通过get方法获得主键值,不用如此操作
模拟背景说明:
添加班级的同时选中班级的学生
先增加班级,其中班级主键id是数据库自增主键,当班级增加完毕之后,要求班级中的主键id值回显
当添加学生的时候,将班级主键id值赋予学生对象的班级id中,然后在增加班级中的学生
Mapper方法写法:
ClassMapper(班级):
void insert(Class c);
UserMapper(学生):
void insert(User u);
xml方法写法:
ClassMapper.xml(班级):
// useGeneratedKeys:说明使用自增主键
// keyProperty:因为增删改的返回值都是受影响的行数,因此只能将回显的自增主键值放在参数对象user的某个属性中,最好依然放在主键中,方便回显
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into t_class values(null, #{className})
</insert>
UserMapper.xml(学生):
<insert id="insert">
insert into t_user values(null, #{userName}, #{password}, #{sex}, #{age}, #{classId})
</insert>
实体类代码写法:
// 班级(需要在pom.xml文件中添加lombok依赖)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Class {
private Integer id;
private String className;
}
// 学生(需要在pom.xml文件中添加lombok依赖)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String userName;
private String password;
private String sex;
private Integer age;
private Integer classId;
public User(String userName, String password, String sex, Integer age, Integer classId) {
this.userName = userName;
this.password = password;
this.sex = sex;
this.age = age;
this.classId = classId;
}
}
调用Mapper方法的代码写法:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
ClassMapper classMapper = sqlSession.getMapper(ClassMapper.class);
Class c = new Class();
c.setClassName("1班");
// 创建班级数据
classMapper.insert(c);
// 准备学生数据
User u1 = new User("小明", "123456", "男", 1, c.getId());
User u2 = new User("小亮", "123456", "男", 3, c.getId());
User u3 = new User("小华", "123456", "女", 5, c.getId());
// 创建学生数据
userMapper.insert(u1);
userMapper.insert(u2);
userMapper.insert(u3);
9、动态SQL写法
注意: 标签后面会自动添加空格,所以不用担心字符串挨到一起的情况
9.1、if
说明:
- if标签可通过test属性(即传递过来的数据)中的表达式进行判断,若表达式的结果为true,则标签中的内容会执行;反之标签中的内容不会执行
- test属性的值一般写法:
<if test="xxx != null and xxx != ''">
Mapper方法写法:
List<User> findByKeyword(@Param("keyword") String keyword);
xml方法写法:
<select id="findByKeyword" resultType="User">
select * from t_user
<if test="keyword != null and keyword != ''">
where userName like "%"#{keyword}"%"
</if>
</select>
调用Mapper方法的代码写法:
List<User> list = userMapper.findByKeyword("小");
System.out.println(list);
结果:
[User(id=2, userName=小明, password=123456, sex=男, age=1, classId=3), User(id=3, userName=小亮, password=123456, sex=男, age=3, classId=3), User(id=4, userName=小华, password=123456, sex=女, age=5, classId=3)]
9.2、where
说明:
- where和if一般结合使用:
- 若where标签中的所有if条件都不满足,则不会添加where关键字
- where标签会将条件最前方多余的
and
/or
去掉,但是where标签不能去掉条件后多余的and
/or
Mapper方法写法:
List<User> findByCondition(User user);
xml方法写法:
<!-- 参数是单类对象,所以可以直接使用属性名,在四、java代码》1、传递参数值写法》1.4、实体类对象单参(不加@Param注解)中写到了 -->
<select id="findByCondition" resultType="User">
select * from t_user
<where>
<if test="userName != null and userName != ''">
userName like "%"#{userName}"%"
</if>
<if test="sex != null and sex != ''">
and sex = #{sex}
</if>
<if test="age != null">
and age = #{age}
</if>
</where>
</select>
调用Mapper方法的代码写法:
User user = new User();
user.setUserName("小");
user.setSex("男");
user.setAge(1);
List<User> list = userMapper.findByCondition(user);
System.out.println(list);
结果:
[User(id=2, userName=小明, password=123456, sex=男, age=1, classId=3)]
9.3、trim
说明:
- trim用于去掉或添加标签中的内容,常用属性如下
- prefix:在trim标签中内容的前面添加某些内容
- suffix:在trim标签中内容的后面添加某些内容
- prefixOverrides:在trim标签中内容的前面去掉某些内容
- suffixOverrides:在trim标签中内容的后面去掉某些内容
- 若最终trim标签内容为空,那么trim标签没有任何效果
Mapper方法写法:
void insertSelect(User user);
xml方法写法:
<insert id="insertSelect">
insert into t_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="userName != null">
userName,
</if>
<if test="password != null">
password,
</if>
<if test="sex != null">
sex,
</if>
<if test="age != null">
age
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id},
</if>
<if test="userName != null and userName != ''">
#{userName},
</if>
<if test="password != null and password != ''">
#{password},
</if>
<if test="sex != null and sex != ''">
#{sex},
</if>
<if test="age != null">
#{age}
</if>
</trim>
</insert>
调用Mapper方法的代码写法:
User user = new User();
user.setUserName("小李");
user.setPassword("123456");
user.setSex("男");
user.setAge(3);
userMapper.insertSelect(user);
9.4、choose、when、otherwise
说明:
choose、when、otherwise
相当于if...else if..else
- when至少要有一个,otherwise至多只有一个
Mapper方法写法:
// 查询1岁的男孩子,或者2岁的女孩子,或者查询所有孩子
List<User> findByAge(@Param("age") Integer age);
xml方法写法:
<select id="findByAge" resultType="User">
select * from t_user
<where>
<choose>
<when test="age == 1">
sex = "男"
</when>
<when test="age == 2">
sex = "女"
</when>
<otherwise>
sex = "男" or sex = "女"
</otherwise>
</choose>
</where>
</select>
调用Mapper方法的代码写法:
// 下面代码用于查询所有孩子
List<User> list = userMapper.findByAge(3);
System.out.println(list);
结果:
[User(id=2, userName=小明, password=123456, sex=男, age=1, classId=3), User(id=3, userName=小亮, password=123456, sex=男, age=3, classId=3), User(id=4, userName=小华, password=123456, sex=女, age=5, classId=3), User(id=5, userName=小李, password=123456, sex=男, age=3, classId=null)]
9.5、foreach
说明:
- 属性说明如下:
- collection:设置要循环的数组或集合
- item:表示集合或数组中的每一个数据
- separator:设置循环体之间的分隔符,分隔符前后默认有一个空格,如,
- open:设置foreach标签中的内容的开始符
- close:设置foreach标签中的内容的结束符
Mapper方法写法:
int insertBatch(@Param("users") List<User> users);
xml方法写法:
<insert id="insertBatch">
insert into t_user
values
<foreach collection="users" item="user" separator=",">
(null, #{user.userName}, #{user.password}, #{user.sex}, #{user.age})
</foreach>
</insert>
调用Mapper方法的代码写法:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> users = new ArrayList<>();
User u1 = new User("小明", "123456", "男", 1);
User u2 = new User("小亮", "123456", "男", 3);
User u3 = new User("小华", "123456", "女", 5);
users.add(u1);
users.add(u2);
users.add(u3);
int count = userMapper.insertBatch(users);
System.out.println(count);
结果:
3
9.6、SQL片段
说明:
sql片段可以重复利用,内容中一般写字段,但是也可以写其他sql片段
Mapper方法写法:
User getById(Integer id);
xml方法写法:
<sql id="someField">
id, userName, sex, age
</sql>
<select id="getById" resultType="User">
select
<include refid="someField" />
from t_user
where id = #{id}
</select>
调用Mapper方法的代码写法:
User user = userMapper.getById(1);
System.out.println(user);
结果:
User(id=1, userName=小李, password=null, sex=男, age=3)
六、Mybatis缓存
1、一级缓存
1.1、概念
- 一级缓存默认开启
- 一级缓存是SqlSession级别的,对于同一个sqlSession,第一次查询结果会被缓存,第二次使用相同查询条件进行查询,将直接从缓存中获取结果,不会在访问数据库
- 一级缓存失效的四种情况:
- SqlSession不同
- 同一个SqlSession,但是查询条件不同
- 同一个SqlSession,并且同一个查询条件,然后两次查询期间执行了任何一次增删改操作
- 同一个SqlSession,并且同一个查询条件,然后两次查询期间手动清空了缓存(
例如:sqlSession.clearCache();
)
1.2、演示一级缓存
说明:
通过用户id获取用户信息,同一个sqlSession,同一个查询条件,进行两次查询
Mapper方法写法:
User getById(Integer id);
xml方法写法:
<select id="getById" resultType="User">
select * from t_user where id = #{id}
</select>
调用Mapper方法的代码写法:
System.out.println(">>>开始第1次查询……");
User user1 = userMapper.getById(1);
System.out.println(">>>结束第1次查询……");
System.out.println(">>>开始第2次查询……");
User user2 = userMapper.getById(1);
System.out.println(">>>结束第2次查询……");
System.out.println(user1);
System.out.println(user2);
结果:
>>>开始第1次查询……
Opening JDBC Connection
Created connection 860481979.
==> Preparing: select * from t_user where id = ?
==> Parameters: 1(Integer)
<== Columns: id, userName, password, sex, age
<== Row: 1, 小李, 123456, 男, 3
<== Total: 1
>>>结束第1次查询……
>>>开始第2次查询……
>>>结束第2次查询……
User(id=1, userName=小李, password=123456, sex=男, age=3)
User(id=1, userName=小李, password=123456, sex=男, age=3)
分析:
可以看到第一次查询的时候访问了数据库,而第二次查询没有访问数据库,说明第二次查询使用了一级缓存
1.3、演示一级缓存失效的四种情况
说明: 一级缓存失效,将导致每次都查询数据库
演示代码如下:
// 情况1:SqlSession不同
// openSession(xxx)中参数为true的时候,执行sql语句之后自动执行sqlSession.commit()提交事务
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
System.out.println(">>>开始第1次查询……");
User user1 = userMapper1.getById(1);
System.out.println(">>>结束第1次查询……");
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
System.out.println(">>>开始第2次查询……");
User user2 = userMapper2.getById(1);
System.out.println(">>>结束第2次查询……");
System.out.println(user1);
System.out.println(user2);
// 情况2:同一个SqlSession,但是查询条件不同
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println(">>>开始第1次查询……");
User user1 = userMapper.getById(1);
System.out.println(">>>结束第1次查询……");
System.out.println(">>>开始第2次查询……");
User user2 = userMapper.getById(2);
System.out.println(">>>结束第2次查询……");
System.out.println(user1);
System.out.println(user2);
// 情况3:同一个SqlSession,并且同一个查询条件,然后两次查询期间执行了任何一次增删改操作
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println(">>>开始第1次查询……");
User user1 = userMapper.getById(1);
System.out.println(">>>结束第1次查询……");
System.out.println("开始添加用户……");
User user = new User("灵儿", "123456", "女", 3);
userMapper.insert(user);
System.out.println("结束添加用户……");
System.out.println(">>>开始第2次查询……");
User user2 = userMapper.getById(1);
System.out.println(">>>结束第2次查询……");
System.out.println(user1);
System.out.println(user2);
// 情况4:同一个SqlSession,并且同一个查询条件,然后两次查询期间手动清空了缓存(例如:sqlSession.clearCache();)
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println(">>>开始第1次查询……");
User user1 = userMapper.getById(1);
System.out.println(">>>结束第1次查询……");
sqlSession.clearCache();
System.out.println(">>>开始第2次查询……");
User user2 = userMapper.getById(1);
System.out.println(">>>结束第2次查询……");
System.out.println(user1);
System.out.println(user2);
2、二级缓存
2.1、概念
-
二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession,然后通过该SqlSession创建的Mapper对象进行非首次查询时将使用缓存,当然首先需要满足二级缓存开启条件
-
二级缓存开启的条件
-
在核心配置文件中,设置全局配置属性
cacheEnabled="true"
,默认为true
,不需要设置 -
在映射文件中设置标签
<cache />
-
二级缓存必须在SqlSession 手动 关闭 或 提交 之后才生效,手动的意思也就是调用commit()或者close()方法,而不是使用自动提交方式,mybatis框架自动会做这件事
-
查询的数据所转换的实体类必须实现序列化接口
-
-
二级缓存失效的情况:
- 两次查询期间执行了任何一次增删改操作
2.2、单个xml文件中的cache标签使用说明
- 在mapper.xml配置文件中添加的cache标签可以设置一些属性
- eviction属性:缓存回收策略(默认值:LRU)
- LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。
- FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
- flushInterval属性:刷新间隔,单位毫秒(默认值:不设置)
- 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句(增删改)时刷新
- size属性:引用数目,正整数
- 代表缓存最多可以存储多少个对象,太大容易导致内存溢出
- readOnly属性:是否是只读缓存,也就是返回给调用者缓存对象还是缓存对象复制品(默认值:false)
- true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势,但是安全性大大降低,毕竟缓存对象存在被修改的风险,这样可能造成数据不准确,因此当能保证缓存对象不会修改的时候,在设置参数值为true
- false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false
- eviction属性:缓存回收策略(默认值:LRU)
2.3、演示二级缓存
说明:
通过用户id获取用户信息,同一个SqlSessionFactory,不同的sqlSession,同一个查询条件,进行两次查询
Mapper方法写法:
User getById(Integer id);
xml方法写法:
<!-- 开启二级缓存 -->
<cache />
<select id="getById" resultType="User">
select * from t_user where id = #{id}
</select>
调用Mapper方法的代码写法:
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
System.out.println(">>>开始第1次查询……");
User user1 = userMapper1.getById(1);
System.out.println(">>>结束第1次查询……");
// 手动提交
sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
System.out.println(">>>开始第2次查询……");
User user2 = userMapper2.getById(1);
System.out.println(">>>结束第2次查询……");
System.out.println(user1);
System.out.println(user2);
结果:
>>>开始第1次查询……
Cache Hit Ratio [com.atguigu.demo.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 1661123505.
==> Preparing: select * from t_user where id = ?
==> Parameters: 1(Integer)
<== Columns: id, userName, password, sex, age
<== Row: 1, 小李, 123456, 男, 3
<== Total: 1
>>>结束第1次查询……
>>>开始第2次查询……
Cache Hit Ratio [com.atguigu.demo.mapper.UserMapper]: 0.5
>>>结束第2次查询……
User(id=1, userName=小李, password=123456, sex=男, age=3)
User(id=1, userName=小李, password=123456, sex=男, age=3)
分析:
可以看到第一次查询的时候访问了数据库,而第二次查询没有访问数据库,说明第二次查询使用了二级缓存
2.4、演示二级缓存失效的四种情况
说明: 二级缓存失效,将导致每次都查询数据库
演示代码如下:
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
System.out.println(">>>开始第1次查询……");
User user1 = userMapper1.getById(1);
System.out.println(">>>结束第1次查询……");
// 添加操作
User user = new User("小青", "123456", "女", 3);
userMapper1.insert(user);
// 手动提交
sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
System.out.println(">>>开始第2次查询……");
User user2 = userMapper2.getById(1);
System.out.println(">>>结束第2次查询……");
System.out.println(user1);
System.out.println(user2);
3、Mybatis缓存查询顺序
- 二级缓存(如果没有命中,继续向下查找)
- 一级缓存(如果没有命中,继续向下查找)
- 数据库
另外:如果二级缓存开启,当SqlSession手动关闭 / 提交 之后,一级缓存中的数据才会写入二级缓存
4、整合第三方缓存EHCache
4.1、说明
Mybatis官方二级缓存使用内存来存储数据,但是内存比较小,最好使用磁盘,而第三方缓存EHCache就是使用磁盘来存储数据
4.2、添加依赖
<!-- Mybatis EHCache整合包 -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
<!-- slf4j日志门面的一个具体实现 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
4.3、EHCache配置文件说明
属性名 | 是否必须 | 作用 |
---|---|---|
maxElementsInMemory | 是 | 在内存中缓存的element的最大数目 |
maxElementsOnDisk | 是 | 在磁盘上缓存的element的最大数目,若是0表示无穷大 |
eternal | 是 | 设定缓存的elements是否永远不过期。 如果为true,则缓存的数据始终有效, 如果为false那么还要根据timeToIdleSeconds、timeToLiveSeconds判断 |
overflowToDisk | 是 | 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上 |
timeToIdleSeconds | 否 | 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时, 这些数据便会删除,默认值是0,也就是可闲置时间无穷大 |
timeToLiveSeconds | 否 | 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大 |
diskSpoolBufferSizeMB | 否 | DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区 |
diskPersistent | 否 | 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false |
diskExpiryThreadIntervalSeconds | 否 | 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s, 相应的线程会进行一次EhCache中数据的清理工作 |
memoryStoreEvictionPolicy | 否 | 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。 默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出 |
4.4、配置方法
4.4.1、在resources下创建EHCache配置文件ehcache.xml
注意: 名字必须叫ehcache.xml
案例如下:
<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="D:\atguigu\ehcache"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
4.4.2、设置二级缓存实现类为EhcacheCache
在在xxxMapper.xml文件中设置二级缓存类型,需要添加以下内容到mapper标签内容中,如下:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
4.4.3、添加logback.xml
注意: 存在SLF4J时,作为简易日志的log4j将失效,此时我们需要借助SLF4J的具体实现logback来打印日志。创建logback的配置文件logback.xml,名字固定,不可改变,其实我尝试过了,可换可不换,根据需要来吧!
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- 指定日志输出的位置 -->
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 日志输出的格式 -->
<!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
<pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
</encoder>
</appender>
<!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
<!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
<root level="DEBUG">
<!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
<appender-ref ref="STDOUT" />
</root>
<!-- 根据特殊需求指定局部日志级别 -->
<logger name="com.atguigu.crowd.mapper" level="DEBUG"/>
</configuration>
七、Mybatis逆向工程
1、正向工程和逆向工程的区别
- 正向工程:先创建Java实体类,由框架负责根据实体类生成数据库表。Hibernate是支持正向工程的
- 逆向工程:先创建数据库表,然后框架使用数据库表反向生成如下资源:
- Java实体类
- Mapper接口
- Mapper映射文件
2、创建逆向工程的步骤
2.1、添加依赖和插件
注意: 项目所用mysql版本和插件所用mysql版本号必须一致
依赖、插件配置如下:
<!-- 注意:必须有MyBatis核心依赖包 和 MySQL驱动,如果你的依赖中已经存在了,那就不用在添加了 -->
<dependencies>
<!-- MyBatis核心依赖包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- log4j日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 注意:插件中用到的mysql-connector-java依赖版本必须和上面的mysql依赖版本一致 -->
<!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.0</version>
<!-- 插件的依赖 -->
<dependencies>
<!-- 逆向工程的核心依赖 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.2</version>
</dependency>
<!-- MySQL驱动(注意:此处版本和上面dependencies标签中的mysql驱动版本要一致,如果你用你自己的驱动,那就需要设置一个这个版本是否和上面的mysql依赖版本一致) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
2.2、添加generatorConfig.xml到resources下面
注意: 配置文件名称必须叫generatorConfig.xml
,并放在resources
目录下面;
编写下面配置文件时,只需要注意这几点:targetRuntime版本选择、驱动类全限定类名、用户名、密码、链接、targetPackage前面的目录路径、tableName表名、domainObjectName实体类名称
举例如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!--
targetRuntime: 执行生成的逆向工程的版本
MyBatis3Simple: 生成基本的CRUD(清新简洁版,只有最简单的增删改查方法)
MyBatis3: 生成带条件的CRUD(奢华尊享版,可以通过方法来组装sql语句,类似于Mybatis-plus,可以通过QBC(query by confitional)查询,其中mall商城项目就是使用这种方式生成的数据结果)
-->
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 配置驱动数据库的连接信息 -->
<!-- 配置驱动;如果插件配置的mysql版本是5.X,driverClass值是com.mysql.jdbc.Driver,如果mysql版本是8.X,driverClass值是com.mysql.cj.jdbc.Driver -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/20220807test"
userId="root"
password="123456">
</jdbcConnection>
<!-- 实体类生成策略-->
<javaModelGenerator targetPackage="com.atguigu.demo.pojo" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- SQL映射文件的生成策略 -->
<sqlMapGenerator targetPackage="com.atguigu.demo.mapper"
targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<!-- Mapper接口的生成策略 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.atguigu.demo.mapper" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<!-- 逆向分析的表 -->
<!-- tableName设置为*号,可以对应所有表,此时不写domainObjectName -->
<!-- domainObjectName属性指定生成出来的实体类的类名,然后Mapper接口和Mapper映射文件名都会根据domainObjectName改变,不会受到前缀的影响 -->
<table tableName="t_user" domainObjectName="User"/>
</context>
</generatorConfiguration>
2.3、点击Maven插件,生成项目结构,如果没有该插件,可以点击下图左上角的刷新按钮
3、演示奢华尊享版逆向工程插件生成的项目如何使用
3.1、查询
- selectByExample:按条件查询,需要传入一个example对象或者null;如果传入一个null,则表示没有条件,也就是查询所有数据
- example.createCriteria().xxx:创建条件对象,通过andXXX方法为SQL添加查询添加,每个条件之间是and关系
- example.or().xxx:将之前添加的条件通过or拼接其他条件
举例:
@Test
public void testMBG() throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
EmpExample example = new EmpExample();
//名字为张三,且年龄大于等于20
example.createCriteria().andEmpNameEqualTo("张三").andAgeGreaterThanOrEqualTo(20);
//或者did不为空
example.or().andDidIsNotNull();
List<Emp> emps = mapper.selectByExample(example);
emps.forEach(System.out::println);
}
3.2、增改
-
updateByPrimaryKey:通过主键进行数据修改,如果某一个值为null,也会将对应的字段改为null
mapper.updateByPrimaryKey(new Emp(1,"admin",22,null,"456@qq.com",3));
-
updateByPrimaryKeySelective():通过主键进行选择性数据修改,如果某个值为null,则不修改这个字段
mapper.updateByPrimaryKeySelective(new Emp(2,"admin2",22,null,"456@qq.com",3));
八、Mybatis分页插件
1、使用步骤
1.1、添加依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
1.2、在核心配置文件中配置分页插件
- 在MyBatis的核心配置文件(mybatis-config.xml)中配置插件
<plugins>
<!--设置分页插件-->
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
示例截图如下:
2、使用方法
2.1、开启分页功能
- 在查询List集合之前使用PageHelper.startPage(int pageNum, int pageSize)开启分页功能
- pageNum:当前页的页码
- pageSize:每页显示的条数
- 常用数据(参数或者返回值):
- pageNum:当前页的页码
- pageSize:每页显示的条数
- size:当前页显示的真实条数
- total:总记录数
- pages:总页数
- prePage:上一页的页码
- nextPage:下一页的页码
- isFirstPage/isLastPage:是否为第一页/最后一页
- hasPreviousPage/hasNextPage:是否存在上一页/下一页
- navigatePages:导航分页的页码数
- navigatepageNums:导航分页的页码,[1,2,3,4,5]
2.2、查询普通分页数据
代码:
@Test
public void testPageHelper() throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
// 注意:分页代码从此处开始
// 当前页码是1,页码容量是4
Page<Object> page = PageHelper.startPage(1, 4);
// 查询集合
List<Emp> emps = mapper.selectByExample(null);
// 获取分页结果
System.out.println(page);
}
结果:
Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=8, pages=2, reasonable=false, pageSizeZero=false}[Emp{eid=1, empName='admin', age=22, sex='男', email='456@qq.com', did=3}, Emp{eid=2, empName='admin2', age=22, sex='男', email='456@qq.com', did=3}, Emp{eid=3, empName='王五', age=12, sex='女', email='123@qq.com', did=3}, Emp{eid=4, empName='赵六', age=32, sex='男', email='123@qq.com', did=1}]
2.3、查询带导航分页的分页数据
说明:
- 在查询获取list集合之后,使用PageInfo pageInfo = new PageInfo<>(List list, intnavigatePages)获取分页相关数据
- list:分页之后的数据
- navigatePages:导航分页的页码数
代码:
@Test
public void testPageHelper() throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
// 注意:分页代码从此处开始
// 当前页码是1,页码容量是4
PageHelper.startPage(1, 4);
// 查询list集合
List<Emp> emps = mapper.selectByExample(null);
// emps中的数据 等同于 方法一中直接输出的page数据,而5是导航分页的展示页总数,根据emps中携带的数据总量、当前页码、页面容量、导航分页数量进行计算,然后得到其他可用信息(比如当前页是否是首页、末页、是否有上一页、下一页、导航分页页码数集合等)
PageInfo<Emp> page = new PageInfo<>(emps,5);
System.out.println(page);
}
结果:
PageInfo{
pageNum=1, pageSize=4, size=4, startRow=1, endRow=4, total=8, pages=2,
list=Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=8, pages=2, reasonable=false, pageSizeZero=false}[Emp{eid=1, empName='admin', age=22, sex='男', email='456@qq.com', did=3}, Emp{eid=2, empName='admin2', age=22, sex='男', email='456@qq.com', did=3}, Emp{eid=3, empName='王五', age=12, sex='女', email='123@qq.com', did=3}, Emp{eid=4, empName='赵六', age=32, sex='男', email='123@qq.com', did=1}],
prePage=0, nextPage=2, isFirstPage=true, isLastPage=false, hasPreviousPage=false, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=2, navigatepageNums=[1, 2]}