MyBatisPlus
今日目标
基于MyBatisPlus完成标准Dao的增删改查功能
掌握MyBatisPlus中的分页及条件查询构建
掌握主键ID的生成策略
了解MyBatisPlus的代码生成器
本节主要讲的内容如下:
1,MyBatisPlus入门案例与简介
这一节我们来学习下MyBatisPlus的入门案例与简介,这个和其他课程都不太一样,其他的课程都是先介绍概念,然后再写入门案例。而对于MyBatisPlus的学习,我们将顺序做了调整,先讲入门案例再介绍概念。这么做的主要的原因是MyBatisPlus主要是对MyBatis的开发进行简化(就像SpringBoot一样,SpringBoot是对Spring的开发进行简化的一个框架),下面我们先体会下它简化在哪,然后再学习它是什么,以及它帮我们都做哪些事。
1.1 入门案例
-
MybatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提供效率。
-
MybatisPlus的开发:
- 基于MyBatis使用MyBatisPlus
- 基于Spring使用MyBatisPlus
- 基于SpringBoot使用MyBatisPlus
SpringBoot刚刚我们学习完成,它能快速构建Spring开发环境用以整合其他技术,使用起来是非常简单,对于MP的学习(这个老师习惯将MybatisPlus简称为MP),我们也基于SpringBoot来构建学习。
学习之前,我们先来回顾下,SpringBoot整合Mybatis的开发过程:
-
创建SpringBoot工程
-
勾选配置使用的技术,能够实现自动添加起步依赖包
-
设置dataSource相关属性(JDBC参数)
-
定义数据层接口映射配置
我们可以参考着上面的这个实现步骤把SpringBoot整合MyBatis项目搭建好。
下面我们来把SpringBoot整合MyBatisPlus来快速实现下,具体的实现步骤为:
步骤1:创建数据库及表
sql
create database if not exists mybatisplus_db character set utf8;
use mybatisplus_db;
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(1,'Tom','tom',3,'18866668888');
insert into user values(2,'Jerry','jerry',4,'16688886666');
insert into user values(3,'Jock','123456',41,'18812345678');
insert into user values(4,'传智播客','itcast',15,'4006184000');

步骤2:创建SpringBoot工程
其实这里讲的是,创建一个SpringBoot的model
步骤3:勾选配置使用技术

说明:
- 由于MP并未被收录到idea的系统内置配置,无法直接选择加入,需要手动在pom.xml中配置添加
步骤4:pom.xml补全依赖
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>

说明:
-
druid数据源可以加也可以不加,不加的话,这个项目会用SpringBoot内置的数据源,你加了druid的坐标的话,可以通过SpringBoot的配置文件让这个项目使用Druid作为本项目运行的数据源
-
从MyBatisPlus的起步依赖中可以看出,它通过依赖传递已经将MyBatis与MyBatis整合Spring的jar包导入了,所以我们不需要额外再添加MyBatis的相关jar包了
步骤5:添加MP的相关配置信息
resources默认生成的是properties配置文件,可以将其改成yml文件,并在文件中配置数据库连接的相关信息:application.yml
yml
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
说明: serverTimezone是用来设置时区,UTC是标准时区,和咱们的时间差8小时,所以你可以将其修改为Asia/Shanghai
,即,指定服务器当前是以亚洲上海时区存储内容的
步骤6:根据数据库表创建实体类
java
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
//setter...getter...toString方法略
}
步骤7:创建Dao接口
java
@Mapper
public interface UserDao extends BaseMapper<User>{
}
步骤8:编写引导类
java
@SpringBootApplication
//@MapperScan("com.itheima.dao")
public class Mybatisplus01QuickstartApplication {
public static void main(String[] args) {
SpringApplication.run(Mybatisplus01QuickstartApplication.class, args);
}
}
**说明:**Dao接口要想被容器扫描到,有两种解决方案:
- 方案一:在Dao接口上添加
@Mapper
注解,并且确保Dao处在引导类所在包或其子包中- 该方案的缺点是需要在每一Dao接口中添加注解
- 方案二:在引导类上添加
@MapperScan
注解,其属性为所要扫描的Dao所在包- 该方案的好处是只需要写一次,则指定包下的所有Dao接口都能被扫描到,这样那个包下的接口的
@Mapper
就可以不写了。
- 该方案的好处是只需要写一次,则指定包下的所有Dao接口都能被扫描到,这样那个包下的接口的
步骤9:编写测试类
java
@SpringBootTest
class MpDemoApplicationTests {
@Autowired
private UserDao userDao;
@Test
public void testGetAll() {
List<User> userList = userDao.selectList(null);
System.out.println(userList);
}
}
说明:
测试类这里userDao注入的时候下面有红线提示的原因是什么?
- UserDao是一个接口,不能实例化对象
- 只有在服务器启动IOC容器初始化后,由框架创建DAO接口的代理对象并自动放到容器里后,才能注入
- 现在服务器并未启动,所以代理对象也未创建,IDEA当然查找不到对应的对象注入,所以提示报红
- 一旦服务启动,就能注入其代理对象,所以该错误提示不影响正常运行。
查看运行结果:

跟之前整合MyBatis相比,你会发现我们不需要在DAO接口中编写方法和SQL语句了,只需要继承BaseMapper
接口即可。整体来说简化很多。
1.2 MybatisPlus简介
MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提高效率
通过刚才的案例,相信大家能够体会简化开发和提高效率这两个方面的优点了。
MyBatisPlus的官网为:https://mp.baomidou.com/
,或者https://mybatis.plus/
(后面这个域名是别人捐给他的,因为MyBatisPlus创建者要搞一个官网的时候,发现https://mybatis.plus/
已经被别人占了,所以他就创建了https://mp.baomidou.com/
域名作为MyBatisPlus的官网了,但是后来那个创建https://mybatis.plus/
域名的那个人又把域名捐给他了)
但是我发现这两个网址的内容也不是完全一样的,所以大家还是使用baomidou的网址进行访问吧。
官方文档中有一张很多小伙伴比较熟悉的图片:

从这张图中我们可以看出MP旨在成为MyBatis的最好搭档,而不是替换MyBatis,所以可以理解为MP是MyBatis的一套增强工具,它是在MyBatis的基础上进行开发的,我们虽然使用MP但是底层依然是MyBatis的东西,所以我们也可以在MP中写MyBatis的内容。
对于MP的学习,大家可以参考着官网的快速开始中的文档来进行学习,里面都有详细的代码案例。
MP的特性:
- 无侵入:只做增强不做改变,不会对现有工程产生影响
- 强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作
- 支持 Lambda:编写查询条件无需担心字段写错
- 支持主键自动生成
- 内置分页插件
- ......
2,标准数据层开发
在这一节中我们重点学习的是数据层标准的CRUD(增删改查)的实现与分页功能。代码比较多,我们一个个来学习。
2.1 标准CRUD使用
对于标准的CRUD功能都有哪些以及MP都提供了哪些方法可以使用呢?
我们看下面这张图就知道了:

对于这张图的方法,我们挨个来演示下:
首先说下,下面案例中的环境就是咱们入门案例的内容,第一个先来完成新增
功能
2.2 新增
在进行新增之前,我们可以分析下新增的方法:
java
int insert (T t)
-
T:泛型,新增用来保存新增数据
-
int:返回值,新增成功后返回1,没有新增成功返回的是0
在测试类中进行新增操作:
java
@SpringBootTest
class Mybatisplus01QuickstartApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testSave() {
User user = new User();
user.setName("黑马程序员");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
}

执行测试后,数据库表中就会添加一条数据。

但是数据中的主键ID,有点长,那这个主键ID是如何来的?因为这里的id是自增的,且我们插入的记录的id是n值ull,那么插入后id应该是5才对,这个就是我们后面要学习的主键ID生成策略造成的,这块的这个问题,我们暂时先放放。
2.3 删除
我们再执行一次上面的插入方法,插入后的数据库如下:

这个数据我们到时候用来测试删除。
在进行删除之前,我们可以分析下删除的方法:
java
int deleteById (Serializable id)
-
Serializable:参数类型
-
思考:参数类型为什么是一个序列化类?
从这张图可以看出,
- String和Number是Serializable的子类,
- Number又是Float,Double,Integer等类的父类,
- 能作为主键的数据类型都已经是Serializable的子类,
- MP使用Serializable作为参数类型,就好比我们可以用Object接收任何数据类型一样。
-
-
int:返回值类型,数据删除成功返回1,未删除数据返回0。
在测试类中执行删除测试:
java
@SpringBootTest
class Mybatisplus01QuickstartApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testDelete() {
userDao.deleteById(1401856123725713409L);
}
}

结果:

删除成功。
2.4 修改
在进行修改之前,我们可以分析下修改的方法:
java
int updateById(T t);
-
T:泛型,需要修改的数据内容,注意因为这个方法是根据传入的实体类的ID属性进行修改的,所以传入的对象中需要有ID属性值
-
int:返回值,修改成功后返回1,未修改数据返回0
在测试类中进行新增操作:
java
@SpringBootTest
class Mybatisplus01QuickstartApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate() {
User user = new User();
user.setId(1L);
user.setName("Tom888");
user.setPassword("tom888");
userDao.updateById(user);
}
}

结果:

**说明:**修改的时候,只修改实体对象中有值的字段,比如上面例子里updateById(user)方法执行的时候,这个方法中传的user对象里面属性值为null的属性对应的数据库字段是不会被修改的。
2.5 根据ID查询
在进行根据ID查询之前,我们可以分析下根据ID查询的方法:
java
T selectById (Serializable id)
- Serializable:参数类型,主键ID的值
- T:根据ID查询返回一条数据
在测试类中进行新增操作:
java
@SpringBootTest
class Mybatisplus01QuickstartApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetById() {
User user = userDao.selectById(2L);
System.out.println(user);
}
}
结果:

