SpringBoot收尾+myBatis plus
一、数据传递
返回值为:字符串
package com.apesource.springboot_web_04.controller;import com.apesource.springboot_web_04.pojo.Emp;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;/*** 返回值为:字符串*/
@Controller
@RequestMapping("/string")
public class StringController_01 {/*** 进入首页**/@RequestMapping("/show")public String show(){return "index";}
}
package com.apesource.springboot_web_04.controller;import com.apesource.springboot_web_04.pojo.Emp;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;import java.util.ArrayList;
import java.util.List;
import java.util.Map;@Controller
@RequestMapping("/json")
public class JsonController_02 {/*** @ResponseBody 对象===>json* 位置:1.类* 2.方法** @RequestBody json===>对象* 位置:方法参数** @RestController = @Controller + @ResponseBody*/@RequestMapping("/show1")@ResponseBodypublic List<Emp> show1(){//1.模拟数据库Emp emp1=new Emp(1,"张毅老师","男");Emp emp2=new Emp(2,"张毅老师","男");Emp emp3=new Emp(3,"张毅老师","男");List<Emp> list=new ArrayList<>();list.add(emp1);list.add(emp2);list.add(emp3);return list;}@RequestMapping("/show2")@ResponseBodypublic String show2(){return "helloWorld";}
}
二、文件上传
1.添加坐标
<!--文件上传--><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.6</version></dependency><dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.3</version></dependency>
2.UserController
package com.apesource.springboot_web_04.controller;import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import java.io.IOException;@Controller
@RequestMapping("/for")
public class UserController {//进入测试页面@RequestMapping("/show")public String show(){return "index";}//文件上传@RequestMapping("/fileupload")public String fileupload(String uname, MultipartFile upic, HttpServletRequest request){System.out.println("用户名:"+uname);System.out.println(upic);System.out.println(upic.getOriginalFilename());System.out.println(upic.getName());//方式1.将文件upic以流的方式写入当前服务器磁盘(应用服务器)//方式2.文件服务器(七牛云)//构造一个带指定 Region 对象的配置类Configuration cfg = new Configuration(Region.autoRegion());//...其他参数参考类注释UploadManager uploadManager = new UploadManager(cfg);//...生成上传凭证,然后准备上传String accessKey = "Tfv6J4DIRakNREBeGc8eNL21xEAP2uXsyvUa8esk";String secretKey = "Dr5lD7XypDenJ6i14DtqxKK87dkA9ux47xn53tUB";String bucket = "2025ks";//默认不指定key的情况下,以文件内容的hash值作为文件名String key = null;try {byte[] uploadBytes = upic.getBytes();Auth auth = Auth.create(accessKey, secretKey);String upToken = auth.uploadToken(bucket);try {Response response = uploadManager.put(uploadBytes, key, upToken);//解析上传成功的结果DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);System.out.println(putRet.key);//获取文件名System.out.println(putRet.hash);//获取文件hash值} catch (QiniuException ex) {Response r = ex.response;System.err.println(r.toString());try {System.err.println(r.bodyString());} catch (QiniuException ex2) {//ignore}}} catch (Exception ex) {//ignore}return "success";}}
3.index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>文件上传</title>
</head>
<body>文件上传:<ol><li>坐标</li><li>制作页面-form表单编码</li><li>通过*****接受文件</li></ol><hr/><form action="fileupload" method="post" enctype="multipart/form-data">用户名:<input name="uname"/><br/>图片:<input name="upic" type="file"/><br/><input type="submit" value="上传"/></form></body>
</html>
4.success.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>文件上传</title>
</head>
<body>成功页面
</body>
</html>
三、注册Servlet、Filter、Listener
而由于 Spring Boot 默认是以 jar 包的方式运行嵌入式Servlet容器来启动应用,没有web.xml文件,
Spring提供以下Bean来注册三大组件
ServletRegistrationBean 注册自定义Servlet
FilterRegistrationBean 注册自定义Filter
ServletListenerRegistrationBean 注册自定义Listener
第一种:
package com.apesource.springboot_web_04.servlet;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@WebServlet(urlPatterns = "/myServlet")
public class MyServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("进入servlet");resp.getWriter().println("<h1>hello world</h1>");};@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req,resp);}
}
@ServletComponentScan包扫描 (入口类)
@ServletComponentScan
@SpringBootApplication
public class SpringbootWeb04Application {public static void main(String[] args) {SpringApplication.run(SpringbootWeb04Application.class, args);}}
第二种::配置类
package com.apesource.springboot_web_04.config;import com.apesource.springboot_web_04.servlet.MyServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Arrays;@Configuration
public class MyMvcConfig {//注册替换Servlet//替换:@WebServlet(urlPatterns="/myServlet")@Beanpublic ServletRegistrationBean doServlet(){ServletRegistrationBean<MyServlet> bean=new ServletRegistrationBean<MyServlet>();bean.setServlet(new MyServlet());bean.setUrlMappings(Arrays.asList("/myServlet"));return bean;}
}
四、切换为其他嵌入式Servlet容器
SpringBoot 默认针对Servlet容器提供以下支持:
Tomcat(默认使用)
Jetty :支持长连接项目(如:聊天页面)
Undertow : 不支持 JSP , 但是并发性能高,是高性能非阻塞的容器
默认Tomcat服务器
在spring-boot-starter-web启动器中默认引入了tomcat容器
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><version>2.1.0.RELEASE</version><scope>compile</scope>
</dependency>
切换 Jetty 容器
dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><!-- 排除tomcat容器 --><exclusions><exclusion><artifactId>spring-boot-starter-tomcat</artifactId><groupId>org.springframework.boot</groupId></exclusion></exclusions>
</dependency>
<!--引入其他的Servlet容器-->
<dependency><artifactId>spring-boot-starter-jetty</artifactId><groupId>org.springframework.boot</groupId>
</dependency>
Servlet容器:运行启动类就可启动,或将项目打成可执行的 jar 包
优点:简单、快捷;
缺点:默认不支持JSP、优化定制比较复杂使用定制器, 还需要知道 每个功能 的底层原理
外置Servlet容器:配置 Tomcat, 将项目部署到Tomcat中运行
五、restFul
满足这些约束条件和原则的应用程序或设计就是 RESTful。
设计RESTful风格的API:
1、在RESTful风格的架构中, 每个网址代表一种资源,所以网址中不能有动词,只能有名词。而且所
用的名词往往与数据库的表名对应。
2、HTTP动词设计: GET (获取资源) POST (新建资源) PUT (更新资源,客户端提供改变后的完整资源) DELETE (删除资源)
请求方式
含义
GET /zoos 列出所有动物园
POST /zoos 新建一个动物园
GET /zoos/ID 获取某个指定动物园的信息
PUT /zoos/ID 更新某个指定动物园的信息(提供该动物园的全部信息)
DELETE /zoos 删除某个动物园
GET /zoos/lD/animals 列出某个指定动物园的所有动物
DELETE /zoos/lD/animals/ID 删除某个指定动物园的指定动物
六、springboot与mybtais及mybatis-plus整合
MyBatisPlus的入门案例与简介,这个和其他课程都不太一样,其他的课程都是先介绍概念,然后再写入门案例。而对于MyBatisPlus的学习,我们将顺序做了+调整,主要的原因MyBatisPlus主要是对 MyBatis的简化,所有我们先体会下它简化在哪,然后再学习它是什么,以及它帮我们都做哪些事。
MybatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提供效率。
开发方式
基于SpringBoot使用MyBatisPlus
步骤1:创建数据库及表
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',18,'13991267980');
insert into user values(2,'Jerry','jerry',18,'13991267980');
insert into user values(3,'Jock','Jock',18,'13991267980');
步骤2:创建SpringBoot工程
步骤3:勾选配置使用技术
说明: 由于MP并未被收录到idea的系统内置配置,无法直接选择加入,需要手动在pom.xml中配置添加
步骤4:pom.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>
步骤5:添加MP的相关配置信息
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=GMTusername: rootpassword: 132456
步骤6:根据数据库表创建实体类
public class User { private Long id;private String name;private String password;private Integer age;private String tel;//setter...getter...toString方法略
}
步骤7:创建Dao接口
@Mapper
public interface UserDao extends BaseMapper<User>{
}
步骤8:编写引导类
@SpringBootApplication
public class Mybatisplus01QuickstartApplication {public static void main(String[] args) {SpringApplication.run(Mybatisplus01QuickstartApplication.class, args);}
}
说明:Dao接口要想被容器扫描到,有两种解决方案:
方案一:在Dao接口上添加@Mapper注解,并且确保Dao处在引导类所在包或其子包中
该方案的缺点是需要在每一Dao接口中添加注解
方案二:在引导类上添加@MapperScan注解,其属性为所要扫描的Dao所在包
该方案的好处是只需要写一次,则指定包下的所有Dao接口都能被扫描到,@Mapper就可以
不写。
步骤9:编写测试类
@SpringBootTest
class MpDemoApplicationTests {@Autowiredprivate UserDao userDao;@Testpublic void testGetAll() {List<User> userList = userDao.selectList(null);System.out.println(userList);}
}
分页功能
分页查询使用的方法是:
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper)
IPage:用来构建分页查询条件
Wrapper:用来构建条件查询的条件,目前我们没有可直接传为Null
IPage:返回值,你会发现构建分页条件和方法的返回值都是IPage
IPage是一个接口,我们需要找到它的实现类来构建它,具体的实现类,可以进入到IPage类中按ctrl+h, 会找到其有一个实现类为Page。
步骤1:调用方法传入参数获取返回值
@SpringBootTest
class Mybatisplus01QuickstartApplicationTests {@Autowiredprivate UserDao userDao;//分页查询@Testvoid testSelectPage(){//1 创建IPage分页对象,设置分页参数,1为当前页码,3为每页显示的记录数
步骤2:设置分页拦截器 这个拦截器MP已经为我们提供好了,我们只需要将其配置成Spring管理的bean对象即可。
步骤3:运行测试程序 如果想查看MP执行的SQL语句,可以修改application.yml配置文件,三.DQL编程控制 增删改查四个操作中,查询是非常重要的也是非常复杂的操作,这块需要我们重点学习下,这节我们主
要学习的内容有:
条件查询方式
查询投影
查询条件设定
字段映射与表名映射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());}
}
步骤2:设置分页拦截器
这个拦截器MP已经为我们提供好了,我们只需要将其配置成Spring管理的bean对象即可。
@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){//1 创建MybatisPlusInterceptor拦截器对象MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();//2 添加分页拦截器mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());return mpInterceptor;}
}
步骤3:运行测试程序
如果想查看MP执行的SQL语句,可以修改application.yml配置文件,
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印SQL日志到控制台
DQL编程控制
增删改查四个操作中,查询是非常重要的也是非常复杂的操作,这块需要我们重点学习下,这节我们主要学习的内容有:
条件查询方式
查询投影
查询条件设定
字段映射与表名映射
条件查询
1.构建条件查询
在进行查询的时候,我们的入口是在Wrapper这个类上,因为它是一个接口,所以我们需要去找它对应的实现类,关于实现类也有很多,说明我们有多种构建查询条件对象的方式,
先来看第一种:QueryWrapper
/*** QueryWrapper的lt小于()方法* 查询年龄小于19的*/@Testvoid testGetAll(){QueryWrapper qw=new QueryWrapper();qw.lt("age",19);List<User> userlist = userMapper.selectList(qw);System.out.println(userlist);}
lt: 小于(<) ,最终的sql语句为 select id,name,password,age,tel FROM user WHERE (age < ?)
接着来看第二种:LambdaQueryWrapper
/*** LambdaQueryWrapper的lt小于()方法* 查询年龄小于19的*/@Testvoid testGetAll2(){LambdaQueryWrapper<User> lqw=new LambdaQueryWrapper<User>();lqw.lt(User::getAge,19);List<User> users = userMapper.selectList(lqw);System.out.println(users);}
多条件构建
需求:查询数据库表中,年龄在10岁到30岁之间的用户信息
/*** ,年龄在10岁到30岁之间的用户信息* gt:大于(>)*/@Testvoid testGetAll3(){LambdaQueryWrapper<User> lqw=new LambdaQueryWrapper<User>();lqw.lt(User::getAge,30);lqw.gt(User::getAge,10);List<User> users = userMapper.selectList(lqw);System.out.println(users);}
gt:大于(>),最终的SQL语句为:SELECT id,name,password,age,tel FROM user WHERE (age < ? AND age > ?)
构建多条件的时候,可以支持链式编程
LambdaQueryWrapper<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的数据
/*** 年龄小于10或年龄大于30的* or()就相当于我们sql语句中的or关键字,不加默认是and,* 、构建多条件的时候,可以支持链式编程*/@Testvoid testGetAll4(){LambdaQueryWrapper<User> lqw=new LambdaQueryWrapper<User>();lqw.lt(User::getAge,10).or().gt(User::getAge,30);List<User> users = userMapper.selectList(lqw);System.out.println(users);}
or()就相当于我们sql语句中的or关键字,不加默认是and,最终的sql语句为:
SELECT id,name,password,age,tel FROM user WHERE (age < ? OR age > ?)
null判定
需求:查询数据库表中,根据输入年龄范围来查询符合条件的记录
用户在输入值的时候,
如果只输入第一个框,说明要查询大于该年龄的用户
如果只输入第二个框,说明要查询小于该年龄的用户
如果两个框都输入了,说明要查询年龄在两个范围之间的用户
我们可以使用两个简单数据类型,也可以使用一个模型类,但是User类中目前只有一个age属性
新建一个模型类,让其继承User类,并在其中添加age2属性,UserQuery在拥有User属性后同时添加了age2属性。
@Data
public class User {private Long id;private String name;private String password;private Integer age;private String tel;
}
@Data
public class UserQuery extends User {private Integer age2;
}
@Testvoid 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给我们提供的简化方式:
@Testvoid testGetAll(){//模拟页面传递过来的查询数据UserQuery uq = new UserQuery();uq.setAge(10);uq.setAge2(30);LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();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);}
查询投影
目前我们在查询数据的时候,什么都没有做默认就是查询表中所有字段的内容,我们所说的查询投影即不查询所有字段,只查询出指定内容的数据。
@Testvoid testGetAll7(){LambdaQueryWrapper<User> lqw=new LambdaQueryWrapper<User>();lqw.select(User::getId,User::getName,User::getAge);List<User> users = userMapper.selectList(lqw);System.out.println(users);}
// 如果使用的不是lambda,就需要手动指定字段@Testvoid testGetAll8(){QueryWrapper<User> lqw=new QueryWrapper<User>();lqw.select("id","name","age","tel");List<User> users = userMapper.selectList(lqw);System.out.println(users);}
聚合查询
需求:聚合函数查询,完成count、max、min、avg、sum的使用
count:总记录数
max:最大值
min:最小值
avg:平均值
sum:求和
//聚合查询@Testvoid testGetAll9(){QueryWrapper<User> lqw=new QueryWrapper<User>();
// lqw.select("count(*) as count");
// lqw.select("max(age) as maxAge");
// lqw.select("min(age) as minAge");
// lqw.select("sum(age) as sumAge");lqw.select("avg(age) as avgAge");List<Map<String, Object>> maps = userMapper.selectMaps(lqw);System.out.println(maps);}
分组查询
需求:分组查询,完成 group by的查询使用
//分组查询@Testvoid testGetAll10(){QueryWrapper<User> lqw=new QueryWrapper<User>();lqw.select("count(*) as count,tel");lqw.groupBy("tel");List<Map<String, Object>> maps = userMapper.selectMaps(lqw);System.out.println(maps);}
注意:聚合与分组查询,无法使用lambda表达式来完成
查询条件
前面我们只使用了lt()和gt(),除了这两个方法外,MP还封装了很多条件对应的方法,这一节我们重点把
MP提供的查询条件方法进行学习下。
MP的查询条件有很多:
范围匹配(> 、 = 、between)
模糊匹配(like)
空判定(null)
包含性匹配(in)
分组(group)
排序(order)
......
1.等值查询
需求:根据用户名和密码查询用户信息
@Testvoid testGetAll11(){LambdaQueryWrapper<User> lqw=new LambdaQueryWrapper<User>();lqw.eq(User::getName,"Jerry").eq(User::getPassword,"jerry");User user = userMapper.selectOne(lqw);System.out.println(user);}
eq(): 相当于 =,对应的sql语句为:SELECT id,name,password,age,tel FROM user WHERE (name = ? AND password = ?)
selectList:查询结果为多个或者单个
selectOne:查询结果为单个
2.范围查询
需求:对年龄进行范围查询,使用lt()、le()、gt()、ge()、between()进行范围查询
//范围查询@Testvoid testGetAll12(){LambdaQueryWrapper<User> lqw=new LambdaQueryWrapper<User>();lqw.between(User::getAge,10,30);List<User> users = userMapper.selectList(lqw);System.out.println(users);}
gt():大于(>)
ge():大于等于(>=)
lt():小于(<)
lte():小于等于(<=)
between():between ? and ?
3.查询表中name属性的值以J开头的用户信息,使用like进行模糊查询
//模糊查询@Testvoid testGetAll14(){LambdaQueryWrapper<User> lqw=new LambdaQueryWrapper<User>();lqw.like(User::getName,"J");List<User> users = userMapper.selectList(lqw);System.out.println(users);}
like():前后加百分号,如 %J%
likeLeft():前面加百分号,如 %J
likeRight():后面加百分号,如 J%
4.排序查询::查询所有数据,然后按照id降序
//排序查询
// 需求:查询所有数据,然后按照id降序@Testvoid testGetAll15(){LambdaQueryWrapper<User> lwq=new LambdaQueryWrapper<>();lwq.orderBy(true,false,User::getId);userMapper.selectList(lwq);}
映射匹配兼容性
前面我们已经能从表中查询出数据,并将数据封装到模型类中,这整个过程涉及到一张表和一个模型类:
那么问题就来了:
问题1:表字段与编码属性设计不同步
当表的列名和模型类的属性名发生不一致,就会导致数据封装不到模型对象,这个时候就需要其中一方做出修改,那如果前提是两边都不能改又该如何解决?
MP给我们提供了一个注解@TableField,使用该注解可以实现模型类属性名和表的列名之间的映射关系
问题2:编码中添加了数据库中未定义的属性
当模型类中多了一个数据库表不存在的字段,就会导致生成的sql语句中在select的时候查询了数据库不
存在的字段,程序运行就会报错,错误信息为:
Unknown column '多出来的字段名称' in 'field list'
具体的解决方案用到的还是@TableField注解,它有一个属性叫exist,设置该字段是否在数据库表
中存在,如果设置为false则不存在,生成sql语句查询的时候,就不会再查询该字段了。
采用默认查询开放了更多的字段查看权限

表名与编码开发设计不同步


DML编程控制

2.ID生成策略对比
简化配置
只需要在配置文件中添加如下内容:
mybatis-plus:global-config:db-config:id-type: assign_id
配置起来还是比较繁琐,简化方式为在配置文件中配置如下内容:
mybatis-plus:global-config:db-config:table-prefix: tbl_
多记录操作
@Testvoid testDelete(){//删除多条指定数据List<Long> list=new ArrayList<>();list.add(1402551342481838081L);list.add(1402551344241838054L);list.add(1402851342487778081L);userMapper.deleteBatchIds(list);}
//根据传入的ID集合查询用户信息@Testvoid testGetByIds(){//查询指定多条数据List<Long> list=new ArrayList<>();list.add(1L);list.add(2L);list.add(3L);userMapper.selectBatchIds(list);}

实体类添加属性
(1)添加与数据库表的列对应的一个属性名,名称可以任意,如果和数据表列名对不上,可以使用 @TableField进行关系映射,如果一致,则会自动对应。
(2)标识新增的字段为逻辑删除字段,使用 @TableLogic
运行删除方法
@Testvoid testDelete2(){userMapper.deleteById(1L);}

思考:逻辑删除,对查询有没有影响呢
执行查询操作
@Testvoid testFind(){System.out.println(userMapper.selectList(null));}


乐观锁
咱们可以把乐观锁的这个过程想象成两个人借同一本书的场景,这样就好理解了:
你可以把数据库里的那条数据当成一本畅销书,而version
列就像是这本书的 “借阅次数标签”,一开始这标签上写着 “1”(默认值 1)。
现在有甲和乙两个人都想借这本书,并且借完后要在标签上把次数加 1。
- 甲先来借书,他看到标签上是 “1”,心里记下了这个数字,打算借完后改成 “2”。
- 乙紧接着也来了,他看到的标签也是 “1”,同样打算借完后改成 “2”。
这时候就有两种情况:
甲先借到了书,他按照自己的想法,把标签从 “1” 改成了 “2”,然后把书还回去了。
等乙再来借书时,他拿出自己记下的 “1” 去核对标签,发现标签已经是 “2” 了,跟自己记的不一样,就知道这本书被别人动过了,自己借不了了,只能放弃。要是乙先借到了书,他把标签从 “1” 改成 “2” 还回去。
甲再来时,同样发现标签变成了 “2”,和自己记的 “1” 对不上,也借不了,只能放弃。所以不管是甲先还是乙先,最终只有一个人能成功借书并改标签,另一个人会因为标签对不上而失败。这就是乐观锁的意思:它乐观地认为大家不会抢着修改数据,但会通过 “版本号(version)” 来核对,一旦发现版本对不上,就说明数据被别人改过了,自己的修改就失败,避免了冲突。


步骤3:添加乐观锁的拦截器
package com.apesource.spring_mybatis_plus_01.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MpConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){//1.定义Mp拦截器MybatisPlusInterceptor mybatisPlusInterceptor=new MybatisPlusInterceptor();//2.添加乐观锁拦截器mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return mybatisPlusInterceptor;}
}
步骤4:执行更新操作
/*** 测试乐观锁*/@Testvoid testUpdate(){User user=new User();user.setId(4L);user.setName("王升1");userMapper.updateById(user);}
@Testvoid testUpdate2(){User user=new User();user.setId(4L);user.setName("王升2");user.setVersion(1);userMapper.updateById(user);}
@Testvoid testUpdate3(){//1.先通过要修改的数据id将当前数据查询出来User user=userMapper.selectById(4L);//2.将要修改的属性逐一设置进去user.setName("王升3");userMapper.updateById(user);}
@Testvoid testUpdate4(){User user=userMapper.selectById(3L);User user2=userMapper.selectById(3L);user2.setName("Jock aaa");userMapper.updateById(user2);user.setName("Jock bbb");userMapper.updateById(user);}