MyBatisPlus
- MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提高效率
- 官网:https://mp.baomidou.com/
- MyBatisPlus特性
- 无侵入:只做增强不做改变,不会对现有工程产生影响
- 强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作
- 支持 Lambda:编写查询条件无需担心字段写错
- 支持主键自动生成
- 内置分页插件
- …
快速入门
注意:此处只进行不同步骤演示,具体搭建步骤可详见SpringBoot完整技术汇总
后续小节的代码示例均在该快速入门的基础上演示,所以只进行关键步骤演示
-
Step1: 创建一个SpringBoot整合MyBatisPlus的项目
MbPlusDemo
-
由于该入门仅为演示MyBatisPlus,所以创建的SpringBoot项目并不是web项目,所以并未添加Spring Web起步依赖
注意:SpringBoot整合MyBatisPlus的项目与创建SpringBoot整合MyBatis的项目过程相同,区别就是:
整合MyBatisPlus项目只用添加一个MySQL Driver起步依赖即可,如图所示
因为后期在pom.xml文件中导入的MyBatisPlus坐标中包含了MyBatis的坐标
-
-
Step2: 创建数据库mbplus并在该库中创建表user 并使IDEA与数据库建立连接 ,SQL代码如下
-- 创建mbplus数据库 CREATE DATABASE IF NOT EXISTS mbplus CHARACTER SET utf8;-- 使用mbplus数据库 USE mbplus;DROP TABLE IF EXISTS user;CREATE TABLE IF NOT EXISTS user (id bigint(20) primary key auto_increment,name varchar(32) not null,password varchar(32) not null,age int(3) not null ,tel varchar(32) not null ); insert into user values(1,'Tom','tom',3,'13346355023'); insert into user values(2,'Jerry','jerry',4,'16688336995'); insert into user values(3,'Jock','123456',41,'17725996338'); insert into user values(4,'李四','nigger',15,'18879566321');SELECT * FROM user;
-
Step3: 在pom.xml文件中添加数据源坐标依赖(博主以Druid为例)以及MyBatisPlus的坐标依赖
注意:若不添加,则会使用SpringBoot的默认数据源。此处根据实际情况来决定是否添加数据源。博主使用druid数据源
此处只给出相关的两个数据源坐标,可自行选择使用
<!--druid坐标--> <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.18</version> </dependency> <!--c3p0坐标 <dependency><groupId>com.mchange</groupId><artifactId>c3p0</artifactId><version>0.9.5.5</version> </dependency> --> <!--MyBatisPlus坐标--> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version> </dependency>
-
Step4: 将配置文件application.properties改为application.yml格式的配置文件,并在该配置文件中配置数据库连接信息、数据源信息以及MyBatisPlus的运行日志
注意:
配置MyBatisPlus的运行日志在此处写出来只是为了演示,后续可根据实际情况进行配置,在该快速入门中博主是未进行该配置的。
若配置了MyBatisPlus的运行日志,则在控制台会输出对应的MyBatis日志,该配置的使用具体可详见分页查询部分内容的示例
spring:# 配置数据库连接信息以及数据源信息datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mbplus?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456#mybatis-plus: # configuration:# 配置MyBatisPlus的运行日志 # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
-
Step5: 创建一个与三层架构包同级的pojo包,并在该包下创建实体类
User
,代码如下package at.guigu.pojo;public class User {private Long id;private String name;private String password;private Integer age;private String tel;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}public String getTel() {return tel;}public void setTel(String tel) {this.tel = tel;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +", password='" + password + '\'' +", age=" + age +", tel='" + tel + '\'' +'}';} }
-
Step6: 创建持久层dao包,并在该包下创建继承
BaseMapper<T>
接口的UserDao
接口,且泛型为对应的实体类的名称注意:此时持久层代码就已完全实现,因为MyBatisPlus底层已经封装了一系列的增删改查语句,后续只需调用就可以了
package at.guigu.dao;import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper;@Mapper public interface UserDao extends BaseMapper<User> { }
-
Step7: 在test包下创建dao包并在该包下创建测试类
DaoTest
,代码如下:package at.guigu.dao;import at.guigu.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List;@SpringBootTest public class DaoTest {@Autowiredprivate UserDao userDao;// 获取所有数据@Testpublic void test1() {List<User> users = userDao.selectList(null);for (User user : users) {System.out.println(user);}}// 获取指定id的数据@Testpublic void test2() {List<User> users = userDao.selectByIds(List.of(1L, 2L, 3L));users.forEach(System.out::println);} }
运行测试代码后,截图如下
-
Mockito警告解决,如上图所示,测试后会爆出Mockito警告:
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build what is described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3
,若想对该警告进行处理则在pom.xml文件中导入以下插件<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version><configuration><argLine>-javaagent:"${settings.localRepository}/org/mockito/mockito-core/5.14.2/mockito-core-5.14.2.jar"</argLine></configuration> </plugin>
此时运行截图如下:
-
由以上运行截图可知,仍有一个警告:
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
,原因是IDEA在启动调试器时,可能会尝试使用一些优化设置,比如类数据共享。但是,如果JVM的配置不正确,或者IDEA与特定版本的JDK不完全兼容,就可能出现这个警告。解决方式如图所示:此时运行后截图如下,无任何警告:
实体类简化
-
在上述实体类
User
中,需要写get/set
、toString
等方法,在实际项目开发中这就很浪费时间,所以在此可进行简化,来让系统自动生成对应的一系列方法,步骤如下: -
Step1: 在pom.xml文件中导入聚合工程的聚合工程(即父工程的父工程)
spring-boot-dependencies
的pom.xml文件中的可选依赖lombok
,此时完整的pom.xml文件代码如下<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.0</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>MbPlusDemo</artifactId><version>0.0.1-SNAPSHOT</version><name>MbPlusDemo</name><description>MbPlusDemo</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>21</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--lombok坐标--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--============================外部导入的坐标===========================--><!--druid坐标--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.18</version></dependency><!--MyBatisPlus坐标--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version><configuration><argLine>-javaagent:"${settings.localRepository}/org/mockito/mockito-core/5.14.2/mockito-core-5.14.2.jar"</argLine></configuration></plugin></plugins></build></project>
-
Step2: 更改实体类
User
代码,更改后的代码如下package at.guigu.pojo; import lombok.Data;@Data public class User {private Long id;private String name;private String password;private Integer age;private String tel; }
-
lombok
依赖相关注解解释如下此处只进行关键注解示例,其余注解可自行探索
注解 解释 @Setter
自动为所有字段生成 setter 方法 @Getter
自动为所有字段生成 getter 方法 @ToString
自动生成 toString()
方法,输出类的字段名称和值。@EqualsAndHashCode
自动生成 equals()
和hashCode()
方法,通常用于对象比较和哈希集合@RequiredArgsConstructor
自动生成一个构造函数,该构造函数接受所有 final
字段和@NonNull
注解字段。@Data
该注解融合了以上5个注解 @NoArgsConstructor
自动生成一个无参构造函数 @AllArgsConstructor
自动生成一个包含类中所有字段的构造函数
控制台简化
在实际项目运行过程中可能会有很多日志(图中红色框线)以及banner(图中紫色框线),我们可以将其消除掉,步骤如下
-
Step1: 在源代码配置文件目录(即资源文件
resources
)下创建logback.xml文件,代码如下消除红色框线部分日志
<?xml version="1.0" encoding="UTF-8" ?> <!--运行项目时不显示的日志--> <configuration></configuration>
-
Step2: 在application.yml配置文件中来配置banner,代码如下
spring:# 配置数据库连接信息以及数据源信息datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mbplus?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456main:# 设置SpringBoot的banner不显示(即运行时不显示SpringBoot图标)banner-mode: offmybatis-plus:global-config:# 设置MyBatis的banner不显示(即运行时不显示MyBatis图标)banner: false# 配置MyBatisPlus的运行日志------注意:可根据实际情况自行决定是否配置MyBatis的运行日志 #mybatis-plus: # configuration: # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
此时运行截图如下
实体类常见注解
- MyBatisPlus是根据PO实体的信息来推断出表的信息,从而生成SQL语句,默认情况下:
- MybatisPlus会把PO实体的类名驼峰转下划线作为表名
- 比如:实体类名为:userTable,此时MyBatisPlus会自动将实体类名转化为user_table,然后去数据库中寻找名为user_table的表
- MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
- 比如:实体类中有个属性为userName,此时MyBatisPlus会自动将实体类的属性userName转化为user_name,然后去数据库的对应表中寻找名为user_name的字段
- MybatisPlus会把实体类中名为id的属性默认作为主键
- MybatisPlus会把PO实体的类名驼峰转下划线作为表名
以上三种情况使得MyBtisPLus给我们的开发带来了便捷,但是实际项目开发中,不可能与MyBatisPlus的规范完全一致,因此MyBatisPlus给我们提供了注解来方便我们的使用
@TableName
-
解释:表名注解,指明当前实体类所对应的数据库的表名
-
使用位置:实体类类名上面
-
示例:表明当前实体类User对应的是数据库中的tb_user表
@TableName("tb_user") public class User {private Long id;private String name; }
-
@TableName
注解属性属性 类型 必须指定 默认值 描述 value String 否 “” 表名 schema String 否 “” schema keepGlobalPrefix boolean 否 false 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) resultMap String 否 “” xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) autoResultMap boolean 否 false 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) excludeProperty String[] 否 {} 需要排除的属性名 @since 3.3.1
@TableId
-
解释:主键注解;用于标记实体类中的主键字段
-
使用位置:实体类中属性之上
-
示例
@TableName("tb_user") public class User {@TableId(value="id", type=IdType.NONE)private Long id;private String name; }
-
@TableId
注解属性属性 类型 必须指定 默认值 描述 value
String 否 “” 表名 type
Enum 否 IdType.NONE
指定主键类型 -
IdType
的枚举值IdType
的枚举值解释 AUTO(0)
(默认)自动选择主键生成策略,默认策略。通常在数据库支持自增主键的情况下使用 NONE(1)
没有主键生成策略(即不设置主键生成策略)。这种方式一般用于主键由应用程序生成的场景,MyBatis-Plus 不会尝试处理主键的生成 INPUT(2)
必须手动输入主键值。在主键无自增策略时,主键的值由程序员在插入时手动提供,MyBatis-Plus 不会自动生成, ASSIGN_ID(3)
可利用Snowflake雪花 算法生成ID(可兼容数值型与字符串型)。 ASSIGN_UUID(4)
以UUID生成算法作为id生成策略 -
注意:
AUTO
在主键有自增策略的情况下使用INPUT
、ASSIGN_ID
、ASSIGN_UUID
通常在主键无自增策略时使用- 在主键无自增策略的情况下,若使用
ASSIGN_ID
,则情况有2种:- 若手动输入主键值,则以手动输入的为准
- 若未手动输入主键值,则MyBatisPlus会利用Snowflake雪花算法自动生成ID
- 当主键需要为 UUID 类型且无自增策略时,使用
ASSIGN_UUID
@TableField
-
解释:普通字段注解;标记属性是否是表中的字段及哪个字段;一般特殊的字段才需要这样标记。
-
使用位置:实体类属性之上
-
示例
@TableName("tb_user") public class User {@TableId(value="id", type=IdType.NONE)private Long id;@TableField("username")private String name;private Integer age;@TableField("is_married")private Boolean isMarried;@TableField("`order`")private Integer order;@TableField(exist=false)private String address; }
-
一般情况下我们并不需要给字段添加
@TableField
注解,一些特殊情况除外:- 成员变量名与数据库字段名不一致
- 成员变量是以
isXXX
命名,按照JavaBean
的规范,MybatisPlus
识别字段时会把is
去除,这就导致与数据库不符。- 比如:此时成员变量名为isMarried,字段名为is_married。按正常逻辑来说,MybatisPlus会把PO实体的变量名驼峰转下划线作为表的字段名(即变为is_married);但是MyBatisPlus会默认将所有以is开头的成员变量名中的is给去除掉,所以就会导致其与字段名不匹配,所以此时必须加上
@TableField
注解
- 比如:此时成员变量名为isMarried,字段名为is_married。按正常逻辑来说,MybatisPlus会把PO实体的变量名驼峰转下划线作为表的字段名(即变为is_married);但是MyBatisPlus会默认将所有以is开头的成员变量名中的is给去除掉,所以就会导致其与字段名不匹配,所以此时必须加上
- 成员变量名与数据库一致,但是与数据库的关键字冲突。使用
@TableField
注解给字段名添加飘号 ` 转义 - 成员变量不是数据库对应表中的字段
-
@TableField
注解属性属性 类型 必填 默认值 描述 value
String
否 “” 数据库字段名 exist
boolean
否 true 指定对应字段是否为数据库表中的字段 condition
String
否 “” 自定义字段的查询条件,默认由全局配置决定 update
String
否 “” 用于自定义字段在执行更新操作时的 SQL 表达式(即字段 update set 部分注入),例如:当在version字段上注解update=“%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) insertStrategy
Enum
否 FieldStrategy.DEFAULT 定义字段的插入策略,即是否参与 INSERT
操作updateStrategy
Enum
否 FieldStrategy.DEFAULT 定义字段的更新策略,即是否参与 UPDATE
操作whereStrategy
Enum
否 FieldStrategy.DEFAULT 定义该字段在构建查询条件(即 WHERE
子句)时的策略fill
Enum
否 FieldFill.DEFAULT 指定字段的自动填充策略 select
boolean
否 true 指定当前字段是否参与查询 keepGlobalFormat
boolean
否 false 是否保持使用全局的 format 进行处理 jdbcType
JdbcType
否 JdbcType.UNDEFINED 指定字段对应的 JDBC 类型,默认情况话会根据字段类型自动推断 typeHandler
TypeHander
否 指定字段的自定义类型处理器(当字段类型需要特殊处理时使用) numericScale
String
否 “” 指定小数点后保留的位数 -
update
- 用于自定义字段在执行更新操作时的 SQL 表达式
- 使用占位符
%s
来代表字段名
-
插入策略、更新策略、查询策略可选值有
-
FieldStrategy.NOT_NULL
:仅当字段值不为null
时,才将其作为查询条件的一部分。 -
FieldStrategy.NOT_EMPTY
:仅当字段值不为空(即不为null
且不为空字符串)时,才将其作为查询条件的一部分。 -
FieldStrategy.IGNORED
:无论字段值是否为null
或为空,始终将其作为查询条件的一部分。 -
FieldStrategy.DEFAULT
(默认值):使用全局配置的策略,通常在 MyBatis-Plus 的全局配置中指定
-
MyBatisPlus相关常见配置
-
MyBatisPlus的配置项继承了MyBatis原有的配置和一些自己特有的配置,比如:
- 实体类的别名扫描包—即设置别名
- 全局id类型—即id生成策略
- mapper文件地址—即引入持久层包下所有接口对应的SQL映射文件
-
可能用到的配置示例如下:
mybatis-plus:configuration:# 配置MyBatisPlus的运行日志log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 配置默认的枚举类型处理器default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler# 是否开启下划线和驼峰映射, 默认为truemap-underscore-to-camel-case: true# 是否开启二级缓存,默认为falsecache -enabled: false# 实体类的别名扫描包---即设置别名type-aliases-package: at.guigu.domain.po# mapper文件地址---即引入持久层包下所有接口对应的SQL映射文件# 默认路径即为classpath*:/mapper/**/*.xmlmapper-locations: "classpath*:/mapper/**/*.xml"global-config:# 设置MyBatis的banner不显示(即运行时不显示MyBatis图标)banner: falsedb-config:# 全局id类型---即id生成策略id-type: auto# 更新策略,默认为not_nullupdate-strategy: not_null# 设置数据库表名前缀table-prefix: tbl_# 逻辑删除字段名logic-delete-field: deleteed# 逻辑未删除值(即默认值)logic-not-delete-value: 0# 逻辑删除值logic-delete-value: 1
-
mapper文件地址默认即为:
classpath*:/mapper/**/*.xml
,也就是说- 只要把SQL映射文件放置这个目录下就一定会被加载
- 若SQL映射文件不在该目录下则需要配置mapper文件地址;反之则不需要
持久层相关的接口
BaseMapper
接口
-
在日常进行SQL语句编写过程中,除了新增以外,修改、删除、查询的SQL语句都需要指定where条件,而持久层所继承的
BaseMapper
接口中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。 -
BaseMapper<T>
接口的全部CRUD方法如下图所示-
新增相关的方法
-
删除相关方法
-
修改/更新相关方法
-
查询相关方法
-
判断是否存在的相关方法
-
常用方法如下
BaseMapper<T>
抽象类的方法解释 int insert(T entity)
新增 int deleteById(Serializable id)
根据id删除 int updateById(@Param("et") T entity)
更新/修改 T selectById(Serializable id)
根据id查询 List<T> selectList(@Param("ew") Wrapper<T> queryWrapper)
按条件查询,当参数为 null
时,表示查询所有数据<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper)
按条件分页查询
-
-
从以上截图可以看出,BaseMapper接口中复杂的where条件均是通过
Wrapper
抽象类来实现的
Wrapper<T>
抽象类
-
作用:封装SQL的条件语句
-
Wrapper<T>
是条件构造的抽象类,其有很多默认实现,继承关系如下-
常用的抽象类及其子类如下
-
-
主要用到的子类
主要用到的子类 解释 QueryWrapper
专门用于构造查询条件。它允许你以链式调用的方式添加多个查询条件,并且可以组合使用 and
和or
逻辑UpdateWrapper
专门用于构造更新条件,可以在更新数据时指定条件。与 QueryWrapper
类似,它也支持链式调用和逻辑组合。使用UpdateWrapper
可以在不创建实体对象的情况下,直接设置更新字段和条件LambdaQueryWrapper
这是一个基于 Lambda 表达式的查询条件构造器,它通过 Lambda 表达式来引用实体类的属性,从而避免了硬编码字段名。这种方式提高了代码的可读性和可维护性,尤其是在字段名可能发生变化的情况下 LambdaUpdateWrapper
类似于 LambdaQueryWrapper
,LambdaUpdateWrapper
是基于 Lambda 表达式的更新条件构造器。它允许你使用 Lambda 表达式来指定更新字段和条件,同样避免了硬编码字段名的问题 -
实际项目中 条件查询主要使用的是
QueryWrapper
和LambdaQueryWrapper
这两个子类,区别:- 类型安全:
LambdaQueryWrapper
更加类型安全,因为它使用 Lambda 表达式来引用字段名,而QueryWrapper
直接使用字符串表示字段名,容易发生拼写错误 - 编译时检查:
LambdaQueryWrapper
支持编译时检查,QueryWrapper
仅支持运行时检查 - 字段引用方式:
LambdaQueryWrapper
使用 Lambda 表达式引用实体类的字段,QueryWrapper
直接通过字符串指定字段。
- 类型安全:
AbstractWrapper
抽象类
-
Wrapper
的抽象子类AbstractWrapper
提供了where中包含的所有条件方法:在idea中
crtl+F12
即可查看接口的全部方法下图只列举了部分条件方法
-
常用方法如下
条件查询方法 解释 eq
=
ne
!=
lt
<
le
<=
gt
>
ge
>=
like
LIKE
notlike
NOT LIKE
between
BETWEEN
in
IN
isNull
判断字段是否为NULL 其余方法可详见官网指南中的条件构造器
QueryWrapper
和UpdateWrapper
-
AbstractWrapper
抽象类有两个子类:QueryWrapper
和UpdateWrapper
-
QueryWrapper
在AbstractWrapper
抽象类的基础上拓展了一个select方法,允许查询指定的字段-
代码示例1
@Test public void test3() {//1、构造查询条件;构造 where username like '%o%' and balance >= 10QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.select(true, "id", "name", "password").like("name", "o").ge("age", 10);//2、查询List<User> users = userDao.selectList(queryWrapper);users.forEach(System.out::println); }
-
代码示例2
/*** 更新用户名为Tom的用户的年龄为10 */ @Test public void test4() {//1、构造查询条件;构造 where username = 'Tom'QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("name", "Tom");// user中非null的字段,会进行set操作User user = new User();user.setage(10);//2、修改;只更新非空字段userDao.update(user, queryWrapper); }
-
-
UpdateWrapper
在AbstractWrapper
抽象类的基础上拓展了一个set
方法,允许指定SQL中的SET部分:-
代码示例
/*** 更新id为1,2,4的用户的年龄,减1 */ @Test public void test5(){//1、构造更新条件对象UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();// set age = age - 1updateWrapper.setSql("age = age - 1");// where id in (1,2,4)updateWrapper.in("id", 1, 2, 4);//2、更新userDao.update(null, updateWrapper); }
-
LambdaQueryWrapper
和LambdaUpdateWrapper
-
在
QueryWrapper
和UpdateWrapper
中,QueryWrapper
直接使用字符串表示字段名,容易发生拼写错误。所以我们采用LambdaQueryWrapper
方法,因为它使用 Lambda 表达式来引用字段名,这样更加安全 -
QueryWrapper
的示例更改如下-
示例1
@Test public void test3() { //1、构造查询条件;构造 where username like '%o%' and balance >= 10LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.select(true, User::getId, User::getName, User::getPassword).like(User::getName, "o").ge(User::getAge, 10);//2、查询List<User> users = userDao.selectList(lambdaQueryWrapper);users.forEach(System.out::println); }
-
示例2
/*** 更新用户名为Tom的用户的年龄为10 */ @Test public void test4() {//1、构造查询条件;构造 where username = 'Tom'LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(User::getName, "Tom");// user中非null的字段,会进行set操作User user = new User();user.setage(10);//2、修改;只更新非空字段userDao.update(user, lambdaQueryWrapper); }
-
-
UpdateWrapper
示例更改如下/*** 更新id为1,2,4的用户的年龄,减1 */ @Test public void test5(){//1、构造更新条件对象LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();// set age = age - 1lambdaUpdateWrapper.setSql("age = age - 1");// where id in (1,2,4)lambdaUpdateWrapper.in(User::getId, List.of(1L, 2L, 4L));//2、更新userDao.update(null, lambdaUpdateWrapper); }
自定义拼接SQL语句
-
在UpdateWrapper的示例中,我在代码中编写了更新的SQL语句,如下:
/*** 更新id为1,2,4的用户的年龄,减1 */ @Test public void test5(){//1、构造更新条件对象LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();// set age = age - 1lambdaUpdateWrapper.setSql("age = age - 1");// where id in (1,2,4)lambdaUpdateWrapper.in(User::getId, List.of(1L, 2L, 4L));//2、更新userDao.update(null, lambdaUpdateWrapper); }
这样在实际项目开发中是不允许存在的 ,所以MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml拼接SQL。步骤如下:
-
Step1: 基于Wrapper接口构建where条件,然后调用在持久层接口中自定义的更新方法,并传入更新数值与查询条件对象
/*** 更新id为1,2,4的用户的年龄,减1 */ @Test public void test5(){//1、构造更新条件对象LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();// where id in (1,2,4)lambdaQueryWrapper.in(User::getId, List.of(1L, 2L, 4L));//2、更新:调用在持久层接口中自定义的更新方法,传入更新数值与查询条件对象userDao.updateAgeByWrapper(1, lambdaQueryWrapper) }
-
Step2: 在持久层UserDao接口中写入自定义的更新方法
updateBalanceByWrapper
- Step2-1: 将Wrapper接口对应的子类作为参数传入该自定义的更新方法,然后用
@Param("ew")
注解修饰(注意:此时该注解属性值必须为ew
)
void updateAgeByWrapper(@Param("val") int val, @Param("ew") LambdaQueryWrapper<User> lambdaQueryWrapper);
- Step2-1: 将Wrapper接口对应的子类作为参数传入该自定义的更新方法,然后用
-
Step3: 写出SQL语句------此处有两种形式:
-
利用注解形式
@Update("UPDATE user SET age = age - #{val} ${ew.customSqlSegment}") void updateAgeByWrapper(@Param("val") int val, @Param("ew") LambdaQueryWrapper<User> lambdaQueryWrapper);
-
注意:上述的执行语句中
ew
及customSqlSegment
均为固定形式,都不能修改;1、第一个参数为要减去的值
2、第二个参数为查询条件对象:相当于对要执行的语句进行了语句的拼接
3、
${ew.customSqlSegment}
可以使用在注解中,也可以使用在 Mapper.xml文件中进行SQL语句的拼接
-
-
利用SQL映射文件形式 :在源代码配置文件目录(即资源文件
resources
)下创建持久层对应的目录,然后在该目录下创建UserDao.xml映射文件,该文件代码如下<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="at.guigu.dao.UserDao"><select id="updateAgeByWrapper" resultType="user">UPDATE userSET age = age - #{val}${ew.customSqlSegment}</select> </mapper>
-
多表联合查询
- MyBatisPlus在使用过程中,只适用于单表查询,对于多表查询就没办法了,但是我们可以利用MyBatisPlus的Wrapper接口来封装多表查询的查询条件
需求:查询出所有收货地址在北京且用户id在指定范围的用户(例如:1、2、4)
-
Step1: 假设有如下两个数据库表user、address
-
Step2: 创建测试类代码如下:
public void test4() {// 模拟前端接收到的数据List<Long> ids = List.of(1L, 2L, 4L);String city = "北京";// 构造查询条件QueryWrapper<User> queryWrapper = new QueryWrapper<>().in("user.id", ids).eq("address.city", "北京");userDao.selectMultiTable(queryWrapper); }
-
Step3: 在持久层UserDao接口中写入自定义的查询方法
List<User> selectMultiTable(@Param("ew") LambdaQueryWrapper<User> lambdaQueryWrapper);
-
Step4: 写出SQL语句------此处有两种形式:
-
利用注解形式
@Select("select id, name, password, age, tel from user INNER JOIN address ON user.id=address.user_id ${ew.customSqlSegment}") List<User> selectMultiTable(@Param("ew") LambdaQueryWrapper<User> lambdaQueryWrapper);
-
利用SQL映射文件形式 :在源代码配置文件目录(即资源文件
resources
)下创建持久层对应的目录,然后在该目录下创建UserDao.xml映射文件,该文件代码如下<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="at.guigu.dao.UserDao"><select id="selectMultiTable" resultType="user">SELECT id, name, password, age, tel FROM userINNER JOIN addressON user.id=address.user_id${ew.customSqlSegment}</select> </mapper>
-
MyBatis标准CURD制作涉及的接口
IPage<T>
接口
-
IPage<T>
通过源码可知其是一个接口,-
该接口是继承Serializable接口的子接口,代表该接口可以被序列化和反序列化
Serializable接口内容可详见javese基础部分内容
-
该接口实现类为
Page
,如图所示,也就是说若需要该接口的对象时可通过该实现类来创建
可通过
crtl+h
来查看接口的实现类及其子接口 -
-
该接口中常用的方法
方法 解释 long getCurrent()
获取当前页码值 long getSize()
获取每页显示的数据数量 long getPages()
获取总页数 long getTotal()
获取总数据数量 List<T> getRecords()
获取分页查询结果中的数据列表,也就是当前页的数据集合
DQL查询
分页查询
本示例在快速入门的项目上基准进行演示
-
Step1: 创建测试类
TestOne
,代码如下package at.guigu.dao;import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;/*** 测试按条件分页查询*/ @SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;@Testpublic void test1() {IPage<User> page = new Page<>(1, 2);userDao.selectPage(page, null);System.out.println("当前页码值为:" + page.getCurrent());System.out.println("每页显示的数据数量为:" + page.getSize());System.out.println("总页数为" + page.getPages());System.out.println("总数据数量为" + page.getTotal());System.out.println("当前页的数据集合为:" + page.getRecords());}@Testpublic void test2() {Page<User> page = Page.of(1, 2);Page<User> p = userDao.selectPage(page, null);System.out.println("当前页码值为:" + p.getCurrent());System.out.println("每页显示的数据数量为:" + p.getSize());System.out.println("总页数为" + p.getPages());System.out.println("总数据数量为" + p.getTotal());System.out.println("当前页的数据集合为为:" + p.getRecords());} }
运行后截图如下
从截图中可看出并未获取到当前页的数据,而是获取到了所有数据,原因:
在普通查询中若想执行分页查询,则SQL语句为
select * from user limit 分页限定
,此处MyBatis只会执行select * from user
,而后续的分页限定并未执行,所以若想要其执行的话就需要对其进行拦截增强,而MyBatisPlus已经提供了一个分页拦截器,拿来使用即可。相关步骤如下分页查询知识点可详见MySQL部分内容,拦截器知识点可详见Spring部分内容
-
Step2: 在pom.xml文件中添加mybatis-plus-jsqlparser坐标依赖
博主使用的MyBatisPlus为3.5.9的版本,于
v3.5.9
起,PaginationInnerInterceptor
已分离出来。如需使用,则需单独引入mybatis-plus-jsqlparser
依赖;若使用3.5.9之前的版本则不需要导入以下依赖<!--mybatis-plus-jsqlparser--> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser</artifactId><version>3.5.9</version> </dependency>
-
Step3: 创建一个与三层架构包同级的
config
包并在该包下创建一个MyBatisPlus的配置类MpConfig
,代码如下- Step3-1: 在该类中创建一个使用
@Bean
注解的拦截器方法mybatisPlusInterceptor()
- Step3-2: 在该方法中定义MyBatisPlus的拦截器并添加具体的拦截器—分页拦截器
注意:
DbType
枚举类包括多个数据库类型,用于指定具体的数据库类型。当有多个数据库则可以不配具体类型,否则都建议配上具体的DbType在实际项目中,若要在MyBatis的配置类中添加多个插件方法(即定义多个bean),则分页对应的bean必须最后添加
package at.guigu.config;import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;@Configuration @MapperScan("at.guigu.dao") public class MpConfig {/*** 添加拦截器方法* @return*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 定义MyBatisPlus的拦截器MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();// 配置分页拦截器---注意:若有多个数据源则可以不配具体类型,否则都建议配上具体的DbType(即数据库)mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return mpInterceptor;} }
- Step3-1: 在该类中创建一个使用
-
为明显的看出区别,所以我在application.yml配置文件中配置了MyBatisPlus的运行日志,配置后的运行截图如下
-
若将测试类
TestOne
代码更改如下package at.guigu.dao;import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;/*** 测试按条件分页查询*/ @SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;@Testpublic void test1() {IPage page = new Page<>(2, 2);userDao.selectPage(page, null);System.out.println("当前页码值为:" + page.getCurrent());System.out.println("每页显示的数据数量为:" + page.getSize());System.out.println("总页数为" + page.getPages());System.out.println("总数据数量为" + page.getTotal());System.out.println("当前页的数据集合为:" + page.getRecords());}@Testpublic void test2() {Page<User> page = Page.of(2, 2);Page<User> p = userDao.selectPage(page, null);System.out.println("当前页码值为:" + p.getCurrent());System.out.println("每页显示的数据数量为:" + p.getSize());System.out.println("总页数为" + p.getPages());System.out.println("总数据数量为" + p.getTotal());System.out.println("当前页的数据集合为为:" + p.getRecords());} }
即将
IPage page = new Page<>(1, 2);
改为了IPage page = new Page<>(2, 2);
条件查询
本项目
MbPlusConDemo
已上传至Gitee,可自行下载。具体搭建可详见快速入门,此处只进行不同部分的演示注意:
在快速入门中并未开启MyBatisPlus的运行日志,此处开启
本条件查询示例中只进行个别条件语句的示例,具体详细示例可详见官网指南中的条件构造器
-
用到的方法
方法 解释 public Children lt(boolean condition, R column, Object val)
根据条件设置指定字段的小于条件。即首先判定第一个参数是否为true,若为true则连接当前条件 default Children lt(R column, Object val)
设置指定字段的小于条件 public Children gt(boolean condition, R column, Object val)
设置指定字段的大于条件 default Children gt(R column, Object val)
根据条件设置指定字段的大于条件。即首先判定第一个参数是否为true,若为true则连接当前条件 public Children lt(boolean condition, R column, Object val)
与public Children gt(boolean condition, R column, Object val)
可用于null值处理,详见条件查询---null值处理
的部分内容
-
Step1: 创建
DaoConTest
测试类,并在该类中创建testCon1
方法测试QueryWrapper
,代码如下:-
Step1-1: 创建
QueryWrapper
对象,并调用对应的条件方法package at.guigu.dao;import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest public class DaoConTest {@Autowiredprivate UserDao userDao;// 方式一:利用QueryWrapper进行条件查询@Testpublic void testCon1() {// 创建QueryWrapper对象QueryWrapper queryWrapper = new QueryWrapper();// 使用字段名进行泛型查询queryWrapper.lt("age",15);List<User> users = userDao.selectList(queryWrapper);System.out.println(users);} }
-
-
在上述代码中,
QueryWrapper
直接使用字符串表示字段名,容易发生拼写错误。所以我们采用LambdaQueryWrapper
方法,因为它使用 Lambda 表达式来引用字段名,这样更加安全-
在
DaoConTest
测试类中创建testCon2
方法,代码如下package at.guigu.dao;import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest public class DaoConTest {@Autowiredprivate UserDao userDao;// 方式一:利用QueryWrapper进行条件查询@Testpublic void testCon1() {// 创建QueryWrapper对象QueryWrapper queryWrapper = new QueryWrapper();// 使用字段名进行泛型查询queryWrapper.lt("age",15);List<User> users = userDao.selectList(queryWrapper);System.out.println(users);}// 方式二:利用LambdaQueryWrapper进行条件查询@Testpublic void testCon2() {// 创建LambdaQueryWrapper对象---注意:必须指定泛型LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();// 使用Lambda表达式进行条件查询lambdaQueryWrapper.lt(User::getAge,15);List<User> users = userDao.selectList(lambdaQueryWrapper);System.out.println(users);} }
-
多条件查询—and
-
若我们在查询时条件比较多,则有两种形式(以
LambdaQueryWrapper
方法为例)- 形式一:使用非链式编程
- 形式二:使用链式编程
查询3<age<15的数据
在
DaoConTest
测试类中创建testCon3
方法,代码如下package at.guigu.dao;import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest public class DaoConTest {@Autowiredprivate UserDao userDao;// 方式一:利用QueryWrapper进行链式以及非链式条件的and查询@Testpublic void testConPre3() {// 创建QueryWrapper对象// 若使用链式编程则必须指定泛型,反之则不用QueryWrapper<User> queryWrapper = new QueryWrapper();/* 非链式编程:3<age<15queryWrapper.lt("age",15);queryWrapper.gt("age",3);*/// 链式编程:3<age<15queryWrapper.lt("age",15).gt("age",3);List<User> users = userDao.selectList(queryWrapper);System.out.println(users);}// 方式二:利用LambdaQueryWrapper进行链式以及非链式条件的and查询@Testpublic void testCon3() {// 创建LambdaQueryWrapper对象---注意:必须指定泛型LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();/* 非链式编程:3<age<15lambdaQueryWrapper.lt(User::getAge,15);lambdaQueryWrapper.gt(User::getAge,3);*/// 链式编程:3<age<15lambdaQueryWrapper.lt(User::getAge,15).gt(User::getAge, 3);List<User> users = userDao.selectList(lambdaQueryWrapper);System.out.println(users);} }
-
注意:
- 在QueryWrappe类中若使用链式编程,则必须指定泛型,否则链式编程报错。反之,则泛型可以不指定
多条件查询—or
查询age<4或age>15的数据
在
DaoConTest
测试类中创建testCon4
方法,代码如下
package at.guigu.dao;import at.guigu.pojo.User;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest
public class DaoConTest {@Autowiredprivate UserDao userDao;// 方式一:利用QueryWrapper进行进行链式条件的or查询@Testpublic void testConPre4() {// 创建QueryWrapper对象// 若使用链式编程则必须指定泛型,反之则不用QueryWrapper<User> queryWrapper = new QueryWrapper();// 链式编程:age<4 or age>15queryWrapper.lt("age",4).or().gt("age",15);List<User> users = userDao.selectList(queryWrapper);System.out.println(users);}// 方式二:利用LambdaQueryWrapper进行链式条件的or查询@Testpublic void testCon4() {// 创建LambdaQueryWrapper对象---注意:必须指定泛型LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();// 链式编程:age<4 or age>15lambdaQueryWrapper.lt(User::getAge,4).or().gt(User::getAge, 15);List<User> users = userDao.selectList(lambdaQueryWrapper);System.out.println(users);}
}
条件查询—null值处理
在条件查询中可能会出现null值,则需要对其进行处理
-
Step1: 环境准备:用来模拟前端传来的数据
-
在pojo包下创建一个继承
User
类的UserQuery
类,代码如下:package at.guigu.pojo.query;import at.guigu.pojo.User; import lombok.Data;@Data public class UserQuery extends User {private Integer age2; }
-
-
Step2: 创建测试类
DaoConNullTest
代码如下:package at.guigu.dao;import at.guigu.pojo.User; import at.guigu.pojo.query.UserQuery; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest public class DaoConNullTest {@Autowiredprivate UserDao userDao;@Testpublic void testNull() {// 模拟前端页面传递的数据UserQuery uq = new UserQuery();uq.setAge2(3); // uq.setAge(15); 此处假设age的值为null,所以此处并未给age设值,只给age2设值// null值判定// 创建LambdaQueryWrapper对象---注意:必须指定泛型LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();/* 非链式编程:首先判断n是否是null值,若是则不作为条件语句lambdaQueryWrapper.lt(uq.getAge() != null, User::getAge,uq.getAge());lambdaQueryWrapper.gt(uq.getAge2() != null, User::getAge, uq.getAge2());*/// 链式编程lambdaQueryWrapper.lt(uq.getAge() != null, User::getAge,uq.getAge()).gt(uq.getAge2() != null, User::getAge, uq.getAge2());List<User> users = userDao.selectList(lambdaQueryWrapper);System.out.println(users);} }
由于age为null,age2为3,所以最终会查询age>3的数据,运行截图如下
查询投影
我们在查询数据的时候,什么都没有做默认就是查询表中所有字段的内容,我们所说的查询投影即不查询所有字段,只查询出指定字段对应的数据
本示例会在条件查询的项目
MbPlusConDemo
上进行
-
可能用到的方法
BaseMapper
接口的方法解释 List<T> selectList(@Param("ew") Wrapper<T> queryWrapper)
根据 Wrapper 条件,查询全部记录 List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper)
根据 Wrapper 条件,查询全部记录 QueryWrapper
及LambdaQueryWrapper
方法解释 select(String... sqlSelect)
设置查询字段。 select(Predicate<TableFieldInfo> predicate)
过滤查询字段(主键除外) select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
过滤查询字段(主键除外) groupBy(R... columns)
设置分组条件,使用字段名 groupBy(boolean condition, R... columns)
设置分组条件,使用字段名 属性 解释 String... sqlSelect
是一个变长字符串数组,包含要查询的字段名 Predicate<TableFieldInfo> predicate
是一个 Predicate
函数式接口,用于过滤查询字段。它接受一个TableFieldInfo
类型的参数,并返回一个布尔值,表示是否选择该字段。Class<T> entityClass
实体类的类型,用于获取字段信息 columns
一个可变参数列表,包含用于分组的字段名 boolean condition
一个布尔值,用于控制是否应用这个分组条件 -
注意:
- 查询指定字段即可使用字符串也可使用Lambda表达式。也就是说即能使用
QueryWrapper
,也能使用LambdaQueryWrapper
- 聚合与分组查询,无法使用Lambda表达式来完成。也就是说只能使用
QueryWrapper
,不能使用LambdaQueryWrapper
- 查询指定字段即可使用字符串也可使用Lambda表达式。也就是说即能使用
查询指定字段
-
方式一:利用
QueryWrapper
进行查询指定字段- 创建一个测试投影包
Projection
,并在该包下创建测试类TestOne
,代码如下:- 创建
QueryWrapper
对象 - 指定要查询的数据库表中的字段—字符串形式
- 将
QueryWrapper
对象作为BaseMapper
接口的selectList
方法的参数传入
- 创建
package at.guigu.Projection;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List; /*** 查询指定字段*/ @SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;// 方式一:利用QueryWrapper查询指定字段@Testpublic void test1() {// 创建QueryWrapper对象QueryWrapper queryWrapper = new QueryWrapper();// 指定要查询的数据库表中的字段queryWrapper.select("name", "age");List<User> users = userDao.selectList(queryWrapper);for (User user : users) {System.out.println(user);}} }
- 创建一个测试投影包
-
方式二:利用
LambdaQueryWrapper
查询指定字段- 在测试类
TestOne
下创建test2
方法,代码如下:- 创建
LambdaQueryWrapper
对象 - 使用 Lambda表达式 来指定要查询的数据库表中的字段
- 将
LambdaQueryWrapper
对象作为BaseMapper
接口的selectList
方法的参数传入
- 创建
package at.guigu.Projection;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List; /*** 查询指定字段*/ @SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;// 方式二:利用LambdaQueryWrapper查询指定字段@Testpublic void test2() {// 创建LambdaQueryWrapper对象---注意:必须指定泛型LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();// 使用Lambda表达式来指定要查询的数据库表中的字段lambdaQueryWrapper.select(User::getName, User::getAge);List<User> users = userDao.selectList(lambdaQueryWrapper);for (User user : users) {System.out.println(user);}} }
- 在测试类
聚合查询
-
SQL语句中的聚合查询主要有五种,具体解释可详见MySQL部分内容
函数名 解释 count(列名)
统计数量(count统计的字段对应的列中不能为null,即一般选用不为null的列) max(列名)
最大值 min(列名)
最小值 sum(列名)
求和 avg(列名)
平均值 -
注意
- 聚合查询时就不能使用
BaseMapper
接口中的selectList
方法,而是使用的BaseMapper
接口中的selectMaps
方法。因为聚合查询返回的是键值对,并不是实体类对象列表 - 聚合查询只能使用
QueryWrapper
进行聚合查询,不能使用LambdaQueryWrapper
- 聚合查询时就不能使用
以
count(列名)
以及avg(列名)
为例
-
利用
QueryWrapper
进行聚合查询- 在测试投影包
Projection
下创建测试类TestTwo
,代码如下:- 创建
QueryWrapper
对象 - 指定聚合查询以及对应的数据库表中的字段—字符串形式
- 将
QueryWrapper
对象作为BaseMapper
接口的selectMaps
方法的参数传入
- 创建
package at.guigu.Projection;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List; import java.util.Map;/*** 聚合查询*/ @SpringBootTest public class TestTwo {@Autowiredprivate UserDao userDao;// 利用QueryWrapper进行聚合查询@Testpublic void test1() {// 创建QueryWrapper对象QueryWrapper queryWrapper = new QueryWrapper();// 指定聚合查询以及数据库表中的字段queryWrapper.select("COUNT(*) as count", "AVG(age) as age");// 等同于 queryWrapper.select("COUNT(*) as count, AVG(age) as age");List<Map<String, Object>> users = userDao.selectMaps(queryWrapper);for (Map<String, Object> user : users) {System.out.println(user);}} }
- 在测试投影包
分组查询
-
在测试投影包
Projection
下创建测试类TestThree
,代码如下:- 创建
QueryWrapper
对象 - 指定分组查询对应的数据库表中的字段—字符串形式
- 将
QueryWrapper
对象作为BaseMapper
接口的selectMaps
方法的参数传入
package at.guigu.Projection;import at.guigu.dao.UserDao; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List; import java.util.Map;/*** 聚合查询*/ @SpringBootTest public class TestThree {@Autowiredprivate UserDao userDao;// 利用QueryWrapper进行分组查询@Testpublic void test1() {// 创建QueryWrapper对象QueryWrapper queryWrapper = new QueryWrapper();// 指定聚合查询以及数据库表中的字段queryWrapper.select("COUNT(*) as count, age");// 指定分组查询对应的数据库表中的字段queryWrapper.groupBy("age");List<Map<String, Object>> users = userDao.selectMaps(queryWrapper);for (Map<String, Object> user : users) {System.out.println(user);}} }
- 创建
其余条件查询
-
用到的方法
BaseMapper<T>
接口中的方法解释 T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
根据 entity 条件,查询一条记录 List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
根据 entity 条件,查询全部记录 QueryWrapper
及LambdaQueryWrapper
方法解释 eq(R column, Object val)
设置指定字段的相等条件 eq(boolean condition, R column, Object val)
根据条件设置指定字段的相等条件 between(R column, Object val1, Object val2)
设置指定字段的 BETWEEN 条件 between(boolean condition, R column, Object val1, Object val2)
根据条件设置指定字段的 BETWEEN 条件 属性 解释 boolean condition
一个布尔值,用于控制是否应用这个相等条件 R column
数据库字段名或使用 Lambda
表达式的字段名Object val
与字段名对应的值 Object val1, Object val2
val1
表示 BETWEEN 条件的起始值;val2
表示 BETWEEN 条件的起始值
等值查询
- 由于等值查询出来的是一个实体类对象,所以此时会使用
selectOne
方法
根据用户名和密码查询用户信息
-
创建othercon测试包,并在该包下创建
TestOne
测试类,代码如下package at.guigu.othercon;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;// 方式一:利用QueryWrapper进行等值查询---非链式查询@Testpublic void test1() {// 创建QueryWrapper对象QueryWrapper queryWrapper = new QueryWrapper();// 指定等值查询对应数据库表中的字段以及值queryWrapper.eq("name", "Tom");queryWrapper.eq("password", "tom");User user = userDao.selectOne(queryWrapper);System.out.println(user);}// 方式一:利用QueryWrapper进行等值查询---链式查询@Testpublic void test2() {// 创建QueryWrapper对象---链式查询时必须指定泛型,否则无法进行链式查询QueryWrapper<User> queryWrapper = new QueryWrapper();// 指定等值查询对应数据库表中的字段以及值queryWrapper.eq("name", "Tom").eq("password", "tom");User user = userDao.selectOne(queryWrapper);System.out.println(user);}// 方式二:利用LambdaQueryWrapper进行等值查询@Testpublic void test3() {// 创建LambdaQueryWrapper对象---注意:必须指定泛型LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();// 使用Lambda表达式进行等值查询lambdaQueryWrapper.eq(User::getName,"Tom").eq(User::getPassword, "tom");User users = userDao.selectOne(lambdaQueryWrapper);System.out.println(users);} }
注意
- 其余查询方法的使用可详见MyBatisPlus的官网,此处不在演示
- 若查询出来的是单个实体类对象,则使用
Wrapper<T>
抽象类中的selectOne
方法;若查询出来的是实体类对象列表,则使用Wrapper<T>
抽象类中的selectList
方法;若查询出来的是数值,则使用Wrapper<T>
抽象类中的selectMaps
方法
字段映射与表名映射
-
用到的注解
注解 解释 @TableField(value, exist, select)
设置当前属性与数据库表中对应字段的关系。value:指定数据库表中对应字段的名称; exist
:设置属性在数据库表字段中是否存在,默认为true
,该属性无法与value
属性合并使用;select
:设置属性是否参与查询,此属性与select()
方法的映射配置不冲突@TableName(value)
设置当前类与对应数据库表的关系。 value
:指定数据库表的名称
表字段与编码属性设计不同步问题
-
即实体类属性名与表字段不一致
-
方法:给实体类中与数据库表的字段不一致的属性添加
@TableField
注解并指定value属性值为数据库对应表中的字段名,如图所示
编码中存在数据库中未定义的属性问题
-
在实际开发项目中,实体类中的属性并不都是数据库表中的字段,可能会随着项目的开发自行添加了一些属性,此时就需要利用
@TableField
注解并指定exist
属性值为false
即可(作用:设置该属性是否参与查询)。如图所示online即为自己添加的,数据库表中不存在该对应的字段
默认查询开放字段查看权限问题
-
在实际项目中,我们在查询数据库表时,就可能会把一些敏感字段对应的数据(比如:密码字段)也返回到前端,导致损失产生。所以我们就需要利用
@TableField
注解将这些不需要返回的敏感的字段,同时要指定该注解的属性select
的属性值为false
。如图所示作用:关闭指定字段的查询
此时再去查询数据时,返回的数据会自动屏蔽pwd字段对应的值,可详见逻辑删除的运行截图
表名与编码开发设计不同步问题
-
即数据库表名与对应的实体类名不一致
-
方法:使用
@TableName
注解,通过**value**属性,设置对应实体类对应的数据库表名称。
由于我们使用的数据库表名与实体类名不一致,所以每次都需要使用
@TableName
注解来设置,若项目中过多数据库表与实体类名不一致(即数据库表均有tbl_
前缀,如图所示)的话就会很繁琐,所以简化步骤如下:
-
在application.yml配置文件中进行全局配置,代码如下:
mybatis-plus:global-config:db-config:# 设置数据库表名前缀table-prefix: tbl
完整代码如下
spring:# 配置数据库连接信息以及数据源信息datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mbplus?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456main:# 设置SpringBoot的banner不显示(即运行时不显示SpringBoot图标)banner-mode: offmybatis-plus:configuration:# 配置MyBatisPlus的运行日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:# 设置MyBatis的banner不显示(即运行时不显示MyBatis图标)banner: falsedb-config:# 设置数据库表名前缀table-prefix: tbl_
配置完之后,所有数据库表对应的实体类就不需要用
@TableName
注解就会自动匹配对应的数据库表,如下图所示
DML增删改数据
注意:此处只进行部分示例,具体可详见MyBatisPlus的官网
工作准备
本项目
MpDmlDemo
已上传至Gitee,可自行下载。快速搭建可详见MyBatisPlus的快速入门部分内容,与快速入门不同的区别如下: 1.数据库表名改为了tb_user,并将表中password改为了psw
2.在实体类User中添加了数据库表中没有的字段
-
数据库表的代码如下:
DROP TABLE IF EXISTS tbl_user;CREATE TABLE IF NOT EXISTS tbl_user (id bigint(20) primary key auto_increment,name varchar(32) not null,psw varchar(32) not null,age int(3) not null ,tel varchar(32) not null ); insert into tbl_user values(1,'Tom','tom',3,'13346355023'); insert into tbl_user values(2,'Jerry','jerry',4,'16688336995'); insert into tbl_user values(3,'Jock','123456',41,'17725996338'); insert into tbl_user values(4,'李四','nigger',15,'18879566321');SELECT * FROM tbl_user;
-
实体类代码如下:
package at.guigu.pojo; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;@Data @TableName("tbl_user") public class User {private Long id;private String name;@TableField(value = "psw", select = false)private String password;private Integer age;private String tel;@TableField(exist = false)private Integer online; }
id生成策略控制(INSERT添加数据)
-
不同的业务采用的ID生成方式应该是不一样的(如图所示),所以我们可以通过
@TableId
注解来指定当前类中主键属性的生成策略 -
用到的注解
注解 解释 @TableId(value, type)
设置当前类中主键属性的生成策略 属性 解释 value
(默认)设置数据库表主键名称 type
设置主键属性的生成策略,其值可查照 IdType
的枚举值 -
IdType
的枚举值IdType
的枚举值解释 AUTO(0)
(默认)自动选择主键生成策略,默认策略。通常在数据库支持自增主键的情况下使用 NONE(1)
没有主键生成策略(即不设置主键生成策略)。这种方式一般用于主键由应用程序生成的场景,MyBatis-Plus 不会尝试处理主键的生成 INPUT(2)
必须手动输入主键值。在主键无自增策略时,主键的值由程序员在插入时手动提供,MyBatis-Plus 不会自动生成, ASSIGN_ID(3)
可利用Snowflake雪花 算法生成ID(可兼容数值型与字符串型)。 ASSIGN_UUID(4)
以UUID生成算法作为id生成策略 -
注意:
AUTO
在主键有自增策略的情况下使用INPUT
、ASSIGN_ID
、ASSIGN_UUID
通常在主键无自增策略时使用- 在主键无自增策略的情况下,若使用
ASSIGN_ID
,则情况有2种:- 若手动输入主键值,则以手动输入的为准
- 若未手动输入主键值,则MyBatisPlus会利用Snowflake雪花算法自动生成ID
- 当主键需要为 UUID 类型且无自增策略时,使用
ASSIGN_UUID
代码示例
-
AUTO(0)
测试-
Step1: 给实体类的主键添加
@TableId
属性并指定生成策略为AUTO
package at.guigu.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;@Data @TableName("tbl_user") public class User {@TableId(type = IdType.AUTO)private Long id;private String name;@TableField(value = "psw", select = false)private String password;private Integer age;private String tel;@TableField(exist = false)private Integer online; }
-
Step2: 创建dml测试包,并在该包下创建测试类
TestOne
,并在该类下创建Testsave1
方法,来执行新增操作,代码如下:package at.guigu.dml;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;/*** 新增*/@Testpublic void testSave1() {User user = new User();user.setName("张一");user.setPassword("123456");user.setAge(15);user.setTel("1888888888");userDao.insert(user);} }
运行截图如下,此时由于数据库中的字段id为 自增主键 ,所以MyBatisPlus会自动生成对应的id
-
-
INPUT(2)
测试-
Step1: 将主键
id
的自增策略关闭 -
Step2: 给实体类的主键添加
@TableId
属性并指定生成策略为INPUT
package at.guigu.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;@Data @TableName("tbl_user") public class User {@TableId(type = IdType.INPUT)private Long id;private String name;@TableField(value = "psw", select = false)private String password;private Integer age;private String tel;@TableField(exist = false)private Integer online; }
-
Step3: 在测试类
TestOne
下创建Testsave2()
方法,来执行新增操作,代码如下:package at.guigu.dml;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;/*** 新增:测试IdType.INPUT*/@Testpublic void testSave2() {User user = new User();// 必须传入ID值,否则报错user.setId(666L);user.setName("张二");user.setPassword("654321");user.setAge(16);user.setTel("1777777777");userDao.insert(user);} }
-
-
ASSIGN_ID(3)
测试-
Step1: 将主键
id
的自增策略关闭 -
Step2: 给实体类的主键添加
@TableId
属性并指定生成策略为ASSIGN_ID
package at.guigu.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;@Data @TableName("tbl_user") public class User {@TableId(type = IdType.ASSIGN_ID)private Long id;private String name;@TableField(value = "psw", select = false)private String password;private Integer age;private String tel;@TableField(exist = false)private Integer online; }
-
Step3: 在测试类
TestOne
下创建Testsave3()
方法,来执行新增操作,代码如下:package at.guigu.dml;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest public class TestOne {@Autowiredprivate UserDao userDao;/*** 新增:测试IdType.ASSIGN_ID、给id值*/@Testpublic void testSave3() {User user = new User();user.setId(777L);user.setName("张三");user.setPassword("894564");user.setAge(17);user.setTel("16666666");userDao.insert(user);}}
运行成功后对应数据id为自己设置的id值
-
-
Step4: 在测试类
TestOne
下创建Testsave4()
方法,来执行新增操作,代码如下:package at.guigu.dml;import at.guigu.dao.UserDao;import at.guigu.pojo.User;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTestpublic class TestOne {@Autowiredprivate UserDao userDao;/*** 新增:测试IdType.ASSIGN_ID、给id值*/@Testpublic void testSave3() {User user = new User();user.setId(777L);user.setName("张三");user.setPassword("894564");user.setAge(17);user.setTel("16666666");userDao.insert(user);}/*** 新增:测试IdType.ASSIGN_ID、不给id值*/@Testpublic void testSave4() {User user = new User();user.setName("张四");user.setPassword("2356894");user.setAge(18);user.setTel("1555555");userDao.insert(user);}}
id生成策略简化
若实体类过多且都使用了相同的id生成策略时,此时若用
@TableId
注解来设置就会太麻烦,所以就有了简化版本
-
Step1: 在application.yml配置文件种配置id生成策略,代码如下
mybatis-plus:global-config:# 设置id的生成策略db-config:id-type: assign_id
完整代码如下
spring:# 配置数据库连接信息以及数据源信息datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mbplus?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456main:# 设置SpringBoot的banner不显示(即运行时不显示SpringBoot图标)banner-mode: offmybatis-plus:configuration:# 配置MyBatisPlus的运行日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:# 设置MyBatis的banner不显示(即运行时不显示MyBatis图标)banner: falsedb-config:# 设置id的生成策略id-type: assign_id# 设置数据库表名前缀table-prefix: tbl_
DELETE删除数据
彻底删除
本示例会以添加数据中所添加的那些数据为基准进行删除演示,如下图红框所示
-
**删除单条数据:**在dml测试包下创建
TestTwo
类,并在该类下创建testDelete1()
方法,代码如下:package at.guigu.dml;import at.guigu.dao.UserDao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.ArrayList; import java.util.List;/*** DELETE除操作*/ @SpringBootTest public class TestTwo {@Autowiredprivate UserDao userDao;/*** 删除单条数据:彻底删除*/@Testpublic void testDelete1() {userDao.deleteById(1869711650449047553L);} }
-
**删除多条数据:**在
TestTwo
类下创建testDelete2()
方法,代码如下:package at.guigu.dml;import at.guigu.dao.UserDao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.ArrayList; import java.util.List;/*** DELETE除操作*/ @SpringBootTest public class TestTwo {@Autowiredprivate UserDao userDao;/*** 删除多条数据:彻底删除*/@Testpublic void testDelete2() {List<Long> list = new ArrayList<>();list.add(5L);list.add(666L);list.add(777L);userDao.deleteByIds(list);} }
逻辑删除
彻底删除之后,数据库表中数据变为了原来的四条数据,如图一所示
-
背景引入
-
假设图一中的两个表分别为员工表以及每个员工一年内所签的合同带给公司的盈利,此时1号员工离职,若采用彻底删除方式,则会删除两个表中关于1号员工的所有数据(如图二所示)
-
此时若统计一年内公司的盈利,则会把已经离职的1号员工带给公司的盈利给排除在外(如图三所示)
-
为避免出现图三所示问题,就引入了逻辑删除的概念
-
-
逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中
- 即:为数据添加一个是否可用的状态字段,当要删除数据时,我们不会去彻底删除数据,数据仍然保留在数据库中,只是将其状态字段变更为不可用状态。在后续操作中会通过该状态字段来判断该数据是否被删除,若被删除则不可使用这条数据,反之则可以使用
- 本质:逻辑删除的本质其实是修改操作。如果加了逻辑删除字段,查询数据时也会自动带上逻辑删除字段。可详见代码示例
-
Step1: 在数据库表中添加一个状态字段,并将其默认值设为0
-
Step2: 在对应的实体类User中添加状态字段对应的属性,代码如下
- 添加一个由
@TableLogic
注解修饰的deleted
属性,并给该注解的value
和delval
属性赋值。value
为该状态字段的默认值,即逻辑未删除值delval
为该状态字段变更状态后的值,即逻辑删除值
package at.guigu.pojo; import com.baomidou.mybatisplus.annotation.*; import lombok.Data;@Data @TableName("tbl_user") public class User {@TableId(type = IdType.ASSIGN_ID)private Long id;private String name;@TableField(value = "psw", select = false)private String password;private Integer age;private String tel;@TableField(exist = false)private Integer online;// 该注解代表这个属性为逻辑删除的状态字段@TableLogic(value = "0", delval = "1")private Integer deleteed; }
- 添加一个由
-
Step3: 在
TestTwo
类下创建testDelete3()
方法删除id为1的数据,代码如下:package at.guigu.dml;import at.guigu.dao.UserDao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;import java.util.ArrayList; import java.util.List;/*** DELETE除操作*/ @SpringBootTest public class TestTwo {@Autowiredprivate UserDao userDao;/*** 删除单条数据:逻辑删除*/@Testpublic void testDelete3() {userDao.deleteById(1L);} }
- 在以上运行截图中有三个重要点:
- 删除语句由原来的
DELETE
变为了UPDATE
,即UPDATE tbl_user SET deleteed=1 WHERE id=? AND deleteed=0
- 在执行逻辑删除之前,会去找到对应id的数据并判断它的状态字段的值是否为0.若为0则逻辑删除;若不为0,则代表已经逻辑删除了,此时就不会再执行逻辑删除操作了
- 执行完逻辑删除操作后,id为1的这条数据的状态字段变更为了1
- 删除语句由原来的
- 在以上运行截图中有三个重要点:
-
添加完逻辑删除后,再去执行查询操作时,会自动去判断数据的状态字段是否为0 。若为0则代表未删除,则可以查询到该数据;若不为0,则代表已逻辑删除,此时就无法查询到该数据,如下所示
-
若存在逻辑删除时,会自动加上条件判断语句来判断状态字段的值,此时上图所示代码为:
SELECT id,name,age,tel,deleteed FROM tbl_user WHERE deleteed=0
- 此时若我们想要查询那些已经逻辑删除的数据的话就只能通过MyBtis注解SQL语句的形式来查询了
-
逻辑删除弊端:
- 会导致数据库表中的垃圾数据越来越多,影响查询效率
- SQL中全部均需要对逻辑删除字段做判断,影响查询效率
- 因此在实际项目中,若数据不能被彻底删除时,可以采用把数据迁移到其它表中的方法
逻辑删除简化
若逻辑删除存在大量需求,此时我们通过采用
@TableLogic
注解冗余度就会很高,所以我们可以通过配置的形式来简化
-
application.yml中配置逻辑删除的状态字段的代码如下
mybatis-plus:global-config:db-config:# 逻辑删除字段名(注意:该字段对应的在实体类中的类型必须是boolean或Integer类型)logic-delete-field: deleted# 逻辑删除字面值:未删除为0logic-not-delete-value: 0# 逻辑删除字面值:删除为1logic-delete-value: 1
配置文件完整代码如下
spring:# 配置数据库连接信息以及数据源信息datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mbplus?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456main:# 设置SpringBoot的banner不显示(即运行时不显示SpringBoot图标)banner-mode: offmybatis-plus:configuration:# 配置MyBatisPlus的运行日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:# 设置MyBatis的banner不显示(即运行时不显示MyBatis图标)banner: falsedb-config:# 设置id的生成策略id-type: assign_id# 设置数据库表名前缀table-prefix: tbl_# 逻辑删除字段名logic-delete-field: deleteed# 逻辑未删除值(即默认值)logic-not-delete-value: 0# 逻辑删除值logic-delete-value: 1
乐观锁(UPDATE修改数据)
- 乐观锁是一种并发控制机制 ,用于确保在更新记录时,该记录不会被其他事务修改。MyBatis-Plus 提供了
OptimisticLockerInnerInterceptor
插件,使得在应用中实现乐观锁变得简单。
快速入门
-
Step1: 在数据库中添加一个
int
类型长度为11的表示版本号的标记字段version
,并给其设置一个默认值(该默认值为其假定的版本号,此处设置为1) -
Step2: 在实体类
User
中添加version字段对应的属性值,代码如下:package at.guigu.pojo; import com.baomidou.mybatisplus.annotation.*; import lombok.Data;@Data @TableName("tbl_user") public class User {@TableId(type = IdType.ASSIGN_ID)private Long id;private String name;@TableField(value = "psw", select = false)private String password;private Integer age;private String tel;@TableField(exist = false)private Integer online;// 该注解代表这个属性为逻辑删除的状态字段@TableLogic(value = "0", delval = "1")private Integer deleteed;@Versionprivate Integer version; }
-
Step3: 创建一个与三层架构包同级的
config
包并在该包下创建一个MyBatisPlus的配置类MpConfig
,代码如下- Step3-1: 在该类中创建一个使用
@Bean
注解的拦截器方法mybatisPlusInterceptor()
- Step3-2: 在该方法中定义MyBatisPlus的拦截器并添加具体的拦截器—乐观锁拦截器
注意:在以下代码中配置了乐观锁拦截器外还配置了分页拦截器,可根据自身需求来选择是否配置分页拦截器
package at.guigu.config;import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;@Configuration @MapperScan("at.guigu.dao") public class MpConfig {/*** 添加拦截器方法* @return*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 定义MyBatisPlus的拦截器MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();// 配置分页拦截器---注意:若有多个数据源则可以不配具体类型,否则都建议配上具体的DbType(即数据库)mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 配置乐观锁拦截器mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return mpInterceptor;} }
- Step3-1: 在该类中创建一个使用
-
Step3: 在dml测试包下创建测试类
TestThree
,并在该类下创建testUpdate1()
方法,代码如下- 为了让MyBatisPlus在更新数据时获取到当前版本号,所以必须利用
setter
方法给版本号的标记字段所对应的version
属性赋值传递给MyBatisPlus,,否则不会生效
package at.guigu.dml;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;/*** 乐观锁(UPDATE修改/更新操作)*/ @SpringBootTest public class TestThree {@Autowiredprivate UserDao userDao;/*** 繁琐操作*/@Testpublic void testUpdate1() {User user = new User();user.setId(2L);user.setName("Jerryyyyy");user.setPassword("mimaa");// 修改数据时必须利用`setter`方法给版本号的标记字段所对应的`version`属性赋值,否则不会生效user.setVersion(1);userDao.updateById(user);} }
在Step3的步骤中,每次我们都要去使用setter方法来给version赋值,这样就很麻烦,所以简便方法为:
package at.guigu.dml;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;/*** 乐观锁(UPDATE修改/更新操作)*/ @SpringBootTest public class TestThree {@Autowiredprivate UserDao userDao;/*** 繁琐操作*/@Testpublic void testUpdate1() {User user = new User();user.setId(2L);user.setName("Jerryyyyy");user.setPassword("mimaa");// 修改数据时必须利用`setter`方法给版本号的标记字段所对应的`version`属性赋值,否则不会生效user.setVersion(1);userDao.updateById(user);}/*** 简化操作*/@Testpublic void testUpdate2() {// 首先通过要修改的数据的id获取到要修改的数据User user = userDao.selectById(2L);// 然后将要修改的属性逐一修改user.setName("Jerryyyyy");user.setPassword("mimaa");userDao.updateById(user);} }
- 为了让MyBatisPlus在更新数据时获取到当前版本号,所以必须利用
-
由以上运行截图可知,此时判断条件除了
id
之外,还会有deleteed
和version
;并且在更新时会自动修改version
字段的值,由图可知从1变为了2.。目的是:- 在并发系统中,假设有ABC三个用户同时获取到了version值,但是只有A在修改数据,此时由于A在修改数据时,MyBatisPlus会自动将version这个字段值+1,导致BC获取到的version值失效,从而使其无法操作数据,从而保证了数据在同一时间只能由一个用户操作。
原理示例
-
在dml测试包下创建测试类
TestFour
,并在该类下创建testUpdate()
方法,代码如下- 在该方法中会模拟两个用户同时修改数据的操作
package at.guigu.dml;import at.guigu.dao.UserDao; import at.guigu.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;/*** 乐观锁(UPDATE修改/更新操作)原理演示*/ @SpringBootTest public class TestFour {@Autowiredprivate UserDao userDao;@Testpublic void testUpdate1() {// 模拟用户1User user1 = userDao.selectById(3L); // 用户1 version=1// 模拟用户2User user2 = userDao.selectById(3L); // 用户2 version=1// 用户1修改数据user1.setName("小明");userDao.updateById(user1);// 用户1修改完成后 version变更为2// 用户2修改数据user2.setName("小虎"); userDao.updateById(user2);// 由于用户1修改完后version变更为了2,而用户2的version仍为1,所以用户2执行失败} }