2.6 查询所有
在进行查询所有之前,我们可以分析下查询所有的方法:
java
List<T> selectList(Wrapper<T> queryWrapper)
- Wrapper:用来构建条件查询的条件,目前我们还不会写这个条件,我们可直接传为Null,传null的话,这个方法表示查询全部。
- List:因为查询的是所有,所以返回的数据是一个集合
在测试类中进行新增操作:
java
@SpringBootTest
class Mybatisplus01QuickstartApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll() {
List<User> userList = userDao.selectList(null);
System.out.println(userList);
}
}
这个方法我就不测试了,入门案例里面就有测试过这个方法。
可以看到,虽然我们没有写Dao类的方法和sql,但是我们却已经可以使用很多数据库相关的方法了

其实,我们所调用的这些方法都是来自于DAO接口继承的BaseMapper类中的(注意,继承的时候,要写一个泛型,表示这个接口是操作哪个实体类的,这样,我们继承的这个BaseMapper类才知道CRUD要对应到哪个实体类,然后通过这个实体类生成对应的sql去执行,最后把结果返回),这个类里面的方法有很多,我们后面会慢慢去学习里面的内容。
2.7 Lombok
代码写到这,我们会发现DAO接口类的编写现在变成最简单的了,里面什么都不用写。反过来看看模型类的编写都需要哪些内容:
- 私有属性
- setter...getter...方法
- toString方法
- 构造函数
上面这些内容我们都是可以通过IDEA工具自动生成的,虽然不难,但是过程还是必须得走一遍,那么对于模型类的编写有没有什么优化方法?
这就是我们接下来要学习的Lombok。
概念
- Lombok,一个Java类库,提供了一组注解,可以这些注解简化我们的POJO实体类开发。
使用步骤
步骤1:添加lombok依赖
xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!--<version>1.18.12</version>-->
</dependency>
**注意:**版本可以不用写,因为SpringBoot中已经管理了lombok的版本。

步骤2:安装Lombok的插件
新版本IDEA已经内置了该插件,如果你是旧版的idea,没有内置这个插件,那么就需要你自己手动去idea插件商店里去安装一下这个插件了,或者你离线安装也行

如果在IDEA中找不到lombok插件,可以访问如下网站
https://plugins.jetbrains.com/plugin/6317-lombok/versions
根据自己IDEA的版本下载对应的lombok插件,下载成功后,在IDEA中采用离线安装的方式进行安装。

步骤3:模型类上添加注解
Lombok常见的注解有:
- @Setter:为当前模型类的属性提供setter方法
- @Getter:为当前模型类的属性提供getter方法
- @ToString:为当前模型类的属性提供toString方法
- @RequiredArgsConstructor:这个注解我们稍微了解一下就行了。如果一个类需要把容器里面管理的对象依赖注入到这个类的某些属性中,那么我们可以使用这个注解来帮我们注入,当然哈,我们直接用之前的
@Autowired
、@Resource
注入、或者用构造注入也行。而且一般,我们实体类的属性不需要进行依赖注入,所以这里就了解一下这个注解就行了。 - @EqualsAndHashCode:为当前模型类的属性提供equals和hashcode方法
- @Data:是个组合注解,相当于上面的五个注解加在一起的功能
- @NoArgsConstructor:为当前模型类提供一个无参构造函数
- @AllArgsConstructor:为当前模型类提供一个包含所有参数的构造函数
Lombok的注解还有很多,上面标高亮的三个注解是比较常用的,其他的大家后期用到了,再去补充学习。
测试:
java
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}



说明:
Lombok只是简化模型类的编写,当然我们之前的自己写setter、getter和构造方法的方式也能用,比如有人会问:我如果只想要有name和password的构造函数,该如何编写?
可以像下面这样,直接写你要的那个构造方法就行了:
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
public User(String name, String password) {
this.name = name;
this.password = password;
}
}
这种方式是被允许的。
注解不能给你自动生成的方法,你直接自己写就行了。
2.8 分页功能
基础的增删改查就已经学习完了,刚才我们在分析基础开发的时候,有一个分页功能还没有实现,在MP中如何实现分页功能,就是咱们接下来要学习的内容。
分页查询使用的方法是:
java
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper)
- IPage:用来构建分页查询条件
- Wrapper:用来构建条件查询的条件,目前我们没有学,可直接传为Null,
- IPage:返回值,你会发现构建分页条件和方法的返回值都是IPage
IPage是一个接口,我们需要找到它的实现类来构建它,具体的实现类,可以进入到IPage类中按ctrl+h,然后你会找到他只有一个实现类为Page
,那么我们就用这个实现类就行了。
步骤1:调用方法传入参数获取返回值
java
@SpringBootTest
class Mybatisplus01QuickstartApplicationTests {
@Autowired
private UserDao userDao;
//分页查询
@Test
void testSelectPage(){
//1 创建IPage分页对象,设置分页参数,1为当前页码,3为每页显示的记录数。相当于是你告诉别人把查询的结果按每页3条记录来分页,并要求别人到时候把第1页的所有记录都返回
IPage<User> page=new Page<>(1,3);
//2 执行分页查询
userDao.selectPage(page,null);//调用后,会自动把结果放在我们传进来的Page对象里面。
//3 获取分页结果。下面我们通过Page对象来看看拿到的数据是什么。
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());
}
}

步骤2:设置分页拦截器
这个拦截器MP已经为我们提供好了,我们只需要将其配置成Spring管理的bean对象即可。
java
//SpringBoot启动类可以扫描到启动类所在的包或者其子包中类的注解,所以我们在配置类上面写一个@Configuration,这个类就会生效了。不用在启动类上面添加扫描什么的。
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1 创建MybatisPlusInterceptor拦截器对象。这个对象里面其实是可以添加多个拦截器的,这里我们的需求只要添加一个分页拦截器就行了。
MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();
//2 添加分页拦截器。
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}


**说明:**上面的代码记不住咋办呢?
这些内容在MP的官方文档中有详细的说明,我们可以查看官方文档类配置

步骤3:运行测试程序

我们要让他每页5条记录地分页,然后显示第一页的记录,我们只要改一下selectPage(page,null)中page对象的属性current和size属性就行了:

如果想查看MP执行的SQL语句,可以修改application.yml配置文件,添加日志的配置就行了
yml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印SQL日志到控制台

打开日志后,就可以在控制台打印出对应的SQL语句。但是注意,开启日志功能,其实是会影响程序的性能的,我们调试的时候可以借助这个日志来帮助我们开发,但是调试完后要记得关闭。

2.9 扩展一下@RequiredArgsConstructor
前面我们讲lombok的时候,说这个@RequiredArgsConstructor注解我们就了解一下。这里我想研究一下这个注解,因为这个注解可能可以加快我的开发速度。我们先来看一篇博客来学习一下这个知识点。
lombok 使用 @RequiredArgsConstructor 注解完成 spring 注入问题
本知识点来自:https://www.knowledgedict.com/tutorial/lombok-spring-injection.html#@RequiredArgsConstructor 注入
Spring 依赖注入方式主要有 2 种,一是通过
@Autowire
、@Resource
等注解注入,二是通过构造器的方式进行依赖注入。除此之外,其实 lombok 的@RequiredArgsConstructor
注解也可以完成 spring 的依赖注入,且更简便,更灵活。
- @Autowire、@Resource 等注入方式
- 显性构造器注入
- @RequiredArgsConstructor 注入
@Autowire、@Resource 等注入方式
目前使用最广泛的是
@Autowired
、@Resource
注入方式:
java@Service public class KnowledgeDictServiceImpl implements KnowledgeDictService { @Autowired private PushService pushService; @Resource private AdminService adminService; }
显性构造器注入
spring 通过显性构造器注入依赖,示例如下:
java@Service public class KnowledgeDictServiceImpl implements KnowledgeDictService { private final PushService pushService; private final AdminService adminService; public KnowledgeDictServiceImpl(PushService pushService, AdminService adminService) { this.pushService = pushService; this.adminService = adminService; } }
@RequiredArgsConstructor 注入
@RequiredArgsConstructor
注解可以更简洁地实现构造器的注入,无需显性定义,示例如下:
java@Service @RequiredArgsConstructor public class KnowledgeDictServiceImpl implements KnowledgeDictService { final PushService pushService; final AdminService adminService; }
从显性构造器注入和
@RequiredArgsConstructor
对比可看出,其实它们和 lombok 的@NoArgsConstructor
和@AllArgsConstructor
注解设计是一脉相承的;
@RequiredArgsConstructor
本质上是为每个需要特殊处理的字段生成一个带有 1 个参数的构造函数。所有未初始化的 final 字段都获得一个参数,或标有@NonNull
注解且未初始化的字段也会从构造函数获得一个参数;参数的顺序与字段在类中出现的顺序一致。
@NoArgsConstructor
顾名思义是生成一个没有参数的构造函数,如果有未初始化的final
字段,会导致编译错误。
@AllArgsConstructor
则是为类中的每个字段生成一个带有 1 个参数的构造函数,常用在 pojo 类上。
总之,就是,如果我们类上面使用@RequiredArgsConstructor
这个注解的话,那么,我们这个类里面所有未初始化的final
修饰的字段和标有 @NonNull
注解且未初始化的字段都会被自动注入,自动把容器里面对应的类型的bean对象注入进来,就不用我们和之前那样,在每个要注入的字段上面写@Autowired
或者@Resource
或者自己写构造方法进行构造注入了。使用这个注解,就相当于自动生成一个构造方法,方法的形参是,这个类里面所有未初始化的final
修饰的字段和标有 @NonNull
注解且未初始化的字段,然后自动通过这个构造方法,把IOC里面的bean注入进来。
看到,虽然我们一般实体类里面不需要进行注入,但是如果我们把@RequiredArgsConstructor
注解用在我们Controller层和Service层,就可以减少我们很多工作量了。他又没有说lombok中的注解只能用在实体类里面,是吧!
要注意几点:
- 你要注入的属性你用final修饰或者用@NonNull修饰(推荐使用final)
- 你即使属性设置了是final修饰或者用@NonNull修饰,但是你给了初始化值,他就不会自动注入了,就直接用你给的初始化的值。
- 是@NonNull不是@@NotNull
2.9.1 了解一下构造注入的案例
2.9.1.1 测试1
我们先看看构造注入,之前我们都没有怎么讲注解的构造注入,这里讲一下。
先看看环境:








