MybatisPlus
入门
1.1 快速入门
1.1.1 新建工程
新建模块,导入依赖坐标
<?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>
<groupId>com.cy</groupId>
<artifactId>mybatisplus_01_quickstart</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- mybaits plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!-- mysql数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- spring-test -->
<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>
<version>1.18.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.1.2 导入数据库
create database if not exists mybatisplus_db character set utf8;
use mybatisplus_db;
DROP TABLE IF EXISTS user;
CREATE TABLE 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(null,'tom','123456',12,'12345678910');
insert into user values(null,'jack','123456',8,'12345678910');
insert into user values(null,'jerry','123456',15,'12345678910');
insert into user values(null,'tom','123456',9,'12345678910');
insert into user values(null,'snake','123456',28,'12345678910');
insert into user values(null,'张益达','123456',22,'12345678910');
insert into user values(null,'张大炮','123456',16,'12345678910');
1.1.3 新建实体类
package com.cy.domain;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
1.1.4 application.yml
中配置Jdbc
参数
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC
username: root
password: root
1.1.5 定义数据接口,继承BaseMapper
package com.cy.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cy.domain.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserDao extends BaseMapper<User> {
}
1.1.6 测试类中注入dao
接口,测试功能
package com.cy;
import com.cy.dao.UserDao;
import com.cy.domain.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
class Mybatisplus01QuickstartApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll() {
List<User> userList = userDao.selectList(null);
System.out.println(userList);
}
}
1.2 MP
简介
1.2.1 MyBatisPlus
介绍
-
MyBatisPlus
(简称MP)是基于MyBatis
框架基础上开发的增强型工具,旨在简化开发、提高效率 -
官网:https://mybatis-plus.com / https://mp.baomidou.com
MyBatisPlus
特性
- 无侵入:只做增强不做改变,不会对现有工程产生影响。导入
MP
后也可以使用原生Mybatis
。 - 强大的
CRUD
操作:内置通用Mapper
,少量配置即可实现单表CRUD
操作 - 支持
Lambda
:编写查询条件无需担心字段写错 - 支持主键自动生成
- 内置分页插件
-
已提供的通用
Mapper
内置的CRUD通用方法功能 BaseMapper
中提供方法备注 新增int insert(T entity)
根据id删除int deleteById(Serializable id)
id
类型不确定,但是都要实现序列化接口 根据id修改int updateById(@Param("et") T entity)
按需修改,动态SQL
根据id查询T selectById(Serializable id)
查询所有List<T> selectList(Wrapper<T> queryWrapper)
分页查询Ipage<E> selectPage(Ipage<E> page,null)
条件查询Ipage<E> selectPage(Ipage<E> page, Wrapper<T> queryWrapper)
-
演示代码
package com.cy; import com.cy.dao.UserDao; import com.cy.domain.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 class Mybatisplus01QuickstartApplicationTests { @Autowired private UserDao userDao; @Test void testSave() { User user = new User(); user.setName("程序员"); user.setPassword("cy"); user.setAge(12); user.setTel("4006184000"); userDao.insert(user); } @Test void testDelete() { userDao.deleteById(1401856123725713409L); } @Test // 已经实现了按需更新 void testUpdate() { User user = new User(); user.setId(1L); user.setName("Tom888"); user.setPassword("tom888"); userDao.updateById(user); } @Test void testGetById() { User user = userDao.selectById(2L); System.out.println(user); } @Test void testGetAll() { List<User> userList = userDao.selectList(null); System.out.println(userList); } }
-
已提供的通用
Mapper
内置的分页查询方法功能 BaseMapper
中提供方法备注 分页查询Ipage<E> selectPage(Ipage<E> page,null)
条件查询Ipage<E> selectPage(Ipage<E> page, Wrapper<T> queryWrapper)
-
演示代码
装配分页拦截器作为Spring管理的bean
package com.cy.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ //1 创建MybatisPlusInterceptor拦截器对象 MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor(); //2 添加分页拦截器 mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mpInterceptor; } }
执行分页查询
//分页查询 @Test void testSelectPage(){ //1 创建IPage分页对象,设置分页参数 IPage<User> page=new Page<>(1,3); //2 执行分页查询 userDao.selectPage(page,null); //3 获取分页结果 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()); }
-
问题描述
使用该插件过程中,常见的问题是总记录数为0。
为0是最容易发现的,其实还有其他问题
- 不是分页查询,而是查了所有记录
SQL
语句中没有添加分页条件
等等。
-
原因
分页拦截器,未配置或者配置错误。
-
解决方法
见3.1.1中装配分页拦截器作为Spring管理的bean
MyBatisPlus
日志
3.2.1 开启日志
# 开启mp的日志(输出到控制台)
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.2.2 简化日志(不推荐)
3.2.2.1 取消初始化spring
日志打印
-
原日志显示如下:
-
取消做法
在
resources
下新建一个logback.xml
文件,名称固定,内容为空,如下:<?xml version="1.0" encoding="UTF-8"?> <configuration> </configuration>
关于logback参考播客:https://www.jianshu.com/p/75f9d11ae011
SpringBoot
启动banner
图标
-
原日志显示如下:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.0)
-
取消做法
spring: main: banner-mode: off # 关闭SpringBoot启动图标(banner)
MybatisPlus
启动banner
图标
-
原日志显示如下:
_ _ |_ _ _|_. ___ _ | _ | | |\/|_)(_| | |_\ |_)||_|_\ / | 3.4.1
-
取消做法
mybatis-plus: global-config: banner: off # 关闭mybatisplus启动图标
QueryWrapper
MyBatisPlus
将书写复杂的SQL
条件进行了封装,使用编程的方式完成查询条件的组合- 所有的查询条件封装在
QueryWrapper
对象中,并且在调用通用方法时传递该对象
简单的查询,建议使用QueryWrapper
;复杂的SQL
语句,不推荐使用;
-
按条件查询
手写字段名,字段名容易写错
//方式一:按条件查询 QueryWrapper<User> qw=new QueryWrapper<>(); // 设置条件,参数分为为字段名和值 // lt : less then 小于 qw.lt("age", 18); List<User> userList = userDao.selectList(qw); System.out.println(userList);
-
lambda
格式按条件查询可以避免字段名写错
//方式二:lambda格式按条件查询 QueryWrapper<User> qw = new QueryWrapper<User>(); User user /* = 获取一个user对象*/; qw.lambda().lt(User::getName,user.getName()); List<User> userList = userDao.selectList(qw); System.out.println(userList);
-
lambda
格式按条件查询(推荐)//方式三:lambda格式按条件查询 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>(); // LambdaQueryWrapper支持链式编程 lqw.lt(User::getAge, 10); // qw.lambda().lt(User::getName,user.getName()); List<User> userList = userDao.selectList(lqw); System.out.println(userList);
-
并且关系(and)
设置条件时默认是并且关系
// 并且关系,默认就是并且关系 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //10到30岁之间 lqw.lt(User::getAge, 30).gt(User::getAge, 10); List<User> userList = userDao.selectList(lqw); System.out.println(userList);
-
或者关系(or)
// 或者关系,需要通过or()方法连接 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); / 小于10岁或者大于30岁 lqw.lt(User::getAge, 10).or().gt(User::getAge, 30); List<User> userList = userDao.selectList(lqw); System.out.println(userList);
QueryWrapper
方法与功能
-
常用方法
方法 功能 备注 eq 指定列的值相同 equals的简写 ne 指定列的值不相同 not equals的简写 lt 指定列的值小于 less then的简写 le 指定列的值小于等于 less then and equals的简写 gt 指定列的值大于 great then 的简写 ge 指定列的值大于等于 great then and equals的简写 between 指定列的值在..和…之间 notBetween 指定列的值不在..和…之间 like 指定列的值等值模糊查询 notLIke 指定列的值不等值模糊查询 orderByAsc 按照指定的列升序排序 orderByDesc 按照指定的列降序排序 groupBy 按照指定的列分组 having 按照指定的列在分组后条件过滤 in 单条件多值查询 isNull 指定列的值为空 isNOtNull 指定列的值不为空 -
用户登录(eq匹配)
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
//等同于=
lqw.eq(User::getName, "Jerry").eq(User::getPassword, "jerry");
User loginUser = userDao.selectOne(lqw);
System.out.println(loginUser);
- 购物设定价格区间、户籍设定年龄区间(le ge匹配 或 between匹配)
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
//范围查询 lt le gt ge eq between
lqw.between(User::getAge, 10, 30);
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
- 查信息,搜索新闻(非全文检索版:like匹配)
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
//模糊匹配 like
lqw.likeLeft(User::getName, "J");
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
- 统计报表(分组查询聚合函数)
QueryWrapper<User> qw = new QueryWrapper<User>();
qw.select("gender","count(*) as nums");
qw.groupBy("gender");
List<Map<String, Object>> maps = userDao.selectMaps(qw);
System.out.println(maps);
官方API
文档
- 更多查询条件设置参看
https://mybatis.plus/guide/wrapper.html#abstractwrapper
在多条件查询中,有条件的值为空会影响查询结果?
3.3.4.1 常规处理方式手动判空
Integer minAge=10; //将来有用户传递进来,此处简化成直接定义变量了
Integer maxAge=null; //将来有用户传递进来,此处简化成直接定义变量了
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
if(minAge!=null){
lqw.gt(User::getAge, minAge);
}
if(maxAge!=null){
lqw.lt(User::getAge, maxAge);
}
List<User> userList = userDao.selectList(lqw);
userList.forEach(System.out::println);
3.3.4.2 QueryWrapper
判空
-
准备查询参数封装对象
@Data public class User { private Long id; private String name; private String password; private Integer age; private String tel; private Integer online; } @Data public class UserQuery extends User { private Integer age2; }
-
测试类中,使用
QueryWrapper
判空LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //先判定第一个参数是否为true,如果为true连接当前条件 //lqw.lt(null != uq.getAge2(),User::getAge, uq.getAge2()); //lqw.gt(null != uq.getAge(),User::getAge, uq.getAge()); // 链式编程 lqw.lt(null != uq.getAge2(),User::getAge, uq.getAge2()) .gt(null != uq.getAge(),User::getAge, uq.getAge()); List<User> userList = userDao.selectList(lqw); System.out.println(userList);
查询投影,就是要查询的列的问题。可能存在
- 只查询表中存在的部分字段,
- 或者查询表中没有的字段(聚合查询)
复杂查询不推荐使用QueryWrapper
,使用Mybatis
原生方式,自己编写SQL。
/*LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.select(User::getId, User::getName, User::getAge);*/
//或者
QueryWrapper<User> lqw = new QueryWrapper<User>();
// 只指定部分类
lqw.select("id", "name", "age", "tel");
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
3.4.2 查询结果包含模型类中未定义的属性
QueryWrapper<User> lqw = new QueryWrapper<User>();
// count(*)不存在于表结构中国的字段
lqw.select("count(*) as count, tel");
lqw.groupBy("tel");
List<Map<String, Object>> userList = userDao.selectMaps(lqw);
System.out.println(userList);
3.5 字段映射与表名映射
3.5.1 问题一:表字段与实体属性名不一致
- 在模型类属性上方,使用
@TableField
属性注解,通过value属性,设置当前属性对应的数据库表中的字段关系。
-
在模型类属性上方,使用
@TableField
注解,通过exist属性,设置属性在数据库表字段中是否存在,默认为true。此属性无法与value合并使用。查询和增删改的时候,都不会参与数据库的操作。
- 在模型类属性上方,使用
@TableField
注解,通过select属性:设置该属性是否参与默认查询(select *
)。此属性与select()映射配置不冲突。
- 在模型类上方,使用@TableName注解,通过value属性,设置当前类对应的数据库表名称。
@Data
@TableName("tbl_user")
public class User {
/*
id为Long类型,因为数据库中id为bigint类型,
并且mybatis有自己的一套id生成方案,生成出来的id必须是Long类型
*/
private Long id;
private String name;
@TableField(value = "pwd",select = false) // 仅仅是不参与查询
private String password;
private Integer age;
private String tel;
@TableField(exist = false) //表示online字段不参与CRUD操作
private Boolean online;
}
4. 增删改DML
4.1 增:主键生成策略
4.1.1 不同的表的id生成策略不同
- 日志:自增(1,2,3,4,……)
- 购物订单:特殊规则(FQ23948AK3843)
- 外卖单:关联地区日期等信息(10 04 20200314 34 91)
- 关系表:可省略id
- ……
MybatisPlus
支持的生成策略
4.1.2.1MybatisPlus
支持8种生成策略
-
五种可用,三种已过期。详情如下:
public enum IdType { /** * 数据库ID自增 * <p>该类型请确保数据库设置了 ID自增 否则无效</p> */ AUTO(0), /** * 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) */ NONE(1), /** * 用户输入ID * <p>该类型可以通过自己注册自动填充插件进行填充</p> */ INPUT(2), /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */ /** * 分配ID (主键类型为number或string), * 默认使用com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator(雪花算法) * * @since 3.3.0 */ ASSIGN_ID(3), /** * 分配UUID (主键类型为 string) * 默认使用 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); }
-
使用注解
@TableId
-
名称:
@TableId
-
类型:属性注解
-
位置:模型类中用于表示主键的属性定义上方
-
作用:设置当前类中主键属性的生成策略
-
相关属性
type:设置主键属性的生成策略,值参照
IdType
枚举值
-
-
application.yaml
mybatis-plus: global-config: db-config: id-type: assign_id # 全局使用雪花算法生成主键 table-prefix: tbl_ # 全局指定表名前缀,实体类上就不需要添加@TableName()注解了
//删除指定多条数据
List<Long> list = new ArrayList<>();
list.add(1402551342481838081L);
list.add(1402553134049501186L);
list.add(1402553619611430913L);
userDao.deleteBatchIds(list);
4.2.2 根据主键查询多条记录
//查询指定多条数据
List<Long> list = new ArrayList<>();
list.add(1L);
list.add(3L);
list.add(4L);
userDao.selectBatchIds(list);
4.2.3 逻辑删除(Delete/Update)
4.2.3.1 概念
在实际环境中,如果想删除一条数据,是否会真的从数据库中删除该条数据?
-
删除操作业务问题:业务数据从数据库中丢弃
-
逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中
-
数据库表中添加逻辑删除标记字段
-
实体类中添加对应成员变量,并(逐个指定)标记对应属性
package com.cy.domain; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; @Data public class User { private Long id; //逻辑删除字段,标记当前记录是否被删除 // value表示未删除,delval表示已删除 @TableLogic(value="0", delval="1") private Integer deleted; }
-
实体类中添加成员变量后,也可以全局配置
mybatis-plus: global-config: db-config: table-prefix: tbl_ # 逻辑删除字段名 logic-delete-field: deleted # 逻辑删除字面值:未删除为0 logic-not-delete-value: 0 # 逻辑删除字面值:删除为1 logic-delete-value: 1
逻辑删除本质:逻辑删除的本质其实是修改操作。如果加了逻辑删除字段,查询数据时也会自动带上逻辑删除字段。
4.3 改:乐观锁 4.3.1 概念-
概念
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不用上锁。只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
-
实现思路
使用数据版本(Version)记录:
- 这最常用的一种实现方式。为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的
version
字段来实现。 - 当读取数据时,将
version
字段的值一同读出,数据每更新一次,对此version
值加一。 - 当提交更新的时候,判断数据库表对应记录的当前版本信息与取出来的version值进行比对,如果数据库表当前版本号与之前取出来的
version
值相等,则予以更新,否则认为是过期数据。
CAS:Compare And Swap
使用时间戳记录
- 这最常用的一种实现方式。为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的
-
数据库表中添加锁标记字段
-
实体类中添加对应字段,并设定当前字段为版本字段
package com.cy.domain; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.Version; import lombok.Data; @Data public class User { private Long id; @Version private Integer version; }
-
配置乐观锁拦截器实现锁机制对应的动态
SQL
语句拼装package com.cy.config; 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.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MpConfig { @Bean public MybatisPlusInterceptor mpInterceptor() { //1.定义Mp拦截器 MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor(); //2.添加乐观锁拦截器 mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return mpInterceptor; } }
-
使用乐观锁机制在修改前必须先获取到对应数据的
verion
方可正常进行@Test public void testUpdate() { /*User user = new User(); user.setId(3L); user.setName("Jock666"); user.setVersion(1); userDao.updateById(user);*/ //1.先通过要修改的数据id将当前数据查询出来 //User user = userDao.selectById(3L); //2.将要修改的属性逐一设置进去 //user.setName("Jock888"); //userDao.updateById(user); //1.先通过要修改的数据id将当前数据查询出来 User user = userDao.selectById(3L); //version=3 User user2 = userDao.selectById(3L); //version=3 user2.setName("Jock aaa"); userDao.updateById(user2); //version=>4 user.setName("Jock bbb"); userDao.updateById(user); //verion=3?条件还成立吗? }