# Mybatis-plus入门
> 前提:您已经了解SpringBoot和Mybatis的基础知识,否则不建议学习
## 简介
[MyBatis-Plus (opens new window)](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 [MyBatis (opens new window)](https://www.mybatis.org/mybatis-3/)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 [Mybatis-plus官方地址](https://baomidou.com/)
## 创建并初始化数据库
- ##### 建立hospital数据库
```sql
CREATE DATABASE `hospital`;
USE `hospital`;
```
- ##### 建立User表并插入数据
```sql
/*
Navicat Premium Data Transfer
Source Server : 本地连接
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : hospital
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 28/03/2022 16:08:11
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(0) NOT NULL COMMENT '主键ID',
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(0) NULL DEFAULT NULL COMMENT '年龄',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'Jone', 18, 'test1@baomidou.com');
INSERT INTO `user` VALUES (2, 'Jack', 20, 'test2@baomidou.com');
INSERT INTO `user` VALUES (3, 'Tom', 28, 'test3@baomidou.com');
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'test4@baomidou.com');
INSERT INTO `user` VALUES (5, 'Billie', 24, 'test5@baomidou.com');
SET FOREIGN_KEY_CHECKS = 1;
```
## 确认IDEA配置
![image-20220328162237203](C:\Users\Zhoujiankang\AppData\Roaming\Typora\typora-user-images\image-20220328162237203.png)
截止目前,利用IDEA创建SpringBoot项目的版本最低为2.5.11。但是为了项目演示并且学习Mybatis与SpringBoot的整合,我们仅仅需要SpringBoot为==2.2.1.RELEASE==的版本。因此,需要在pom.xml文件中修改配置。
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
```
## 创建项目
- ##### 添加Mybatis-plus的依赖和其他相关依赖
```xml
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
```
特别注意:==添加了Mybatis-plus依赖后,就不要添加Mybatis依赖==。
如果您的lombok不起作用,请及时安装在IDEA中安装`lombok`插件。一般情况下,已经默认安装。(该插件帮助生成Setter和Getter方法)
- ##### 配置数据库信息;编写application.properties文件 或者编写application.yaml文件
```yaml
#mysql数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/hospital # database替换为对应的数据库
username: root # root替换为对应的用户
password: 123456 # password替换为对应的密码
driver-class-name: com.mysql.cj.jdbc.Driver
```
相比application.properties文件,我更喜欢application.yaml文件。因为它更简洁明了。(可以了解一下配置文件的生效顺序,以及他们两者之间的区别)
## 编写代码
- 编写实体类
```java
package com.example.demomp.entity;
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
```
- 编写Mapper
```java
public interface UserMapper extends BaseMapper<User> {
// 使用继承BaseMapper<E> ,插入User实体类类型
}
```
> BaseMapper<E> 接口已经为我们封装好各种操作数据的方法。可以看一下它的源码
```java
//BaseMapper的源码
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> wrapper);
int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
T selectOne(@Param("ew") Wrapper<T> queryWrapper);
Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
<E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper);
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper);
}
```
- 启动类
这里我们需要的在DemompApplication加入注解@MapperScan("com.example.demomp.mapper"),以获取该包动态生成的对象。相比在每个接口类添加@Mapper注解,还不如在使用@MapperScan,直接获取该包下的所有的接口类的动态的生成的实现类。
```java
package com.example.demomp;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.demomp.mapper")
//作用:指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类
public class DemompApplication {
public static void main(String[] args) {
SpringApplication.run(DemompApplication.class, args);
}
}
```
- 测试类
```java
@SpringBootTest
class DemompApplicationTests {
@Autowired
private UserMapper userMapper;
//查询操作,查询所有
@Test
public void findAll() {
List<User> users = userMapper.selectList(null);
System.out.println(users);
}
@Test
void contextLoads() {
}
}
```
- 查看sql输出日志
```yaml
#mybatis 日志输出sql
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
```
运行测试类,查看测试结果,并查看输出日志。
![image-20220328192132976](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328192132976.png)
- 插入操作
```java
// 插入操作
@Test
public void add() {
User user = new User();
//id在插入时,Mybatis会默认帮我们生成
user.setName("李剑桥就是铁憨憨");
user.setAge(20);
user.setEmail("lijianqiao@ccit.edu");
// 返回影响行数,插入1个数据,就影响一行
int insert = userMapper.insert(user);
System.out.println("影响行数"+insert);
}
```
![image-20220328193045798](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328193045798.png)
![image-20220328193145689](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328193145689.png)
由上述Springboot会自动帮助我们生成主键(也就是User.id)这一操作说起,那么又引发了一个问题,`主键的生成`该如何把握呢?或者该如何设计呢?
这里就不得不讨论一下`主键生成策略`。
## 主键策略
- 在实体类User添加注解`@TableId(type = IdType.ASSIGN_ID)`,可以使用@TableId确定主键,使用type属性规定主键策略:
```java
@Data
public class User {
@TableId(type = IdType.ASSIGN_ID)
//
private Long id;
private String name;
private Integer age;
private String email;
}
```
- 这里的添加策略共有8种,但是后面的3种已经弃用(没有学习得必要了),这里只学习使用前面的5种,比较常用的是`AUTO`、`ASSIGN_UUID`、`ASSIGN_ID`、`INPUT`、`NONE`
![image-20220328202535467](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328202535467.png)
| 值 | 描述 |
| :---------------: | :----------------------------------------------------------: |
| AUTO | 数据库ID自增 |
| NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
| INPUT | insert前自行set主键值 |
| ASSIGN_ID | 分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口`IdentifierGenerator`的方法`nextId`(默认实现类为`DefaultIdentifierGenerator`雪花算法) |
| ASSIGN_UUID | 分配UUID,主键类型为String(since 3.3.0),使用接口`IdentifierGenerator`的方法`nextUUID`(默认default方法) |
| ~~ID_WORKER~~ | ~~分布式全局唯一ID 长整型类型(please use `ASSIGN_ID`)~~ |
| ~~UUID~~ | ~~32位UUID字符串(please use `ASSIGN_UUID`)~~ |
| ~~ID_WORKER_STR~~ | ~~分布式全局唯一ID 字符串类型(please use `ASSIGN_ID`)~~ |
在这里我们可以查看一下`IdType`的源码,就能简单知道主键的生成策略。
```java
/**
* 生成ID类型枚举类
*
*/
@Getter
public enum IdType {
/**
* 数据库ID自增
*/
AUTO(0),
/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),
/**
* 用户输入ID
* <p>该类型可以通过自己注册自动填充插件进行填充</p>
*/
INPUT(2),
/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 分配ID (主键类型为number或string),
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
*
* @since 3.3.0
*/
ASSIGN_ID(3),
/**
* 分配UUID (主键类型为 string)
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
*/
ASSIGN_UUID(4),
/**
*
* @deprecated 3.3.0 please use {@link #ASSIGN_ID}
*/
@Deprecated
ID_WORKER(3),
/**
* @deprecated 3.3.0 please use {@link #ASSIGN_ID}
*/
@Deprecated
ID_WORKER_STR(3),
/**
* @deprecated 3.3.0 please use {@link #ASSIGN_UUID}
*/
@Deprecated
UUID(4);
private final int key;
IdType(int key) {
this.key = key;
}
}
```
> PS:如果您有兴趣,可以了解一下的雪花策略。
来看看使用`主键策略`后,插入的数据有什么不同?准确说是ID值的不同。再来执行一遍插入操作,看看有什么不同?
![image-20220328204521711](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328204521711.png)
这里的`Id`长度为`19`位Long型值。
#### 配置主键策略的方式
1.全局配置
```yml
#这里的全局配置在application.yaml文件中修改
mybatis-plus:
global-config:
db-config:
id-type: AUTO
```
2.局部配置
```java
// 直接在实体类中添加注解
@TableId(type = IdType.AUTO) //指定主键
private Long id;
```
## 自动填充和乐观锁
### 1.修改操作
注意: `update时生成的sql自动是动态sql: UPDATE user SET name = ? WHERE id=? `
```java
//修改操作
@Test
public void Update() {
User user = new User();
user.setId(1508424863220776962L);
user.setName("张三丰");
// 返回影响行数,修改1个数据,就影响一行
int count = userMapper.updateById(user);
System.out.println("影响行数" + count);
}
```
![image-20220328211519485](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328211519485.png)
### 2.自动填充
需求描述:在项目中经常会遇到一些数据,每次都使用相同的方式进行填充,例如记录的创建时间和更新时间等。
而我们可以使用Mybatis Plus的自动填充功能,来完成这些的字段的赋值工作。
为了演示Mybatis-Plus的自动填充的过程,我们需要修改User表:添加2个新的`datatime`类型字段create_time、update_time。
![image-20220328212901577](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328212901577.png)
与之对应的,我们也要修改相应的实体类。
```java
@Data
public class User {
//
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
// 驼峰命名 对应的数据库的字段create_time、update_time
private Date createTime;
private String updateTime;
}
```
**(1).添加注解**
为实体类中要自动填充的属性添加注解
```java
// 驼峰命名 对应的数据库的字段create_time、update_time
@TableField(fill= FieldFill.INSERT)
private Date createTime;
@TableField(fill=FieldFill.INSERT_UPDATE)
private String updateTime;
```
**(2).创建实现类:实现元对象处理器的接口**
创建实现类,实现接口中的2个方法, 一个方法添加执行,另一个方法更新执行。
```java
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
// 当Mybatis-plus执行添加操作时,此方法就会执行
@Override
public void insertFill(MetaObject metaObject) {
/** 通过相应的字段名赋值该字段值
* 此方法有3个参数
* fieldName : 字段名
* fieldVal : 字段值
* metaObject: 元对象
*
* 在本业务中,我们添加一个对象,就自动填充创建时间;
* 这里的时间就是当前时间 new Date();
*/
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
// 当Mybatis-plus执行修改操作时,此方法就会执行
@Override
public void updateFill(MetaObject metaObject) {
/** 通过相应的字段名赋值该字段值
* 此方法有3个参数
* fieldName : 字段名
* fieldVal : 字段值
* metaObject: 元对象
*
* 在本业务中,我们修改一个对象,就自动填充更新时间;
* 这里的时间就是当前时间 new Date();
*/
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
```
==注意:不要忘记添加@Componet==,将元对象处理器交给Spring进行管理。
> @Component (把普通pojo实例化到spring容器中,相当于配置文件中的 )
>
> 泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类。
>
>
>
> 如果您还不太了解@Component 注解,请先仔细学习@Componet注解的作用原理与功能
运行测试类的添加和修改操作,看看效果:
![image-20220328221556080](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328221556080.png)
![image-20220328221914289](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220328221914289.png)
### 3.乐观锁
什么是乐观锁?
乐观锁( Optimistic Locking):顾名思义,对加锁持有一种乐观的态度,即先进行业务操作,不到最后一步不进行加锁,"乐观"的认为加锁一定会成功的,在最后一步更新数据的时候再进行加锁。
为什么要使用乐观锁呢?假设,现在同时有多人(至少2个人),主管A和主管B要修改小张的工资。小张的工资为5000元,而主管A认为小张最近表现很好,要增加他的工资,修改他的工资5000元到7000元;另一位主管B认为小张表现令他特别满意,表现十分优秀。并且提议要增加小张的工资由5000元增加到9000元。遇到这种情况怎么办?如果没有乐观锁,那么会出现如下情况:主管A修改5000到7000,而执行主管B的操作时,小张工资不再是5000元,二是7000元。那么5000元到9000元的操作就会执行错误。
那么怎么实现乐观锁呢?很简单,再添加一个字段`version`
version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
sql实现代码:
```sql
update table
set x=x+1, version=version+1
where id=#{id} and version=#{version};
```
> 如果您还有兴趣,可以了解一下悲观锁
现在就开始实现乐观锁。
**(1).修改实体类**
在实体类User中添加新的属性`version`,并添加注解`@Version`,并且在与之对应的数据库添加`int`型`version`字段
```java
@Data
public class User {
//
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
// 驼峰命名 对应的数据库的字段create_time、update_time
@TableField(fill= FieldFill.INSERT)
private Date createTime;
@TableField(fill=FieldFill.INSERT_UPDATE)
private Date updateTime;
@Version
private Integer version;
}
```
**(2).创建配置文件**
创建包config,创建文件`MpConfig.java`,而此时可以删除主类中的`@MapperScan`扫描注解。
**(3).注册乐观锁插件**
在MpConfig中注册`@Bean`
```java
@Configuration
@MapperScan(" com.example.demomp.mapper")
public class MpConfig {
/**
* 乐观锁插件
* 注意在Mybatis-plus3.4.0版本后,放弃此方法乐观锁处理器
* */
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
```
注意:别忘记添加==@Configuration注解==以及==@MapperScan扫描注解==
**4.测试乐观锁**
```java
// 测试乐观锁
@Test
public void OptimisticInsert() {
// 查询ID,获得对象
User user = userMapper.selectById(1508686375965462530L);
// 这里无需设置version,Mybatis-plus会自动帮我们填充修改
user.setAge(25);
// 返回影响行数,修改1个数据,就影响一行
int count = userMapper.updateById(user);
}
```
我们可以看一看测试结果
![image-20220329143601709](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220329143601709.png)
## 查询
**(1).批量查询**
```java
//批量查询
@Test
public void BatchQuery() {
/**
* 批量查询ID为1,2,3,4的用户
* */
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3, 4));
System.out.println(users);
}
```
**(2).条件查询**
```java
//条件查询
@Test
public void conditionQuery() {
/**
* 查询name = 张无忌,age= 25
*
* */
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("name","张无忌");
columnMap.put("age", 25);
List<User> users = userMapper.selectByMap(columnMap);
}
```
## 分页查询
**(1).分页插件**
在Config包中的MpCnnfig类添加分页插件`PaginationInterceptor`
```java
@Configuration
@MapperScan(" com.example.demomp.mapper")
public class MpConfig {
/**
* 乐观锁插件
* 注意在Mybatis-plus3.4.0版本后,放弃此方法乐观锁处理器
*/
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
/**
* 分页查询插件
*
* */
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
```
**(2).分页查询测试**
```java
// 分页查询
@Test
public void PagingQuery() {
// current: 当前页数 size: 每页的记录数
Page<User> page = new Page<>(1, 3);
// 返回对象得到所有数据
Page<User> userPage = userMapper.selectPage(page, null);
long pages = userPage.getPages(); //得到总页数
long current = userPage.getCurrent(); // 当前页数
List<User> records = userPage.getRecords();// 查询数据集合
long size = userPage.getSize();// 查询总记录数
boolean previous = userPage.hasPrevious();//是否有上一页
boolean next = userPage.hasNext();// 是否有下一页
System.out.println(pages);
System.out.println(current);
System.out.println(records);
System.out.println(size);
System.out.println(previous);
System.out.println(next);
}
```
测试一下,看看SQL日志。
![image-20220329154303799](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220329154303799.png)
> 不得不感叹!Mybatis-plus真的太强大了!太方便了!确实帮助我们省却不少时间,几乎不怎需要编写SQL语句。
## 删除与逻辑删除
**(1).根据Id删除**
```java
//根据Id删除
@Test
public void deleteById() {
// 返回影响行数,删除1个数据,就影响一行
int count = userMapper.deleteById(1508714176735117313L);
System.out.println(count);
}
```
**(2).批量删除**
```java
//根据id批量删除
@Test
public void batchDelete() {
// 返回影响行数,删除多个数据,就影响多行
int count = userMapper.deleteBatchIds(Arrays.asList(1508714176735117313L, 1508714176735117313L));
System.out.println(count);
}
```
**(3).条件删除**
```java
//条件查询
@Test
public void conditionDelete() {
/**
* name = 东方不败,age= 20
*
* */
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("name", "东方不败");
columnMap.put("age", 20);
int count = userMapper.deleteByMap(columnMap);
}
```
**(4).逻辑删除**
- 物理删除:==真实删除==,将对应的数据从数据库中,之后查询不到此条被删除数据;
- 逻辑删除:==假删除==,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后再在数据库中仍旧能看到此条数据记录。在表中增加is_delete字段来标识是否删除,**查询时可以根据这个字段跳过**。
> 如果您有兴趣,可以详细了解一下物理删除与逻辑删除的作用场景
逻辑删除的使用场景:
- 可以进行数据恢复;
- 有关联数据,不便删除。
在表添加`Int`型`deleted` 字段,作为逻辑删除标志,每次执行删除时,修改标志位。 `0` 表示没有删除,`1`
表示删除。并且修改实体类,添加新的属性`Intger`型的`deleted`
```java
@Data
public class User {
//
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
// 驼峰命名 对应的数据库的字段create_time、update_time
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
// 乐观锁 Version属性
@TableField(fill = FieldFill.INSERT)
@Version
private Integer version;
// 逻辑删除 deleted属性
@TableLogic
@TableField(fill = FieldFill.INSERT )
private Integer deleted;
}
```
当我们配置==逻辑删除==后,为了演示效果。我们直接在后台修改部分数据的逻辑状态,将它们设为`1`
。权当它们已经被“逻辑删除”了,接下来我们再执行查询所有``findAll`方法。思考一下,在控制台中,会显示全部记录吗?而目前数据库后台有10条记录,我修改其中2条记录的`deleted`为`1`,其他的设为`0`,那么控制台又会显示几条记录呢?
![image-20220329191459219](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220329191459219.png)
![image-20220329191720642](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220329191720642.png)
答案很明显,结果只有8条。不过这里通过sql日志,可以发现,配置完==逻辑删除==后,再执行查询操作,会添加限制条件了`where deleted = 0`,默认查询没有被“逻辑删除”的数据。
**(5).物理删除**
数据可贵!删除请三思!
## 条件构造器和常用接口
文章写到这里,您应该会好奇,怎么没有提及到复杂的条件查询,如`Like`、`Between`、`IsNull`、`>`、`<`等条件查询。那么现在就是这些复杂条件查询亮相的时候了!这部分内容很重要!
### **1. Wrapper简介**
![image-20220329202000874](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220329202000874.png)
- Wrapper : 条件构造抽象类,最顶端父类
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
- QueryWrapper : 查询条件封装
- UpdateWrapper : Update 条件封装
- AbstractLambdaWrapper : 使用 Lambda 语法
- LambdaQueryWrapper :用于 Lambda 语法使用的查询 Wrapper
- LambdaUpdateWrapper : Lambda 更新封装 Wrapper
==在实际业务中QueryWrapper使用得比较多==
### 2、QueryWrapper
**2.1、例1:组装查询条件**
```java
@Test
public void test01() {
//查询用户名包含a,年龄在20到30之间,并且邮箱不为null的用户信息
/* SELECT id,name AS name,age,email,deleted FROM user WHERE
deleted=0 AND (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) */
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("name", "b").between("age", 20, 30).isNotNull("email");
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}
```
**2.2、例2:组装排序条件**
```java
@Test
public void test02() {
//按年龄降序查询用户,如果年龄相同则按id升序排列
/* SELECT id,name AS name,age,email,deleted FROM user WHERE deleted=0 ORDER BY age DESC,id ASC */
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("age").orderByAsc("id");
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
```
**2.3、例3:组装删除条件**
```java
@Test
public void test03() {
//删除email为空的用户
//DELETE FROM user WHERE (email IS NULL)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("email");
//条件构造器也可以构建删除语句的条件
int result = userMapper.delete(queryWrapper);
System.out.println("受影响的行数:" + result);
}
```
**2.4、例4:条件的优先级**
```java
@Test
public void test04() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//将(年龄大于20并且用户名中包含有a)或邮箱为null的用户信息修改
/* UPDATE user SET age=?, email=? WHERE (name LIKE ? AND age > ? OR email IS NULL) */
queryWrapper
.like("name", "a")
.gt("age", 20)
.or()
.isNull("email");
User user = new User();
user.setAge(18);
user.setEmail("1811421199@ccit.stu.edu");
int result = userMapper.update(user, queryWrapper);
System.out.println("受影响的行数:" + result);
}
@Test
public void test04_2() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//将(年龄大于20或邮箱为null)并且用户名中包含有a的用户信息修改
/* UPDATE user SET age=?, email=? WHERE (name LIKE ? AND (age > ? OR email IS NULL)) */
//lambda表达式内的逻辑优先运算
queryWrapper
.like("name", "a")
.and(i -> i.gt("age", 20).or().isNull("email"));
User user = new User();
user.setAge(18);
user.setEmail("1811421199@ccit.stu.edu");
int result = userMapper.update(user, queryWrapper);
System.out.println("受影响的行数:" + result);
}
```
**2.5、例5:组装select子句**
```java
@Test
public void test05() {
//查询用户信息的name和age字段
//SELECT name,age FROM user
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("name", "age");
//selectMaps()返回Map集合列表,通常配合select()使用,避免User对象中没有被查询到的列值为null
List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
}
```
**2.6、例6:实现子查询**
```java
@Test
public void test06() {
//查询id小于等于3的用户信息
/* SELECT id,name AS name,age,email,deleted FROM user WHERE (id IN(select id from user where id <= 3)) */
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.inSql("id", "select id from user where id <= 3");
//selectObjs的使用场景:只返回一列
List<Object> objects = userMapper.selectObjs(queryWrapper);
objects.forEach(System.out::println);
}
```
------
### 3、UpdateWrapper
```java
@Test
public void test07() {
//将(年龄大于20或邮箱为null)并且用户名中包含有a的用户信息修改
//组装set子句以及修改条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
//lambda表达式内的逻辑优先运算
updateWrapper
.set("age", 18)
.set("email", "1811421199@ccit.stu.edu")
.like("name", "a")
.and(i -> i.gt("age", 20).or().isNull("email"));
//这里必须要创建User对象,否则无法应用自动填充。如果没有自动填充,可以设置为null
/* UPDATE user SET name=?, age=?,email=? WHERE (name LIKE ? AND (age > ? OR email IS NULL)) */
User user = new User();
user.setName("张三");
int result = userMapper.update(user, updateWrapper);
/* UPDATE user SET age=?,email=? WHERE (name LIKE ? AND (age > ? OR email IS NULL)) */
int result1 = userMapper.update(null, updateWrapper);
System.out.println(result1);
}
```
------
### 4、condition
在真正开发的过程中,组装条件是常见的功能,而这些条件数据来源于用户输入,是可选的,因此我们在组装这些条件时,必须先判断用户是否选择了这些条件,若选择则需要组装该条件,若没有选择则一定不能组装,以免影响SQL执行的结果。
**思路一:**
```java
@Test
public void test08() {
//定义查询条件,有可能为null(用户未输入或未选择)
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//StringUtils.isNotBlank()判断某字符串是否不为空且长度不为0且不由空白符(whitespace)构成
if (StringUtils.isNotBlank(name)) {
queryWrapper.like("name", "a");
}
if (ageBegin != null) {
queryWrapper.ge("age", ageBegin);
}
if (ageEnd != null) {
queryWrapper.le("age", ageEnd);
}
/* SELECT id,name AS name,age,email,deleted FROM user WHERE (age >=? AND age <= ?) */
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
```
**思路二:**
上面的实现方案没有问题,但是代码比较复杂,我们可以使用带condition参数的重载方法构建查询条件,简化代码的编写:
```java
@Test
public void test08UseCondition() {
//定义查询条件,有可能为null(用户未输入或未选择)
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//StringUtils.isNotBlank()判断某字符串是否不为空且长度不为0且不由空白符(whitespace)构成
queryWrapper
.like(StringUtils.isNotBlank(name), "name", "a")
.ge(ageBegin != null, "age", ageBegin)
.le(ageEnd != null, "age", ageEnd);
/* SELECT id,name AS name,age,email,deleted FROM user WHERE (age >=? AND age <= ?) */
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
```
------
### 5、LambdaQueryWrapper
```java
@Test
public void test09() {
//定义查询条件,有可能为null(用户未输入)
String name = "a";
Integer ageBegin = 10;
Integer ageEnd = 24;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
//避免使用字符串表示字段,防止运行时错误
queryWrapper
.like(StringUtils.isNotBlank(name), User::getName, name)
.ge(ageBegin != null, User::getAge, ageBegin)
.le(ageEnd != null, User::getAge, ageEnd);
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
```
------
### 6、LambdaUpdateWrapper
```java
@Test
public void test10() {
//组装set子句
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper
.set(User::getAge, 18)
.set(User::getEmail, "1811421199@ccit.stu.edu")
.like(User::getName, "a")
.and(i -> i.lt(User::getAge, 24).or().isNull(User::getEmail));
//lambda表达式内的逻辑优先运算
User user = new User();
int result = userMapper.update(user, updateWrapper);
System.out.println("受影响的行数:" + result);
}
```
### **7.测试用例**
1.ge、gt、le、lt、isNull、isNotNull
分别代表`>=`、`>`、`<=`、`<`、`isNull`、`isNotNUll`
首先需要构建条件查询器==QueryWrapper<User> queryWrapper = new QueryWrapper<>();==
```java
@Test
public void select() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// ge、gt、le、lt
queryWrapper.ge("age", 18);
List<User> users = userMapper.selectList(queryWrapper);
System.out.println(users);
}
```
2.eq、ne
分别代表`=`、`!=`
```java
@Test
public void select2() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// eq、ne 分别代表 = , !=
//在这里eq就代表查询年龄为20岁的用户
queryWrapper.eq("age", 20);
List<User> users = userMapper.selectList(queryWrapper);
System.out.println(users);
}
```
3.between、notBetween
between、notBetween分别代表包含和不包含。还需要注意:==between、notBetween包含大小边界==
```java
@Test
public void select3() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//between、notBetween分别代表包含和不包含
//在这里eq就代表查询年龄在18岁和21之岁间的用户(包括18岁和21岁)
queryWrapper.between("age", 18,21);
List<User> users = userMapper.selectList(queryWrapper);
System.out.println(users);
}
```
4.like、notLike、likeLeft、likeRight ==模糊查询== ==双非模糊查询== ==左边模糊查询== ==右边模糊查询==
> %表示匹配任意字符
>
> abc%表示左边是abc,右边是任意字符
>
> %abc表示左边是任意字符,右边是abc
>
> abc%abc表示左右两边是abc,中间是任意字符
>
> %abc%表示中间是abc,左右两边是任意字符
它们分别代表`%列值%`、`%列值、 ` ` 列值%`
selectMaps()返回Map集合,通常配合select()使用
```java
@Test
public void select4() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//它们分别代表
//在这里like就代表模糊查询名字中含“a”的用户
queryWrapper.likeLeft("name", "a");
List<Map<String, Object>> usersMaps = userMapper.selectMaps(queryWrapper);
usersMaps.forEach(System.out::println);
}
```
![image-20220329221005013](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220329221005013.png)
5.orderBy、orderByDesc、orderByAsc
它们分别代表`排序依据`、`降序排列` 、`升序排列`
```java
@Test
public void select5() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 以年龄为分组,查询用户情况,降序排列
queryWrapper.orderByDesc("age")
List<User> usersList = userMapper.selectList(queryWrapper);
usersList.forEach(System.out::println);
}
```
其实,还有一些细节没有介绍到,如 or 和 and 、拼接、子查询、嵌套等条件。
![image-20220329222955942](https://mybolg-typora.oss-cn-beijing.aliyuncs.com/image-20220329222955942.png)
到这里,您已经基本上掌握Mybatis-plus基础知识。如若您想了解更加全民、更加复杂的应用,情投入到实际开发中。实际的开发的SQL会更加复杂多样,因此您还需要熟练SQL语句。学习Mybatish或者Mybatis-plus等ORM框架并不意味着,我们不需要编写SQL语句。Mybatis-plus太方便了,它确实减少了我们的工作量,但它也是危险的。太过于依赖Mybatis-plus框架,兴许导致我们最后只会写CRUD代码了。
==如有错误的地方,请指出==
## 参考
建议您到[Mybatis-plus官网](https://baomidou.com/)上学习,一切以官网的资料为准。在不同的版本,有一些方法有弃用、更改、补充。例如在本教程中,Mybatis-plus中的分页插件在3.4.0已经更改。
如果您不遗忘或者不了解Mybatis,您也可以浏览我的博客文章。→[Mybatis学习笔记](https://docs.zhou431615.xyz/)
也可以了解[Mybatis官网](https://mybatis.org/mybatis-3/zh/configuration.html)
Mybatis-plus入门(通过SpringBoot)