执行结果:

2.9.1.2 测试2


解决:

运行:

相当于构造注入只要在构造方法上面加上@Autowired就行了。默认是按类型注入的,如果容器里面这个类型的bean有多个,那么就按照形参名去容器里面找对应bean,进行注入。
但是注意一点:就是



2.9.2 测试@AllArgsConstructor
2.9.2.1 测试1

然后修改Demo类如下:其他的不变。

运行结果:

解决:

运行:

但是上面我们删了一个stringDemo属性,我们才能成功,但是实际开发中,不是我们随便能删除属性的,万一这个类就是需要这个属性呢?你把它删了,这怎么行呢?是吧。等一下我们来讲更好的方法,这里其实就是想展示一下,这个@AllArgsConstructor也能注入,相当于构造注入,但是是把全部属性都进行构造注入的。
2.9.3 测试@RequiredArgsConstructor
2.9.3.1 测试1
上面使用@AllArgsConstructor注解相当于是生成一个所有属性的构造方法,然后进行注入的。可以看到上面有一定的弊端,所以这里我们用更好的方式来解决这个问题:

运行:

解决方式一:

解决方式二:

2.9.3.2 测试2

2.9.3.3 测试3



2.9.3.4 测试4
修改一下DemoServiceImpl1和DemoServiceImpl2类,如下:



2.9.3.5 测试5
我们再创建一个类:



结论:@RequiredArgsConstructor可以同时注入多个bean,你没有被final或者@NonNull修饰的属性不会被注入。
3,DQL编程控制
增删改查四个操作中,查询是非常重要的也是非常复杂的操作,这块需要我们重点学习下,这节我们主要学习的内容有:
- 条件查询方式
- 查询投影
- 查询条件设定
- 字段映射与表名映射
3.1 条件查询
3.1.1 条件查询的类
- MyBatisPlus将书写复杂的SQL查询条件进行了封装,使用编程的形式完成查询条件的组合。
这个我们在前面都有见过,比如查询所有和分页查询的时候,都有看到过一个Wrapper
类,这个类就是用来构建查询条件的,如下图所示:

那么条件查询如何使用Wrapper来构建呢?
3.1.2 环境构建
在构建条件查询之前,我们先来准备下环境
-
创建一个SpringBoot项目
-
pom.xml中添加对应的依赖
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>2.5.0</version> </parent> <groupId>com.itheima</groupId> <artifactId>mybatisplus_02_dql</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
编写UserDao接口
java@Mapper public interface UserDao extends BaseMapper<User> { }
-
编写模型类
java@Data public class User { private Long id; private String name; private String password; private Integer age; private String tel; }
-
编写引导类
java@SpringBootApplication public class Mybatisplus02DqlApplication { public static void main(String[] args) { SpringApplication.run(Mybatisplus02DqlApplication.class, args); } }
-
编写配置文件
yml# dataSource 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 # mp日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
-
编写测试类
java@SpringBootTest class Mybatisplus02DqlApplicationTests { @Autowired private UserDao userDao; @Test void testGetAll(){ List<User> userList = userDao.selectList(null); System.out.println(userList); } }
最终创建的项目结构为:
-
测试的时候,控制台打印的日志比较多,如下图:
这样的话,运行的速度会有点慢而且不利于查看运行结果,所以接下来我们把这个日志处理下:
-
取消初始化spring日志打印。做法是:在resources目录下创建logback.xml,名称固定,内容如下:
xml<?xml version="1.0" encoding="UTF-8"?> <configuration> </configuration>
**说明:**logback.xml的配置内容,不是我们学习的重点,如果有兴趣可以自行百度查询。
-
取消MybatisPlus启动banner图标
做法是:application.yml添加如下内容:
yml# mybatis-plus日志控制台输出 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false # 关闭mybatisplus启动图标
-
取消SpringBoot的log打印
做法:application.yml添加如下内容:
ymlspring: main: banner-mode: off # 关闭SpringBoot启动图标(banner)
-
最终效果:
-
解决控制台打印日志过多的相关操作可以不用去做,这个操作一般会被用来方便我们查看程序运行的结果的。
3.1.3 构建条件查询
接下来我们来看看Wrapper是什么。因为Wrapper是一个抽象类,所以我们需要去找它对应的实现类,他的实现类也有很多,说明我们有多种构建查询条件对象的方式:

- 先来看第一种:QueryWrapper
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
//创建QueryWrapper对象
QueryWrapper qw = new QueryWrapper();
//lt("age",18)表示设置查询条件是age字段小于18
qw.lt("age",18);
List<User> userList = userDao.selectList(qw);
System.out.println(userList);
}
}

-
lt: 小于(<) ,上面代码执行最终的sql语句为
sqlSELECT id,name,password,age,tel FROM user WHERE (age < ?)
数据库的数据如下:

我们看看执行的结果:

第一种方式介绍完后,有个小问题就是在写条件的时候,容易出错,比如age写错,就会导致查询不成功
- 接着来看第二种:QueryWrapper的基础上使用lambda
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
//注意,使用这个lambda的这种方式需要指定QueryWrapper的泛型(查询结果对应什么类,就写什么泛型),前面不用lambda表达式的条件查询方式可以不用指定这个泛型
QueryWrapper<User> qw = new QueryWrapper<User>();
//把原来的qw.lt("age",18);写为qw.lambda().lt(User::getAge, 10);,这样是指设置查询条件是User的age属性对应的字段小于10
qw.lambda().lt(User::getAge, 10);//添加条件
List<User> userList = userDao.selectList(qw);
System.out.println(userList);
}
}

- User::getAget,为lambda表达式中的,类名::方法名,最终的sql语句为:
sql
SELECT id,name,password,age,tel FROM user WHERE (age < ?)
结果:

**注意:**构建LambdaQueryWrapper的时候泛型不能省。
此时我们再次编写条件的时候,就不会存在写错名称的情况,但是qw后面多了一层lambda()调用
- 接着来看第三种:LambdaQueryWrapper
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.lt(User::getAge, 10);
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}

这种方式就解决了上一种方式所存在的问题。其实就是使用LambdaQueryWrapper比第二种方式使用QueryWrapper的方式,写条件的时候少写lambda()这个代码了而已。
结果如下:

3.1.4 多条件构建
学完了三种构建查询对象的方式,每一种都有自己的特点,所以用哪一种都行,刚才都是一个条件,那如果有多个条件该如何构建呢?
需求:查询数据库表中,年龄在10岁到30岁之间的用户信息
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.lt(User::getAge, 30);
//gt表示大于
lqw.gt(User::getAge, 10);
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}

结果:

-
gt:大于(>),最终的SQL语句为
sqlSELECT id,name,password,age,tel FROM user WHERE (age < ? AND age > ?)
-
构建多条件的时候,可以支持链式编程
javaLambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); lqw.lt(User::getAge, 30).gt(User::getAge, 10); List<User> userList = userDao.selectList(lqw); System.out.println(userList);
结果:
需求:查询数据库表中,年龄小于10或年龄大于30的数据
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.lt(User::getAge, 10).or().gt(User::getAge, 30);
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}

结果:

-
or()就相当于我们sql语句中的
or
关键字,不加默认是and
,上面代码最终的sql语句为:sqlSELECT id,name,password,age,tel FROM user WHERE (age < ? OR age > ?)
3.1.5 null判定
先来看一张图,

- 我们在做条件查询的时候,一般会有很多条件可以供用户进行选择查询。
- 这些条件用户可以选择使用也可以选择不使用,比如我要查询价格在8000以上的手机
- 在输入条件的时候,价格有一个区间范围,按照需求只需要在第一个价格输入框中输入8000
- 后台在做价格查询的时候,后端的sql一般会是 price>值1 and price <值2
- 因为前端没有输入值2,所以如果后端我们不处理的话,就会出现 price>8000 and price < null问题
- 这个时候查询的结果就会出问题,具体该如何解决?

需求:查询数据库表中,根据输入年龄范围来查询符合条件的记录
用户在输入值的时候,
如果只输入第一个框,说明要查询大于该年龄的用户
如果只输入第二个框,说明要查询小于该年龄的用户
如果两个框都输入了,说明要查询年龄在两个范围之间的用户
思考第一个问题:后台如果想接收前端的两个数据,该如何接收?
我们可以使用两个简单数据类型去接收,也可以使用一个模型类,但是User类中目前只有一个age属性怎么接收两个年龄呢,如:
java
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
使用一个age属性,如何去接收页面上的两个值呢?这个时候我们有两个解决方案
方案一:添加属性age2,这种做法可以但是会影响到原模型类的属性内容
java
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
private Integer age2;
}
方案二:新建一个模型类,让其继承User类,并在其中添加age2属性。
java
@Data
public class UserQuery extends User {
private Integer age2;
}

环境准备好后,我们来实现下刚才的需求:
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
//模拟页面传递过来的查询数据
UserQuery uq = new UserQuery();
//uq.setAge(10);
uq.setAge2(30);
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
if(null != uq.getAge2()){
lqw.lt(User::getAge, uq.getAge2());
}
if( null != uq.getAge()) {
lqw.gt(User::getAge, uq.getAge());
}
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}
上面的写法可以完成条件为非空的判断,但是问题很明显,如果条件多的话,每个条件都需要判断,代码量就比较大,来看MP给我们提供的简化方式:
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
//模拟页面传递过来的查询数据
UserQuery uq = new UserQuery();
//uq.setAge(10);
uq.setAge2(30);
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
//这个方法会先判断第一个参数是不是true,如果是true就连接这个条件,如果不是true就不连接这个条件
lqw.lt(null!=uq.getAge2(),User::getAge, uq.getAge2());
lqw.gt(null!=uq.getAge(),User::getAge, uq.getAge());
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}

执行结果:

-
lt()方法
condition为boolean类型,返回true,则添加条件,返回false则不添加条件
3.2 查询投影
3.2.1 查询指定字段
目前我们在查询数据的时候,什么都没有做默认就是查询表中所有字段的内容,我们所说的查询投影即不查询所有字段,只查询出指定字段的数据。
具体如何来实现?
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.select(User::getId,User::getName,User::getAge);
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}

看到没有指定的字段查询出来都是默认值了。
-
select(...)方法用来设置查询的字段列,可以设置多个,最终的sql语句为:
sqlSELECT id,name,age FROM user
-
如果使用的不是LambdaQueryWrapper,就需要手动指定字段
java@SpringBootTest class Mybatisplus02DqlApplicationTests { @Autowired private UserDao userDao; @Test void testGetAll(){ QueryWrapper<User> lqw = new QueryWrapper<User>(); lqw.select("id","name","age","tel"); List<User> userList = userDao.selectList(lqw); System.out.println(userList); } }
- 最终的sql语句为:SELECT id,name,age,tel FROM user
3.2.2 聚合查询
需求:聚合函数查询,完成count、max、min、avg、sum的使用
count:总记录数
max:最大值
min:最小值
avg:平均值
sum:求和
如果我们把查询字段写为聚合查询的函数,结果如下:

因为不好用一个实体类来封装结果,所以我们使用selectMaps()方法,这个方法相当于是使用你写的代码对应的sql查询数据库,并把查询到的结果的表中的每一条记录封装到List中的一个Map中去。一条记录就相当于是一个Map,查询结果的第一条记录的每一个字段名,就是list第一个Map元素中的键,这条记录的每一个每一个字段的值都是这个Map中对应的键的值。

但是这样键名不好看,所以,我们可以起一个别名:

java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
QueryWrapper<User> lqw = new QueryWrapper<User>();
//lqw.select("count(*) as count");
//相当于SELECT count(*) as count FROM user
//lqw.select("max(age) as maxAge");
//相当于SELECT max(age) as maxAge FROM user
//lqw.select("min(age) as minAge");
//相当于SELECT min(age) as minAge FROM user
//lqw.select("sum(age) as sumAge");
//相当于SELECT sum(age) as sumAge FROM user
lqw.select("avg(age) as avgAge");
//相当于SELECT avg(age) as avgAge FROM user
List<Map<String, Object>> userList = userDao.selectMaps(lqw);
System.out.println(userList);
}
}
为了在做结果封装的时候能够更简单,我们将上面的聚合函数都起了个别名,方便后期来获取这些数据
3.2.3 分组查询
需求:分组查询,完成 group by的查询使用
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
QueryWrapper<User> lqw = new QueryWrapper<User>();
lqw.select("count(*) as count,tel");
lqw.groupBy("tel");
List<Map<String, Object>> list = userDao.selectMaps(lqw);
System.out.println(list);
}
}
数据库数据如下:

测试方法和结果如下:

-
groupBy为分组,最终的sql语句为
sqlSELECT count(*) as count,tel FROM user GROUP BY tel
注意:
- 聚合与分组查询,无法使用lambda表达式的查询来完成(即,不能用LambdaQueryWrapper来做查询,得用QueryWrapper来做)
- MP只是对MyBatis的增强,对于MP实现不了的操作(比如,MP可以支持count(*)这个分组函数,但是对于其他的一些分组函数,可能不支持,那么我们就得自己用MyBatis的写法来实现),我们可以直接在DAO接口中自己写方法和sql,使用MyBatis的方式来实现我们需要的功能
3.3 查询条件
前面我们只使用了lt()和gt(),除了这两个方法外,MP还封装了很多条件对应的方法,这一节我们重点把MP提供的查询条件方法进行学习下。
sql的查询条件很多,比如:
- 范围匹配(> 、 = 、between)
- 模糊匹配(like)
- 空判定(null)
- 包含性匹配(in)
- 分组(group)
- 排序(order)
- ......
3.3.1 等值查询
需求:根据用户名和密码查询用户信息
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
//设置查询的条件是User的getName这个方法对应的字段值为Jerry并且User的getPassword这个方法对应的字段值为jerry
lqw.eq(User::getName, "Jerry").eq(User::getPassword, "jerry");
User loginUser = userDao.selectOne(lqw);
System.out.println(loginUser);
}
}
-
eq(): 相当于
=
,对应的sql语句为sqlSELECT id,name,password,age,tel FROM user WHERE (name = ? AND password = ?)
-
selectList:查询结果为多个或者单个
-
selectOne:查询结果为单个

如果用selectList(),也行:


3.3.2 范围查询
需求:对年龄进行范围查询,使用lt()、le()、gt()、ge()、between()进行范围查询
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.between(User::getAge, 10, 30);
//SELECT id,name,password,age,tel FROM user WHERE (age BETWEEN ? AND ?)
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}
- gt():大于(>)
- ge():大于等于(>=)
- lt():小于(<)
- lte():小于等于(<=)
- between():between ? and ?

如果范围换一下,效果如下:

3.3.3 模糊查询
需求:查询表中name属性的值以
J
开头的用户信息,使用like进行模糊查询
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.likeLeft(User::getName, "J");
//SELECT id,name,password,age,tel FROM user WHERE (name LIKE ?)
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}
- like():前后加百分号,如 %J%
- likeLeft():前面加百分号,如 %J
- likeRight():后面加百分号,如 J%



3.3.4 排序查询
需求:查询所有数据,然后按照id降序
java
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lwq = new LambdaQueryWrapper<>();
/**
* condition :条件,返回boolean,
当condition为true,进行排序,如果为false,则不排序
* isAsc:是否为升序,true为升序,false为降序
* columns:需要操作的列
*/
lwq.orderBy(true,false, User::getId);
userDao.selectList(lw
}
}
除了上面演示的这种实现方式,还有很多其他的排序方法也可以实现,如图:

- orderBy(boolean condition, boolean isAsc, SFuntion<User,?>... columns)
- condition:条件,true则添加排序,false则不添加排序
- isAsc:是否为升序,true升序,false降序
- columns:排序字段,可以有多个
- orderByAsc/Desc(单个column):按照指定字段进行升序/降序
- orderByAsc/Desc(多个column):按照多个字段进行升序/降序
- orderByAsc/Desc(boolean condition, SFuntion<User,?>... columns)
- condition:条件,true添加排序,false不添加排序
- 多个columns:按照多个字段进行排序
例子1:

例子2:如果要指定一个字段升序,另一个字段降序的排序,我们可以像下面这样。

除了上面介绍的这几种查询条件构建方法以外还会有很多其他的构建条件查询的方法,比如isNull,isNotNull,in,notIn等等方法也可以构建查询条件。想要了解更多的内容,可以查看官方文档的"条件构造器"这一块内容来学习一下,具体的网址为:
https://baomidou.com/pages/10c804/#abstractwrapper
这个网址进入后,可以看到如下:

3.4 映射匹配兼容性
前面我们已经能从表中查询出数据,并将数据封装到模型类中,这整个过程涉及到一张表和一个模型类:

之所以数据能够成功的从表中获取并封装到模型对象中,原因是表的字段列名和模型类的属性名一样。
那么问题就来了:
问题1:表字段与编码属性设计不同步
当数据库表的列名和模型类的属性名不一致,就会导致数据封装不到模型对象的问题,这个时候就需要对数据库表的字段名或者实体类的属性名做出修改,那如果两个都不想改又该如何解决?
MP给我们提供了一个注解@TableField
,使用该注解可以实现模型类属性名和表的列名之间的映射关系,比如下面这样,设置就可以使原来数据库表字段名和实体类属性名不一致的问题解决了,下面这样设计的话,就相当于是让实体类的password这个属性名去对应数据库中表的字段名为pwd的字段:

问题2:编码中添加了数据库中未定义的属性
当模型类中多了一个数据库表不存在的字段,那么就会导致生成的sql语句中在select全部字段的时候查询了数据库不存在的字段,程序运行就会报错,错误信息为:
Unknown column '多出来的字段名称' in 'field list'
具体的解决方案用到的还是@TableField
注解,它有一个属性叫exist
,设置该字段是否在数据库表中存在,如果设置为false则不存在,生成sql语句查询的时候,就不会再查询该字段了。

问题3:采用默认查询开放了更多的字段查看权限
查询表中所有的列的数据,就可能把一些敏感数据查询到返回给前端,这个时候我们就需要限制哪些字段默认不要进行查询。解决方案是@TableField
注解的一个属性叫select
,该属性设置默认是否需要查询该字段的值,true(默认值)表示默认查询该字段,false表示默认不查询该字段。

解决:

知识点1:@TableField
名称 | @TableField |
---|---|
类型 | 属性注解 |
位置 | 模型类属性定义上方 |
作用 | 设置当前属性对应的数据库表中的字段关系 |
相关属性 | value(默认):设置数据库表字段名称 exist:设置属性在数据库表字段中是否存在,默认为true,此属性不能与value合并使用 select:设置属性是否参与查询,此属性与select()映射配置不冲突 |
问题4:表名与编码开发设计不同步
该问题主要是表的名称和模型类的名称不一致,导致查询失败,这个时候通常会报如下错误信息:
Table 'databaseName.tableNaem' doesn't exist,翻译过来就是数据库中的表不存在。
因为一般为了见名知意,所以,一般情况下我们建表的时候都会在表名前面加"tbl_"
或"t_"
,然后再跟表名的,但是我们实体类里面类名一般不加这个前缀。

解决方案是使用MP提供的另外一个注解@TableName
来设置表与模型类之间的对应关系。

知识点2:@TableName
名称 | @TableName |
---|---|
类型 | 类注解 |
位置 | 模型类定义上方 |
作用 | 设置当前类对应于数据库表关系 |
相关属性 | value(默认):设置数据库表名称 |
代码演示
接下来我们使用案例的方式把刚才的知识演示下:
步骤1:修改数据库表user为tbl_user

执行代码:

看到结果报错了,原因是MP默认情况下,是会根据创建LambdaQueryWrapper的时候写的泛型类生成对应的sql的,即,会使用这个泛型的类名首字母小写后得到一个字符串,然后当表名使用去生成sql的。

注意哈,这里我们如果把User改为UsEr,你可以看到默认MP是支持驼峰命名的。

对于属性名对应字段名也一样哈,也是支持驼峰命名的,把字段名的首字母小写,并且后面的大写字母,当作是"_大写字母"这样的规则,生成一个字符串,然后去查询数据库的字段名。

步骤2:模型类添加@TableName注解
java
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
好了,现在我们把上面的数据库中的表不存在的问题解决掉,解决方式如下:

步骤3:将字段password修改成pwd

还是运行之前的测试代码,看到查询会报错,原因是MP使用模型类的属性名去生成sql,但是现在这个属性名和数据库中的字段名不一样,所以运行出错了。


步骤4:使用@TableField映射关系
java
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd")
private String password;
private Integer age;
private String tel;
}
解决:

步骤5:添加一个数据库表不存在的字段
java
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd")
private String password;
private Integer age;
private String tel;
private Integer online;
}
还是运行之前测试的方法,看到结果会报错,原因是查询全部字段的时候,MP模型会去查询这个类的所有属性对应的数据库表的列,但是模型类里面存在online,但是数据库表中不存在这字段名,所以出错了。


注意:要是你查询的时候不是查询所有字段,或者没有查询那个online属性对应字段,那么就不会出错,比如:

但是你要是查询这个online字段的话,还是会出错的哈,即,就算不是查全部字段,你就查部分字段,但是部分字段里面涉及到这个数据库中没有的online字段,那么运行还是会出错的。总之,你只要查的属性对应的字段再在数据库不存在,那么就会出错。

步骤6:使用@TableField排除字段
我们把测试方法还是改为原来的:

解决:
java
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd")
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}

步骤7:查询时将pwd隐藏
java
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}

加了select=false,你查询全部的时候就不查这个字段了。但是你要指定查询这个字段,还是可以查询出来的,如下,只是查询全部的时候默认不能查询出来了。

4,DML编程控制
查询相关的操作我们已经介绍完了,紧接着我们需要对增删改的内容进行讲解。挨个来说明下,首先是新增(insert)中的内容。
4.1 id生成策略控制
前面我们在新增的时候留了一个问题,就是新增成功后,主键ID是一个很长串的内容。在那个案例里面,我们更想要的是让数据库表的记录进行主键自增,那我们得怎么做呢?在解决这个问题之前,我们先来分析下不同场景的ID生成策略我们该如何选择:
- 不同的表应该使用不同的id生成策略,比如
- 日志:自增(1,2,3,4,......)
- 购物订单:特殊规则(FQ23948AK3843)
- 外卖单:关联地区日期等信息生成一个主键(10 04 20200314 34 91)
- 关系表:可省略id
- ......
不同的业务采用的ID生成方式应该是不一样的,那么在MP中都提供了哪些主键生成策略,以及我们该如何进行选择呢?
在这里我们又需要用到MP的一个注解叫@TableId
知识点1:@TableId
名称 | @TableId |
---|---|
类型 | 属性注解 |
位置 | 模型类中用于表示主键的属性定义上方 |
作用 | 设置当前类中主键属性的生成策略 |
相关属性 | value:设置数据库表主键名称 type:设置主键属性的生成策略,值查照IdType的枚举值 |
4.1.1 环境构建
在构建条件查询之前,我们先来准备下环境
-
创建一个SpringBoot项目
-
pom.xml中添加对应的依赖
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>2.5.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.itheima</groupId> <artifactId>mybatisplus_03_dml</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <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>
-
编写UserDao接口
java@Mapper public interface UserDao extends BaseMapper<User> { }
-
编写模型类
java@Data @TableName("tbl_user") public class User { private Long id; private String name; @TableField(value="pwd",select=false) private String password; private Integer age; private String tel; @TableField(exist=false) private Integer online; }
-
编写引导类
java@SpringBootApplication public class Mybatisplus03DqlApplication { public static void main(String[] args) { SpringApplication.run(Mybatisplus03DqlApplication.class, args); } }
-
编写配置文件
application.yml
yml# dataSource 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 # mp日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logback.xml
xml<?xml version="1.0" encoding="UTF-8"?> <configuration> </configuration>
-
编写测试类
java@SpringBootTest class Mybatisplus03DqlApplicationTests { @Autowired private UserDao userDao; @Test void testSave(){ User user = new User(); user.setName("黑马程序员"); user.setPassword("itheima"); user.setAge(12); user.setTel("4006184000"); userDao.insert(user); } @Test void testDelete(){ userDao.deleteById(1401856123925713409L) } @Test void testUpdate(){ User user = new User(); user.setId(1L); user.setName("Tom888"); user.setPassword("tom888"); userDao.updateById(user); } }
-
最终创建的项目结构为:
4.1.2 代码演示
AUTO策略
步骤1:设置生成策略为AUTO
java
@Data
@TableName("tbl_user")
public class User {
//设置主键生成策略为:主键自增,注意,数据库的字段也必须设置为自增
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
步骤2:删除测试数据并修改自增值
-
删除测试数据
-
因为之前生成主键ID的值比较长,会把MySQL的自动增长的值变的很大,所以需要将其调整为目前最新的id值。

要是你不想使用上面的步骤来重置自增的值,你也可以使用下面这个sql语句来重置自增的值为5:
sql
ALTER TABLE tbl_user AUTO_INCREMENT = 5
但是注意:这个sql语句设置的自增值如果大于tbl_user表所有记录中的自增字段最大值,那么自增值就改为了你设置的值。如果这个sql语句设置的自增值小于或者等于tbl_user表所有记录中的自增字段最大值,那么自增值就会被重置为那个最大值+1。
步骤3:运行新增方法
运行刚才的新增的测试方法。

执行完毕,你会发现,新增成功,并且主键id也是从5开始的


经过这三步的演示,会发现AUTO
的作用是使用数据库ID自增,在使用该策略的时候一定要确保对应的数据库表设置了ID主键自增,否则无效。
接下来,我们可以进入源码查看下ID的生成策略有哪些?
打开源码后,你会发现并没有看到中文注释,这就需要我们点击右上角的Download Sources
,会自动帮你把这个类的java文件下载下来,我们就能看到具体的注释内容。因为这个技术是国人制作的,所以他代码中的注释还是比较容易看懂的。

当把源码下载完后,就可以看到如下内容:

从源码中可以看到,除了AUTO这个策略以外,还有如下几种生成策略:
- NONE: 不设置id生成策略
- INPUT:用户手工输入id
- ASSIGN_ID:雪花算法生成id(可兼容数值型与字符串型)
- ASSIGN_UUID:以UUID生成算法作为id生成策略(和那个UUID的工具类生成的通用唯一识别码的算法一样)
- 其他的几个策略均已过时,都将被ASSIGN_ID和ASSIGN_UUID代替掉,所以不用去了解他们了。
拓展:
分布式ID是什么?
- 当数据量足够大的时候,一台数据库服务器存储不下,这个时候就需要多台数据库服务器进行存储
- 比如订单表就有可能被存储在不同的服务器上
- 如果用数据库表的自增主键,因为在两台服务器上所以会出现冲突
- 这个时候就需要一个全局唯一ID,这个ID就是分布式ID。
INPUT策略
步骤1:设置生成策略为INPUT
java
@Data
@TableName("tbl_user")
public class User {
@TableId(type = IdType.INPUT)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}

**注意:**这种ID生成策略,需要将表的自增策略删除掉。但是这个字段还是主键哈,只是取消了自增。

步骤2:添加数据手动设置ID
如果你不手动指定id,那么运行将会出错,错误提示就是主键ID没有给值,因为是主键,所以必须要有一个值,除非你是自动递增的,这里我们取消了自动递增,所以必须要给值了:

运行结果如下:

注意:上面这个报错是,要求把数据库里面主键的自增给取消了,不然,你要是插入一个id为null的数据是可以成功的,不会报错的。相当于是你插入了null,但是数据库里实际插入的会是"6,黑马程序员,itheima,12,4006184000"这个记录。即,如果你使用了INPUT策略,但是数据库里面的主键没有取消自增,那么你插入一个这个主键字段的值为null,那么实际将会使用数据库的自增值当主键值插入。
上面这个报错的解决:
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testSave(){
User user = new User();
//设置主键ID的值
user.setId(666L);
user.setName("黑马程序员");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
}

步骤3:运行新增方法
运行后如下:

数据添加成功,如下:

注意:上面这个运行的环境是把数据库的id的自增取消了的情况下。但是其实,你数据库的id字段,不取消自增,后端代码设置了User的id是INPUT策略的,然后你插入的时候,在实体类里面给定id值,再进行插入,那么也会使用你给的id值插入的。
总结,有下面几种情况:
- 后端代码设置了User的id是INPUT策略的,你设置了数据库的id自增,插入时设置了User的id值,插入的记录的id值用你插入的值。
- 后端代码设置了User的id是INPUT策略的,你设置了数据库的id自增,插入时不设置User的id值,插入的记录的id值用数据库的自增值。
- 后端代码设置了User的id是INPUT策略的,你设置了数据库的id不开启自增,插入时设置了User的id值,插入的记录的id值用你插入的值。
- 后端代码设置了User的id是INPUT策略的,你设置了数据库的id不开启自增,插入时不设置User的id值,插入会报异常。
ASSIGN_ID策略
步骤1:设置生成策略为ASSIGN_ID
java
@Data
@TableName("tbl_user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
步骤2:添加数据并运行新增方法
如果你添加数据的时候设置了ID:

数据库中结果如下:

如果你不设置ID:
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testSave(){
User user = new User();
user.setName("黑马程序员");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
}

生成的ID是一个Long类型的数据。注意,这里的案例里面是取消数据库的id自增功能的哈。
数据库结果如下:

**注意:**这种生成策略,不需要手动设置ID,如果手动设置ID,则会使用自己设置的值。
注意:上面的例子里面都是在取消数据库的自增的情况下的。
经过,我私下测试,我发现如果我们这个例子里面数据库的id字段是设置了数据库自增的。那么,如果你插入的时候给了一个有id值的User对象进行插入,那么将会成功插入一条记录,且插入的记录的id值是你User对象里面指定的id。如果你设置了数据库的id是自增的,且插入的时候给了一个没有设置id的User对象(这里说的没有设置id,是指id值为null),那么你将会成功插入一条记录,且插入的记录的id值是雪花算法算出来的值。
总结:你使用ASSIGN_ID策略,不管你有没有设置数据库的自增字段,结果都一样。如果你给User对象一个非null的id值,那么将插入你给的id值,如果你User对象id的值是null,那么这个插入将用雪花算法生成id进行插入。
ASSIGN_UUID策略
步骤1:设置生成策略为ASSIGN_UUID
使用uuid需要注意的是,主键的类型不能是Long,而应该改成String类型
java
@Data
@TableName("tbl_user")
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
步骤2:修改表的主键类型

主键类型设置为varchar,长度要大于32,因为UUID生成的主键为32位,如果长度小的话就会导致插入失败。
步骤3:添加数据不设置ID
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testSave(){
User user = new User();
user.setName("黑马程序员");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
}
步骤4:运行新增方法

上面这个界面就表示插入成功了。关于ASSIGN_UUID的策略,我就不演示了。
接下来我们来聊一聊雪花算法:
雪花算法(SnowFlake),是Twitter官方给出的算法实现 是用Scala写的。其生成的结果是一个64bit大小整数,它的结构如下图:

- 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位默认为0。
- 41bit-时间戳,用来记录时间戳,毫秒级。这个和我们当前电脑系统的当前时间也不是完全一样的,只是有关,你不用管他,反正知道它是一个时间就行了。
- 10bit-工作机器id,用来记录工作机器id,其中高位5bit是数据中心ID,其取值范围0-31,低位5bit是工作节点ID其取值范围0-31,两个组合起来最多可以容纳1024个节点。
- 序列号占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生4096个ID。比如,你一个计算机在一毫秒内接了5个请求,然后如果你当前要生成的id的请求是第四个请求,那么就生成一个4,作为生成的id的序列号位置的值。这个序列号可以放下4096个值,你一个计算机不太可能在一毫秒内接受4096个请求,所以这个序列号一般够用了。
4.1.3 ID生成策略对比
介绍了这些主键ID的生成策略,我们以后该用哪个呢?
- NONE: 不设置id生成策略,MP不自动生成,约等于INPUT,所以这两种方式都需要用户手动设置,但是手动设置第一个问题是容易出现相同的ID造成主键冲突,为了保证主键不冲突就需要做很多判定,实现起来比较复杂
- AUTO:数据库ID自增,这种策略适合在数据库服务器只有1台的情况下使用,不可作为分布式ID使用
- ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证唯一,但是生成的主键是32位的字符串,长度过长占用空间而且还不能排序,查询性能也慢
- ASSIGN_ID:可以在分布式的情况下使用,生成的是Long类型的数字,可以排序性能也高,但是生成的策略和服务器时间有关,如果修改了系统时间就有可能导致出现重复主键
- 综上所述,每一种主键策略都有自己的优缺点,根据自己项目业务的实际情况来选择使用才是最明智的选择。
4.1.4 简化配置
前面我们已经完成了表关系映射、数据库主键策略的设置,接下来对于这两个内容的使用,我们再讲下他们的简化配置:
模型类主键策略设置
对于主键ID的策略已经介绍完,但是如果要在项目中的每一个模型类上都需要使用相同的生成策略,如:
这样,在每一个实体类上面都指定id生成策略,确实是稍微有点繁琐,我们能不能在某一处进行配置,就能让所有的模型类都可以使用该主键ID策略呢?
答案是肯定有,我们只需要在配置文件中添加如下内容:
yml
mybatis-plus:
global-config:
db-config:
id-type: assign_id

配置完成后,每个模型类的主键ID策略都将成为assign_id.
我们把原来的实体类指定的主键生成策略取消了,看看结果:



看到,设置成功了。
注意:如果你设置了全局的主键生成策略,又在User实体类里id属性上设置与全局配置的主键生成策略不一样的主键生成策略,那么我们插入这个User的时候,将会使用User上面定义的主键生成策略来插入主键值,插入其他的实体类的话,你没有在那个实体类上面特别设置使用某个主键生成策略,那么就会使用全局的主键生成策略。相当于你在某个具体的实体类中设置了主键生成策略,那么插入这个实体类数据的时候,将会使用这个实体类上面设置的主键生成策略,而不是你全局设置的主键生成策略。
数据库表与模型类的映射关系
MP会默认将模型类的类名名首字母小写,后续的大写字母会被替换为"_小写字符",然后作为表名使用,假如数据库表的名称都以tbl_
开头,那么我们就需要将所有的模型类上添加@TableName
了,如:

配置起来还是比较繁琐,我们这时可以在配置文件中配置全局的数据库表的映射前缀,如下:
yml
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_

设置表的前缀内容,这样MP就会拿 tbl_
加上模型类的首字母小写,并且后续的大写字母会被替换为"_小写字符",生成表名。
比如上面这个实体类的类名是UsEr,那么你设置了上面这个全局前缀的话,到时候去执行这个UsEr相关的操作,比如说是使用这个UsEr实体类去执行插入数据,MP就相当于是认为去找数据库里的tbl_us_er表,进行插入。

现在我们把,实体类的类名改回来,继续学习。我们把模型类上面的@TableName("tbl_user")注释了,运行看看效果:


结果:


注意:如果你设置了全局的表前缀,又再某个实体类上面自己写表名的映射,那么生成的sql将会用你实体类上面写的表名。
比如:

4.2 多记录操作
先来看下问题:

之前添加了很多商品到购物车,过了几天发现这些东西又不想要了,该怎么办呢?
很简单删除掉,但是一个个删除的话还是比较慢和费事的,所以一般会给用户一个批量操作,也就是前面有一个复选框,用户一次可以勾选多个也可以进行全选,然后删一次就可以将购物车清空,这个就需要用到批量删除
的操作了。
具体该如何实现多条删除,我们找找MP中对应的API方法
java
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
翻译方法的字面意思为:删除(根据ID 批量删除),参数是一个集合,可以存放多个id值。
需求:根据传入的id集合将数据库表中的数据删除掉。
我们修改测试类中的删除方法。如下:
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testDelete(){
//删除指定多条数据
List<Long> list = new ArrayList<>();
list.add(1402551342481838081L);
list.add(1402553134049501186L);
list.add(1402553619611430913L);
userDao.deleteBatchIds(list);
}
}

实体类如下:

运行前数据库如下:

运行后数据库,如下:

执行成功后,数据库表中的数据就会按照指定的id进行删除了。
除了上面这样按照id集合进行批量删除,MP也有对应的API可以实现按照id集合进行批量查询,还是先来看下API
java
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
方法名称翻译为:查询(根据ID 批量查询),参数是一个集合,可以存放多个id值。
需求:根据传入的ID集合查询用户信息
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetByIds(){
//查询指定多条数据
List<Long> list = new ArrayList<>();
list.add(1L);
list.add(3L);
list.add(4L);
userDao.selectBatchIds(list);
}
}

查询结果就会按照指定传入的id值进行查询

4.3 逻辑删除
接下来要讲解是删除中比较重要的一个操作,逻辑删除,先来分析下问题:

-
这是一个员工和其所签的合同表,关系是一个员工可以签多个合同,是一(员工)对多(合同)的关系
-
员工ID为1的张业绩,总共签了三个合同,如果此时他离职了,我们需要将员工表中的数据进行删除,会执行delete操作
-
如果表在设计的时候有主外键关系,那么同时也得将合同表中的前三条数据也删除掉才行。
-
但是这样,后期要统计所签合同的总金额的时候,就会发现对不上,原因是已经将员工1签的合同信息删除掉了
-
如果两个表没有主外键关系,那么可以做到删除员工表中的被关联的数据。但是只删除员工不删除合同表数据,那么合同的员工编号对应的员工信息就可能不存在,那么就会出现垃圾数据,即,就会出现无主合同,拿这个合同去找张业绩这个人,但是根本不知道有张业绩这个人的存在。
-
所以经过分析,我们不应该将员工表中的数据删除掉,而是需要进行保留,同时又得把离职的人和在职的人进行区分,这样就可以解决上述问题,如:
-
区分的方式,就是在员工表中添加一列数据
deleted
,如果为0说明在职员工,如果离职则将其改完1,(0和1所代表的含义是可以自定义的)
所以对于删除操作业务问题来说有:
- 物理删除:业务数据从数据库中丢弃,执行的是delete操作
- 逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,执行的是update操作
MP中逻辑删除具体该如何实现呢?
步骤1:修改数据库表添加deleted
列
我们在原来的表上面添加一个deleted字段,字段名可以任意,内容意义也可以自定义,比如0
代表正常,1
代表删除,也可以在添加列的同时设置其默认值为0
。


步骤2:实体类添加属性
(1)添加与数据库表的列对应的一个属性名,名称可以任意,如果属性名和数据表列名对不上,可以使用@TableField进行关系映射,如果一致,可以不用使用@TableField,因为它会自动对应。
(2)标识新增的字段为逻辑删除字段,使用@TableLogic
java
@Data
//@TableName("tbl_user") 可以不写是因为配置了全局配置
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
@TableLogic(value="0",delval="1")
//value设置一个值表示没有删除,delval设置一个值表示这条记录已经删除了。即,上面这样设置,就是指,这个属性对应的字段的值如果是0表示这条记录没有删,1表示这条记录已经删了。
private Integer deleted;
}

步骤3:运行删除方法
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testDelete(){
userDao.deleteById(1L);
}
}

结果:


注意:我们执行的sql里,修改数据的时候,还会判断一下那个字段当前值是不是那个表示未删除的值的。
从测试结果来看,逻辑删除最后走的是update操作,且会将指定的字段修改成删除状态对应的那个值。
思考
逻辑删除,对查询有没有影响呢?
-
执行查询操作
java@SpringBootTest class Mybatisplus03DqlApplicationTests { @Autowired private UserDao userDao; @Test void testFind(){ System.out.println(userDao.selectList(null)); } }
运行测试,会发现打印出来的sql语句中会多一个查询条件,如:
可想而知,MP的逻辑删除会将所有的查询都添加一个未被删除的条件,也就是已经被删除的数据是不应该被查询出来的。
-
如果还是想把已经删除的数据都查询出来该如何实现呢?
自己用mybatis去查询:
java@Mapper public interface UserDao extends BaseMapper<User> { //查询所有数据包含已经被删除的数据 @Select("select * from tbl_user") public List<User> selectAll(); }
-
如果每个表都要有逻辑删除,那么就需要在每个模型类的属性上添加
@TableLogic
注解,如何优化?在配置文件中添加全局配置,如下:
ymlmybatis-plus: global-config: db-config: # 逻辑删除字段名 logic-delete-field: deleted # 逻辑删除字面值:未删除为0 logic-not-delete-value: 0 # 逻辑删除字面值:删除为1 logic-delete-value: 1
测试:
介绍完逻辑删除,可以知道逻辑删除的本质为:
逻辑删除的本质其实是修改操作。如果加了逻辑删除字段,查询数据时也会自动带上逻辑删除字段。
执行的SQL语句为:
UPDATE tbl_user SET deleted=1 where id = ? AND deleted=0
知识点1:@TableLogic
名称 | @TableLogic |
---|---|
类型 | 属性注解 |
位置 | 模型类中用于表示删除字段的属性定义上方 |
作用 | 标识该字段为进行逻辑删除的字段 |
相关属性 | value:逻辑未删除值 delval:逻辑删除值 |
4.4 乐观锁
4.4.1 概念
关于乐观锁和悲观锁的概念我们可以看下面这个图片:

这里我们要学的是MP提供的乐观锁功能。在讲解MP的乐观锁之前,我们还是先来分析下问题:
业务并发现象带来的问题:秒杀
- 假如有100个商品或者票在出售,为了能保证每个商品或者票只能被一个人购买,如何保证不会出现超买或者重复卖。即,比如你每一个商品都有一个id,你要做到保证这个id的商品只能被一个买家绑定(即,被一个买家购买),不能出现1个商品被多个人同时购买,然后这几个人同时拥有这个id的商品的情况。
- 对于这一类问题,其实有很多的解决方案可以使用
- 对于这个问题的解决方案中,我们比较容易想到的是用java多线程的锁来实现,我们让一个方法上锁,然后一个线程执行这个方法,另一个线程就无法执行这个方法了,就可以解决这个问题了,但是这种方式有一个问题,就是如果这个程序是分布式部署的即,多台服务器上部署了一样的程序,你一个请求可能在A服务器上这个程序的这个方法执行,一个请求可能在B服务器同样的方法上执行。如果这样的话,你用户1的请求在A服务器上运行,用户2的请求在B服务器上运行,即使你出来用户1的请求的时候,给这个方法上锁了,但是那也是在A服务器上锁的,B服务器还是可以通过B服务器中同样的这个方法去修改数据库数据的,AB服务器代码一样的嘛(多线程的锁是依靠进程内存的,不同服务器肯定不在一个电脑,那肯定不在一个进程里面呀,所以当然无效了),所以这样就不能解决现在这个并发问题了。但是你可以使用分布式锁来做哈。这里我们不讲分布式锁。这里我们讲乐观锁的其中一种方式来解决这个问题(因为乐观锁是一个大概念,乐观锁的方式有很多,只要你符合乐观锁的定义都叫乐观锁,所以这里我们讲的只是其中一种乐观锁的方式而已,并不是乐观锁解决方式都能解决这个问题哈,只是这里我们讲的乐观锁的这种方式可以解决这个问题。因为下面讲的这个方式不是基于内存的,而是借助数据库查询出来的字段一个字段来达到逻辑上的锁的效果,所以就算是程序被分布式部署也没有关系的)。
- 我们接下来介绍的这种方式是针对于小型企业的解决方案。因为我们这个方式解决,多个请求执行的时候,不会限制与数据库交互的,所以我们同时去访问数据库的访问量可能也不少。比如,同时有5000个用户执行购买操作,这些请求是都会去看查数据库,所以数据库的访问量还是很多,因为数据库本身的性能就是个瓶颈,如果对其并发量超过2000以上,就会出现问题,就需要考虑其他的解决方案了。
在关系数据库管理系统中,乐观并发控制是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的部分数据。在提交数据之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,那么会放弃修改。
简单来说,乐观锁主要解决的问题是当要更新一条记录的时候,希望这条记录没有被别人更新。
这里扩展一下:你一个部署在服务器上的程序,没有用任何开启多线程的语句,没有new Thread().start()开启线程类似的语句,那么你两个人同时访问服务器上的这个程序呢?服务器会自动给你开两个线程来处理这两个人的请求吗?答:会的。因为tomcat内部就有代码实现了多线程,即,你多个请求同时访问访问这个服务器,服务器会自动给你开多个线程来执行,一个请求一个线程。就算你没有手动开启线程,也没有事,tomcat内部的机制有开线程的。tomcat最大支持线程数,大概是500,当然这是理论值,实际可能只能支持同时开启300-400个线程。如果哈,这里只是假设,tomcat里他没有给你搞多线程,那么用户1访问的这个服务器的时候,用户2访问这个服务器就会出现问题,即服务器只能同时处理一个请求。
4.4.2 实现思路
乐观锁的实现方式:
数据库表中添加version列,比如默认值给1
第一个线程要修改数据之前,取出记录时,获取当前数据库中的version
第二个线程要修改数据之前,取出记录时,获取当前数据库中的version
第一个线程执行更新时,执行的sql是set version = newVersion where version = oldVersion
其中newVersion = oldVersion+1,oldVersion就是我们第一个线程修改前查询出来的version值
第二个线程执行更新时,执行的sql是set version = newVersion where version = oldVersion
其中newVersion = oldVersion+1,oldVersion就是我们第二个线程修改前查询出来的version值
假如这两个线程都来更新数据,第一个和第二个线程都可能先执行
- 假设一开始两个线程执行更新前读取的version值都是1
- 假如第一个线程先执行更新,会把version改为2,
- 第二个线程再更新的时候,set version = 2 where version = 1,此时数据库表的数据version已经为2,所以第二个线程会修改失败
- 假如第二个线程先执行更新,会把version改为2,
- 第一个线程再更新的时候,set version = 2 where version = 1,此时数据库表的数据version已经为2,所以第一个线程会修改失败
- 不管谁先执行都会确保只能有一个线程更新数据,这就是MP提供的乐观锁的实现原理分析。
上面所说的步骤具体该如何实现呢?
4.4.3 实现步骤
分析完步骤后,具体的实现步骤如下:
步骤1:数据库表添加列
列名可以任意,比如叫version
,并且给列设置默认值为1
。


步骤2:在模型类中添加对应的属性
根据添加的字段列名,在模型类中添加对应的属性,并加上@Version注解。
java
@Data
//@TableName("tbl_user") 可以不写是因为配置了全局配置
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
private Integer deleted;
//注意:一个类里面只能写一个@Version。且写@Version的属性不能是String类型的,不然乐观锁无效。
@Version
private Integer version;
}

步骤3:添加乐观锁的拦截器
java
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}

步骤4:执行更新操作
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
User user = new User();
user.setId(3L);
user.setName("Jock666");
userDao.updateById(user);
}
}

你会发现,这次修改并没有更新version字段,原因是没有携带version数据,即,User对象中的version属性为null,null的属性值不会被MP生成到sql中。
添加user对象的version属性值后:
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
User user = new User();
user.setId(3L);
user.setName("Jock666");
user.setVersion(1);
userDao.updateById(user);
}
}

你会发现,我们传递的是1,修改的时候MP会将1进行加1,然后,更新回到数据库表中。我们可以看到,在修改的sql中他加了一个条件,就是判断当你设置乐观锁的字段,是不是你原来认为的值,如果不是将会这个sql将会无法执行,如果是你原来认为的值,就可以修改成功,并且这个字段值+1.
所以要想实现乐观锁,首先第一步应该是拿到表中的version,然后拿version当条件在将version加1更新回到数据库表中,所以我们在修改之前应该,需要对这个version值进行查询。
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
//1.先通过要修改的数据id将当前数据查询出来,再进行修改。或者,你前端发送的数据里面,把上一次查询并在前端展示的version值传过来,然后设置到User里面,然后进行修改,这样你这里就不用自己调用查询去获取到version值了。反正,不管是你查询后再修改,还是从前端获取之前的version值后,再在后端把这个值设置到User里面,再进行修改都是一样的,就是要获取这个记录的version后再进行修改操作。
User user = userDao.selectById(3L);
//2.将要修改的属性逐一设置进去
user.setName("Jock888");
userDao.updateById(user);
}
}


大概分析完乐观锁的实现步骤以后,我们来模拟一种加锁的情况,看看能不能实现多个人修改同一个数据的时候,只能有一个人修改成功。我们下面来模拟一下:
java
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
//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?条件还成立吗?不会成立,所以这里这个语句执行的时候,将不会进行修改。
}
}

运行程序,分析结果:

乐观锁就已经实现完成了,如果对于上面的这些步骤记不住咋办呢?
参考官方文档来实现:
https://baomidou.com/pages/0d93c0/#optimisticlockerinnerinterceptor

5,快速开发
5.1 代码生成器原理分析
造句:
我们可以往空白内容进行填词造句,比如:
在比如:
观察我们之前写的代码,会发现其中也会有很多重复内容,比如:

那我们就想,如果我想做一个Book模块的开发,是不是只需要将红色部分的内容全部更换成Book
即可,如:

所以我们会发现,做任何模块的开发,对于这段代码,基本上都是对红色部分的调整,所以我们把去掉红色内容的东西称之为模板,红色部分称之为参数,以后只需要传入不同的参数,就可以根据模板创建出不同模块的dao代码。
除了Dao可以抽取模块,其实我们常见的类都可以进行抽取,只要他们有公共部分即可。再来看下模型类的模板:

- ① 可以根据数据库表的表名来填充
- ② 可以根据用户的配置来生成ID生成策略
- ③到⑨可以根据数据库表字段名称来填充
- 其实还有上面的属性的类型也可以通过数据库表的字段的类型来进行字段填充的。
所以只要我们知道是对哪张表进行代码生成,这些内容我们都可以进行填充。
分析完后,我们会发现,要想完成代码自动生成,我们需要有下面三个东西:
- 模板: MyBatisPlus提供,可以自己写,但是自己写比较麻烦。
- 数据库相关配置:读取数据库获取表和字段信息
- 开发者自定义配置:手工配置,比如ID生成策略
5.2 代码生成器实现
步骤1:创建一个Maven项目
代码2:导入对应的jar包
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>2.5.1</version>
</parent>
<groupId>com.itheima</groupId>
<artifactId>mybatisplus_04_generator</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--spring webmvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatisplus-->
<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>
<!--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>
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!--velocity模板引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

步骤3:编写引导类
java
@SpringBootApplication
public class Mybatisplus04GeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(Mybatisplus04GeneratorApplication.class, args);
}
}
步骤4:创建代码生成类
java
public class CodeGenerator {
public static void main(String[] args) {
//1.获取代码生成器的对象
AutoGenerator autoGenerator = new AutoGenerator();
//设置数据库相关配置(改为你自己的要自动生成代码的数据库)
DataSourceConfig dataSource = new DataSourceConfig();
dataSource.setDriverName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("root");
autoGenerator.setDataSource(dataSource);
//设置全局配置
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setOutputDir(System.getProperty("user.dir")+"/mybatisplus_04_generator/src/main/java"); //设置代码生成位置。这里System.getProperty("user.dir")表示用户当前工作目录(即,获取当前运行代码所在项目的根路径)
globalConfig.setOpen(false); //设置生成完毕后是否打开生成代码所在的目录
globalConfig.setAuthor("黑马程序员"); //设置作者。即,类上面的注释的@author后面的值。
globalConfig.setFileOverride(true); //设置如果生成的文件名和原来的文件名一样,是否覆盖
globalConfig.setMapperName("%sDao"); //设置数据层接口名叫什么什么dao,%s为占位符,指代数据库的表名。
globalConfig.setIdType(IdType.ASSIGN_ID); //设置Id生成策略。这样你生成的实体类上面的主键就自动加上设置id生成策略为IdType.ASSIGN_ID的注解。
autoGenerator.setGlobalConfig(globalConfig);
//设置包名相关配置
PackageConfig packageInfo = new PackageConfig();
packageInfo.setParent("com.aaa"); //设置生成的包名,如果你设置的包名现在已经存在了,那么就会把这些生成的东西放在那个包下,不会删除原来的包的。但是要是自动生成文件的名字和之前存在的文件名字一样且在同一个位置的话,自动生成的文件还是会覆盖的哈,只是包不会覆盖而已,文件还是会覆盖的,因为上面设置了globalConfig.setFileOverride(true);。
packageInfo.setEntity("domain"); //设置生成的实体类所在包名
packageInfo.setMapper("dao"); //设置生成的数据层所在的包名
autoGenerator.setPackageInfo(packageInfo);
//策略设置
StrategyConfig strategyConfig = new StrategyConfig();
strategyConfig.setInclude("tbl_user"); //设置当前所有参与代码生成的表有哪些(写的是表名),参数为可变参数,所以你要生成多个表的话,可以strategyConfig.setInclude("tbl_user,"tbl_xxx");这样写,即,把你要生成的表的名字都写进去就行了
strategyConfig.setTablePrefix("tbl_"); //设置去掉表名的tbl_前缀再进行自动代码生成,比如表名叫tbl_user,那么这样设置后,生成的实体类就叫User,业务逻辑层接口就叫IUserService,数据层就叫......,即去掉表名的前缀再生成。
strategyConfig.setRestControllerStyle(true); //设置控制层生成的时候,是否按Rest风格生成(设置了之后,生成的控制层的类就自带@RestController注解的)
strategyConfig.setVersionFieldName("version"); //设置乐观锁字段名,这样数据库里面表中你的字段名叫version,就会生成的代码就自动设置乐观锁的注解了。
strategyConfig.setLogicDeleteFieldName("deleted"); //设置逻辑删除字段名,这样数据库里面表中你的字段名叫deleted,就会生成的代码就自动设置逻辑删除的注解了。
strategyConfig.setEntityLombokModel(true); //设置生成的实体类是否启用lombok的注解。开启后,生成的实体类上面有一个@EqualsAndHashCode(callSuper = false),表示不继承父类中的EqualsAndHashCode的方法。这样的话,会出现同一个父类下不同实例,虽然存入不同父类属性,但是子类属性相同时,但是判断的时候还是会认为这两个的实例哈希值是相等的。想了解更多可以看https://blog.csdn.net/jsxjlps/article/details/119744337。
autoGenerator.setStrategy(strategyConfig);
//2.执行生成操作
autoGenerator.execute();
}
}
对于代码生成器中的代码内容,我们可以直接从官方文档中获取代码,然后进行对应的修改。
https://baomidou.com/pages/d357af/#%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B
步骤5:运行程序
运行成功后,会在当前项目中生成很多代码,代码包含controller
,service
,mapper
和entity

至此代码生成器就已经完成工作,我们能快速根据数据库表来创建对应的类,简化我们的代码开发。
自动生成的实体类上面的@EqualsAndHashCode(callSuper = false)不懂,可以看下面这里:
Lombok 的 @EqualsAndHashCode(callSuper = false) 的使用
摘录于:https://blog.csdn.net/jsxjlps/article/details/119744337
Lombok 的 @EqualsAndHashCode(callSuper = false)翻译成中文的意思就是:
不继承父类中自动生成的EqualsAndHashCode的方法
因为父类中有父类自己的属性,所以如果选择false,则会出现同一个父类下不同实例存入不同父类属性,但是子类属性相同时,被判断这两个的实例哈希相等的逻辑错误现象。
实例代码:
javaimport lombok.Data; @Data public class BillFather { private long id; public BillFather(long id) { this.id = id; } }
javaimport lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = false) public class Bill extends BillFather{ private String name; public Bill(long id, String name) { super(id); this.name = name; } }
javapublic class EqualsAndHashCodeTest { public static void main(String[] args) { Bill b1 = new Bill(1, "王五"); Bill b2 = new Bill(2, "王五"); System.out.println(b1.equals(b2)); Bill b3 = new Bill(2, "张三"); System.out.println(b1.equals(b3)); } }
最后结果是一个true,一个false。
只写@Date的结果是和 @Date+@EqualsAndHashCode(callSuper = false)一致,所以应该是默认不继承父类中的EqualsAndHashCode的判断方法,而是只继承属性的。
为了保证从父类那继承来的属性被进行equals判断时,不会只判断父类是否相同,而不判断父类属性是否相同的问题,感觉还是在继承父类的时候加上@EqualsAndHashCode(callSuper = true)免得报bug
版权声明:本文为CSDN博主「jsxjlps」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jsxjlps/article/details/119744337
5.3 MP中Service的CRUD
上面自动生成的代码里面,你可以看到生成的UserServiceImpl继承自ServiceImpl<UserDao, User> 并且实现了 IUserService接口。我们下面来简单地讲讲这个接口,为什么简单地讲讲呢?因为我们实际开发的时候,Service层一般会写一些我们自己的逻辑,逻辑一般不写在Controller层的,但是MP给我们提供的IService其实只提供了一些基本功能的代码,比如根据id查询呀什么的,即它给我们提供的方法是比较通用的方法,要是我们要在Service层写自己的处理逻辑的这个通用的方法就不好用了,但是一些简单的逻辑,我们可以使用它给我们提供的这个IService接口中的方法了。
学习之前,我们先来回顾我们之前业务层代码的编写。
编写接口和对应的实现类:
java
public interface UserService{
}
java
@Service
public class UserServiceImpl implements UserService{
}
接口和实现类有了以后,需要在接口和实现类中声明方法
java
public interface UserService{
public List<User> findAll();
}
java
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao;
public List<User> findAll(){
return userDao.selectList(null);
}
}
MP看到上面的代码以后就说这些方法也是比较固定和通用的,那我来帮你抽取下,所以MP提供了一个Service接口和实现类,分别是:IService
和ServiceImpl
,后者是对前者的一个具体实现。
以后我们自己写的Service就可以进行如下修改:
java
public interface UserService extends IService<User>{
}
java
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService{
}
修改以后的好处是,MP已经帮我们把业务层的一些基础的增删改查都已经实现了,我们的接口继承它就可以直接拥有它的方法了,并且有自己特别的需求,也可以自己在UserService和UserServiceImpl里面写我们自己的代码。
编写测试类进行测试:
java
@SpringBootTest
class Mybatisplus04GeneratorApplicationTests {
private IUserService userService;
@Test
void testFindAll() {
List<User> list = userService.list();
System.out.println(list);
}
}
如果想知道在MP封装的IService都有哪些方法可以用?
我们可以查看官方文档:https://baomidou.com/pages/49cc81/
进行学习使用