Mybatis-Plus

mybatis-plus学习笔记,参考B站视频

参考博客

Mybatis-Plus官方文档

快速入门

使用mybatis-plus的基础步骤

  1. 引入mybatisPlus的Maven依赖,代替原始Mybatis依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
    </dependency>
  2. 定义Mapper接口继承自BaseMapper

    1
    2
    3
    4
    //继承BaseMapper接口,传入数据库对应实体类的泛型
    public interface UserMapper extends BaseMapper<User> {

    }
  3. 单元测试即可直接使用

    mapper存在一系列增删改查的方法可以直接调用

    image-20231021195840724

小插曲

测试如下方法

1
2
3
4
5
@Test
void testQueryByIds() {
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L));
users.forEach(System.out::println);
}

一直报错:

1
java: 找不到符号   符号:   方法 of(long,long,long,long)   位置: 接口 java.util.List

原因:

List.of()JDK9之后才有的版本

解决方法:提升java编译的JDK版本

修改如下位置(注意:无论修改ProjectStructure还是pom.xml中的java版本均无效)

image-20231021200120027

常见注解

MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。

基本约定

image-20231021200634159

常用注解

  • @TableName:用来指定表名
  • @TableId:用来指定表中的主键字段信息
  • @TableField:用来指定表中的普通字段信息

注意的点:

当实体类和数据库不满足既定的约定的话,就必须要使用对应的注解

  • 对于@TableId是用来指定主键字段,对于数据库中主键存在一些属性(比如自增),所以注解中有时也要注明主键类型
    • IdType枚举:
      • AUTO:数据库自增长
      • INPUT:通过set方法自行输入
      • ASSIGN_ID:分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法
    • 默认采用雪花算法
  • 使用@TableField的常见场景:
    • 成员变量名与数据库字段名不一致
    • 成员变量名以is开头,且是布尔值
      • 布尔变量反射时会去掉is
    • 成员变量名与数据库关键字冲突
      • 使用转义字符进行说明
    • 成员变量不是数据库字段
      • 使用exist说明

image-20231021202134848

常见配置

MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。

1
2
3
4
5
6
7
8
9
10
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 别名扫描包
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: assign_id # id为雪花算法生成
update-strategy: not_null # 更新策略:只更新非空字段

具体可参考官方文档:使用配置 | MyBatis-Plus (baomidou.com)

总结

Mybatis-Plus使用的基本流程

  • 引入起步依赖
  • 自定义Mapper,继承自BaseMapper
  • 在实体类上添加注解申明信息
  • application.yml中根据需要添加配置

核心功能

条件构造器

Mybatis-Plus支持各种复杂的where条件,可以满足日常开发的各种需求

下图中方法参数中的wrapper其实就是条件构造器

image-20231021205520443

image-20231021205627495

image-20231021205819231

案例

基于QueryWrapper的查询

①查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testQueryWrapper(){
//1.构造查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id","username","info","balance")
.like("username","o")
.ge("balance",1000);
//2.查询
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}

②更新用户名为jack的用户的余额为2000

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testUpdateByQueryWrapper(){
//1.构造更新数据
User user = new User();
user.setBalance(2000);
//2.构造更新条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.eq("username","jack");
//3.更新
userMapper.update(user,wrapper);
}

基于UpdateWrapper的更新

需求:更新id为1,2,4的用户的余额,扣200

1
2
3
4
5
6
7
8
9
10
@Test
public void testUpdateWrapper(){
//1.构造upadteWrapper
List<Long> ids = List.of(1L,2L,4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance=balance-200")
.in("id",ids);
//2.更细
userMapper.update(null,wrapper);
}

基于LambdaQueryWrapper的查询

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testLambdaQueryWrapper(){
//1.构造查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.select(User::getId,User::getUsername,User::getInfo,User::getBalance)
.like(User::getUsername,"o")
.ge(User::getBalance,10);
//2.查询
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}

总结

  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
  • UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
  • 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码

自定义SQL

我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。

需求:将id在指定范围的用户(例如1、2、4 )的余额扣减指定值

背景

image-20231021214903600

  • 直接写SQL语句,where条件语句比较繁琐
  • 全部用Wrapper来完成,不符合业务逻辑规范(参数往往只允许在Service层去定义)

自定义SQL流程

①基于Wrapper构建where条件

1
2
3
4
5
List<Long> ids = List.of(1L,2L,4L);
int amount=-20000;
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>()
.in(User::getId,ids);
userMapper.updateBalanceById(lambdaQueryWrapper,amount);

②在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew

1
2
3
4
5
public interface UserMapper extends BaseMapper<User> {

void updateBalanceById(@Param("ew")LambdaQueryWrapper<User> wrapper,@Param("amount") int amount);

}

③自定义SQL,并使用Wrapper条件

1
2
3
<update id="updateBalanceById">
Update user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>

Service接口

Mybatis-Plus还提供了Service层的接口,有一系列可用的增删改查方法

image-20231022110437139

image-20231022105850973

Mybatis-Plus Service接口使用流程

  1. 自定义Service接口继承IService接口

    1
    2
    public interface IUserService extends IService<User> {
    }
  2. 自定义Service实现类,实现自定义接口并继承ServiceImpl类

    1
    2
    3
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    }

实例一:基于Restful风格实现下面的接口

image-20231022113118176

引入对应依赖
1
2
3
4
5
6
7
8
9
10
11
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
swagger配置信息

本实例借助Swagger实现接口功能的在线测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: bang@bang.cn
concat: bang
url: https://www.bang.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.itheima.mp.controller
创建对应的实体类
  • userFormDTO:代表新增用户的表单
  • UserVO:代表查询的返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.itheima.mp.domain.dto;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {

@ApiModelProperty("id")
private Long id;

@ApiModelProperty("用户名")
private String username;

@ApiModelProperty("密码")
private String password;

@ApiModelProperty("注册手机号")
private String phone;

@ApiModelProperty("详细信息,JSON风格")
private String info;

@ApiModelProperty("账户余额")
private Integer balance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.itheima.mp.domain.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {

@ApiModelProperty("用户id")
private Long id;

@ApiModelProperty("用户名")
private String username;

@ApiModelProperty("详细信息")
private String info;

@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;

@ApiModelProperty("账户余额")
private Integer balance;
}
创建响应结果实体类
1
2
3
4
5
6
7
8
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<T>{
private T data; //响应数据
private Integer status; //响应状态码
private String message; //响应信息
}
按照restful风格编写Controller接口方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Api(tags = "用户管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
public class UserController {
//构造函数注入
private final IUserService userService;

//新增用户
@ApiOperation("新增用户接口")
@PostMapping
public JsonResult<UserVO> saveUser(@RequestBody UserFormDTO userFormDTO){
//1.DTO对象转换成PO对象
User user = BeanUtil.copyProperties(userFormDTO, User.class);
//2.新增
userService.save(user);

JsonResult result = new JsonResult<UserVO>(null,200,"新增用户成功");
return result;
}

//删除用户
@ApiOperation("删除用户接口")
@DeleteMapping("/{id}")
public JsonResult<UserVO> deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id){

userService.removeById(id);
JsonResult<UserVO> result = new JsonResult<UserVO>(null,200,"删除用户成功");
return result;
}

//根据id查询用户
@ApiOperation("根据id查询用户接口")
@GetMapping("/{id}")
public JsonResult<UserVO> queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
//1.查询用户
User user = userService.getById(id);
//对象类型转换
UserVO userVO = BeanUtil.copyProperties(user,UserVO.class);
JsonResult<UserVO> result = new JsonResult<UserVO>(userVO,200,"查询成功");
return result;
}

//根据id批量查询
@ApiOperation("根据id批量查询接口")
@GetMapping
public JsonResult<List<UserVO>> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
//1.查询用户集合
List<User> userList = userService.listByIds(ids);
//2.类型转换
List<UserVO> userVOList = BeanUtil.copyToList(userList, UserVO.class);
JsonResult<List<UserVO>> result = new JsonResult<>(userVOList,200,"查询成功");
return result;
}

//根据id扣减余额
@ApiOperation("根据id扣减余额接口")
@PutMapping("/{id}/deduction/{money}")
public JsonResult<UserVO> reductionBalanceById(@ApiParam("用户id") @PathVariable("id") Long id,@ApiParam("扣减金额") @PathVariable("money") Integer money){

boolean r = userService.reductionBalanceById(id,money);
JsonResult<UserVO> result;
if(r) result = new JsonResult<UserVO>(null,200,"扣减余额成功");
else result = new JsonResult<UserVO>(null,500,"扣减余额失败");
return result;
}

}

对于需求5,删减指定用户金额,业务逻辑无法借助`Mybatis-plus现提供的方法,所以需要自定义方法

UserMapper接口

1
void updateBalanceById(@Param("ew")LambdaQueryWrapper<User> wrapper,@Param("amount") int amount);

UserMapper.xml

1
2
3
<update id="updateBalanceById">
Update user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>

IUserService接口

1
2
3
public interface IUserService extends IService<User> {
public boolean reductionBalanceById(Long id,Integer money);
}

IUserService接口实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

private final UserMapper userMapper;
@Override
public boolean reductionBalanceById(Long id, Integer money) {
//1.查询用户余额
User user = getById(id);
if(user==null || user.getBalance()<money){
return false;
}
//1.定义wrapper
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>().eq(User::getId,id);
userMapper.updateBalanceById(lambdaQueryWrapper,money);
return true;
}
}

访问localhost:8080/doc.html即可测试

实例二:IService的Lambda查询

需求:实现一个根据复杂条件查询用户的接口,查询条件如下:

  • name:用户名关键字,可以为空

  • status:用户状态,可以为空

  • minBalance:最小余额,可以为空

  • maxBalance:最大余额,可以为空

上述功能其实就是多条件筛选,如果采用传统自己编写SQL语句的话,会非常繁琐

image-20231022162757090

如果采用IService提供的lambdaQuery的话会非常简洁

IUserServiceImpl中实现如下方法

1
2
3
4
5
6
7
8
@Override
public List<User> queryUsersByCondition(String name, Integer status, Integer minBalance, Integer maxBalance) {
return lambdaQuery().like(name!=null,User::getUsername,name)
.eq(status!=null,User::getStatus,status)
.gt(minBalance!=null,User::getBalance,minBalance)
.lt(maxBalance!=null,User::getBalance,maxBalance)
.list();
}

UserController添加的代码

1
2
3
4
5
6
7
8
9
10
11
12
//根据条件进行用户查询
@ApiOperation("根据条件查询用户接口")
@GetMapping("/list")
public JsonResult<List<UserVO>> queryUsers(UserQuery userQuery){
//1.查询
List<User> userList = userService.queryUsersByCondition(userQuery.getName(), userQuery.getStatus()
, userQuery.getMinBalance(), userQuery.getMaxBalance());
//2.实体类型转换
List<UserVO> userVOS = BeanUtil.copyToList(userList, UserVO.class);
JsonResult<List<UserVO>> result = new JsonResult<>(userVOS, 200, "查询成功");
return result;
}

案例三:IService的Lambda更新

需求:改造根据id修改用户余额的接口,要求如下

①完成对用户状态校验

②完成对用户余额校验

③如果扣减后余额为0,则将用户status修改为冻结状态(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean reductionBalanceById(Long id, Integer money) {
//1.查询用户余额
User user = getById(id);
if(user==null || user.getBalance()<money){
return false;
}
//2.计算余额,余额为零则需要将用户账户冻结
int remainBalance = user.getBalance()-money;
lambdaUpdate().set(User::getBalance,remainBalance)
.set(remainBalance==0,User::getStatus,2)
.eq(User::getId,id)
.eq(User::getBalance,user.getBalance()) //添加乐观锁,提高并发安全性
.update();
return true;
}

案例四:Iservice批量新增

需求:批量插入10万条用户数据,并作出对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//测试循环插入所需时间
@Test
public void testSaveByFor(){
long start = System.currentTimeMillis();
for(int i=0;i<100000;i++){
userService.save(buildUser(i));
}
long end = System.currentTimeMillis();
System.out.printf("耗费时间:"+(end-start)+" ms");
}


//测试批次插入所需时间
@Test
public void testSaveByBatch(){

List<User> list = new ArrayList<>(1000);
long start = System.currentTimeMillis();
for(int i=0;i<100000;i++){
list.add(buildUser(i));
//每1000条批次插入一次
if(i%1000==0){
userService.saveBatch(list);
list.clear();
}
}
long end = System.currentTimeMillis();
System.out.printf("耗费时间:"+(end-start)+" ms");
}

循环插入所需时间

image-20231022184037735

批次插入所需时间

image-20231022184051514

开启rewriteBatchedStatements参数

1
url: jdbc:mysql://localhost:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true

image-20231022184434795

结论

批处理方案:

  • 普通for循环逐条插入速度极差,不推荐

  • MP的批量新增,基于预编译的批处理,性能不错

  • 配置jdbc参数,开rewriteBatchedStatements,性能最好

扩展功能

代码生成

Mybatis-plus使用流程

  • 定义数据库对应的实体类
  • 定义mapper接口并继承自BaseMapper
  • 定义xxService接口并继承自IService
  • 定义xxService实现类,并继承自ServiceImpl

image-20231022184909209

Mybatis-Plus提供了这些代码的自动生成,官方文档

利用MybatisPlus插件来生成对应代码

image-20231022185434944

使用步骤

image-20231022185933343

image-20231022185940369

image-20231022185915708

image-20231022185949249

静态工具

静态工具中的方法与IService中的方法比较相似,其由于是静态的,不用创建出对象即可调用;但是需要传入数据库对应实体类的class(泛型)

image-20231022202619592

案例:静态工具查询

需求:

①改造根据id查询用户的接口,查询用户的同时,查询出用户对应的所有地址

②改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址

③实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常(练习)

上次需求的实现如果按照常规实现方式时,就会出现循环依赖UserService中会注入AddressServiceAddressService中也会注入UserService

此时如借助静态工具,则无需相互注入,解决循环依赖问题

创建地址数据表VO类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Data
@ApiModel(description = "收货地址VO")
public class AddressVO{

@ApiModelProperty("id")
private Long id;

@ApiModelProperty("用户ID")
private Long userId;

@ApiModelProperty("省")
private String province;

@ApiModelProperty("市")
private String city;

@ApiModelProperty("县/区")
private String town;

@ApiModelProperty("手机")
private String mobile;

@ApiModelProperty("详细地址")
private String street;

@ApiModelProperty("联系人")
private String contact;

@ApiModelProperty("是否是默认 1默认 0否")
private Boolean isDefault;

@ApiModelProperty("备注")
private String notes;
}

改写UserVO,新增地址属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;

@ApiModelProperty("用户名")
private String username;

@ApiModelProperty("详细信息")
private String info;

@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;

@ApiModelProperty("账户余额")
private Integer balance;

@ApiModelProperty("收货地址列表")
private List<AddressVO> addresss;
}

根据Id查询单个用户信息及其收货地址

改写UserController中的queryUserById方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//根据id查询用户
@ApiOperation("根据id查询用户接口")
@GetMapping("/{id}")
public JsonResult<UserVO> queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
// //1.查询用户
// User user = userService.getById(id);
// //对象类型转换
// UserVO userVO = BeanUtil.copyProperties(user,UserVO.class);
// JsonResult<UserVO> result = new JsonResult<UserVO>(userVO,200,"查询成功");
// return result;

UserVO userVO = userService.queryUserAndAddressById(id);
if(userVO==null){
return new JsonResult<>(null,600,"查询失败");
}else{
return new JsonResult<>(userVO,200,"查询成功");
}
}

新增IUserService中的queryUserAndAddressById方法

1
UserVO queryUserAndAddressById(Long id);

新增UserServiceImpl中的queryUserAndAddressById方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public UserVO queryUserAndAddressById(Long id) {
//1.查询用户
User user = getById(id);
//用户不存在或者用户账户被冻结
if(user==null || user.getStatus()==2){
return null;
}
//2.用户对象转换
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
List<Address> addressList = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, id)
.list();
//3.地址对象转换
if(CollUtil.isNotEmpty(addressList)){
List<AddressVO> addressVOS = BeanUtil.copyToList(addressList, AddressVO.class);
userVO.setAddresses(addressVOS);
}
return userVO;
}
根据id列表查询多个用户信息及其收货地址

改写UserController中的queryUserByIds方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//根据id批量查询
@ApiOperation("根据id批量查询接口")
@GetMapping
public JsonResult<List<UserVO>> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
// //1.查询用户集合
// List<User> userList = userService.listByIds(ids);
// //2.类型转换
// List<UserVO> userVOList = BeanUtil.copyToList(userList, UserVO.class);
// JsonResult<List<UserVO>> result = new JsonResult<>(userVOList,200,"查询成功");
// return result;

List<UserVO> userVOS = userService.listUserAndAddressByIds(ids);
if(userVOS==null) return new JsonResult<>(null,600,"查询失败");
else return new JsonResult<>(userVOS,200,"查询成功");
}

新增IUserService中的queryUserAndAddressByIds方法

1
List<UserVO> listUserAndAddressByIds(List<Long> ids);

新增UserServiceImpl中的queryUserAndAddressByIds方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public List<UserVO> listUserAndAddressByIds(List<Long> ids) {
//1.根据id查询用户
List<User> userList = listByIds(ids);
if(CollUtil.isEmpty(userList)){
return null;
}

//根据用户id查出所有地址
//一次性查出所有id比for循环多次查询效率要搞
//时间主要在于数据库连接通信上
List<Address> addressList = Db.lambdaQuery(Address.class)
.in(Address::getUserId, ids)
.list();

List<UserVO> userVOS = BeanUtil.copyToList(userList, UserVO.class);
if(CollUtil.isNotEmpty(addressList)){
List<AddressVO> addressVOS = BeanUtil.copyToList(addressList, AddressVO.class);
//按照用户id进行分组
Map<Long, List<AddressVO>> map = addressVOS.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
//将用户与其对应地址对应起来
for (UserVO userVO : userVOS) {
userVO.setAddresses(map.get(userVO.getId()));
}
}
return userVOS;



// //用户类型转换
// List<UserVO> userVOS = BeanUtil.copyToList(userList, UserVO.class);
// //2.查询对应收货地址
// for (UserVO userVO : userVOS) {
// List<Address> addressList = Db.lambdaQuery(Address.class)
// .eq(Address::getUserId, userVO.getId())
// .list();
// //转换地址类型
// if(CollUtil.isNotEmpty(addressList)){
// List<AddressVO> addressVOS = BeanUtil.copyToList(addressList, AddressVO.class);
// userVO.setAddresses(addressVOS);
// }
// }
// return userVOS;
}

逻辑删除

逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:

  • 在表中添加一个字段标记数据是否被删除

  • 当删除数据时把标记置为1

  • 查询时只查询标记为0的数据

背景:淘宝中的购物订单模块,用户点击订单删除按钮,本地会消失,但是实际上数据库中该数据并未被删除,应为对于商家而言,订单数据比较重要;此时采用的就是逻辑删除逻辑,用户查询时不会查询该数据,但该数据在数据库中仍然存在

image-20231022213309968

MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:

1
2
3
4
5
6
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

注意

逻辑删除本身也有自己的问题,比如:

  • 会导致数据库表垃圾数据越来越多,影响查询效率

  • SQL中全都需要对逻辑删除字段做判断,影响查询效率

因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

枚举处理器

Java中的枚举类型与数据库整数类型之间的转换问题

image-20231022215018724

image-20231022214852226

实现PO类中的枚举变量与数据库字段的转换

①给枚举中的与数据库对应value值添加@EnumValue注解

image-20231022215238486

②在配置文件中配置统一的枚举处理器,实现类型转换

1
2
3
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
实例

定义Status字段对应的枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
public enum UserStatus {
Normal(1,"正常"),
FREEZE(2,"冻结");
//通过注解与数据库字段关联起来
@EnumValue
private int value;
private String description;

UserStatus(int value, String description) {
this.value = value;
this.description = description;
}
}

配置文件中定义Mybatis枚举处理器

1
2
3
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

将UserPO和UserVO类中的Status属性改成枚举类型

枚举类型属性返回值的更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Getter
public enum UserStatus {
Normal(1,"正常"),
FREEZE(2,"冻结");
//通过注解与数据库字段关联起来
@EnumValue
private int value;
//通过注解@JsonValue将该属性作为枚举的返回值
@JsonValue
private String description;

UserStatus(int value, String description) {
this.value = value;
this.description = description;
}
}

image-20231022220514144

JSON处理器

Java数据类型与数据库中Json数据类型的转换

image-20231022221135194

image-20231022221311719

Json处理器使用流程

  • 在VO/PO实体类的对应属性上添加typeHandler属性,让对应处理器生效
  • 在实体类的@TableName注解中开启autoResultMap,对象嵌套过程中的自动映射
实例

创建数据库Json字段对应的实体类

1
2
3
4
5
6
7
8
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}

修改实体类User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Data
@TableName(value = "user",autoResultMap = true)
public class User {

/**
* 用户id
*/
@TableId("id")
private Long id;

/**
* 用户名
*/
@TableField("`username`")
private String username;

/**
* 密码
*/
private String password;

/**
* 注册手机号
*/
private String phone;

/**
* 详细信息
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;

/**
* 使用状态(1正常 2冻结)
*/
private UserStatus status;

/**
* 账户余额
*/
private Integer balance;

/**
* 创建时间
*/
private LocalDateTime createTime;

/**
* 更新时间
*/
private LocalDateTime updateTime;

}

更改VO实体类

image-20231022222341097

插件功能

MyBatisPlus基于MyBatis的Interceptor实现了一个基础拦截器,并在内部保存了MyBatisPlus的内置拦截器的集合

image-20231022222800205

MyBatisPlus提供的内置拦截器有下面这些

image-20231022222545775

其中最常用的是分页插件

分页插件

分页插件的配置

首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class MybatisConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1.初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//2.添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
pageInterceptor.setMaxLimit(1000L); //设置分页上限
//加入核心插件
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}

分页API的使用

image-20231022223335020

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//测试分页插件的使用
@Test
public void testPage(){
//1.查询
int pageNo=1;
int pageSize=5;
//1.1分页参数
Page<User> page = Page.of(pageNo, pageSize);
//1.2分页排序参数,通过OrderItem来指定按照某个字段升序(true)或者降序(false)
page.addOrder(new OrderItem("balance",true));
//1.3分页查询
Page<User> p = userService.page(page);

//2.数据总数
System.out.println("total="+p.getTotal());
//3.总页数
System.out.println("pages="+p.getPages());
//4.分页数据
List<User> records = page.getRecords();
records.forEach(System.out::println);
}

通用分页实体

实例:简单分页查询

需求:遵循下面的接口规范,编写一个UserController接口,实现User的分页查询

image-20231023101326507

返回值类

image-20231023101526797

创建通用的查询参数实体

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@ApiModel("查询通用实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("页大小")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private boolean isAsc;
}

用户查询继承与查询实体类

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery extends PageQuery{
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}

创建通用的查询结果实体

1
2
3
4
5
6
7
8
9
10
@Data
@ApiModel("通用查询结果实体类")
public class QueryDTO<T> {
@ApiModelProperty("数据总数")
private Long total;
@ApiModelProperty("页面总数")
private Long pages;
@ApiModelProperty("当前页面数据")
private List<T> data;
}

Service层编写对应方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public QueryDTO<UserVO> queryUsersByPage(UserQuery query) {

String name = query.getName();
Integer status = query.getStatus();
//1.1构造分页查询条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
if(StrUtil.isNotEmpty(query.getSortBy())){
page.addOrder(new OrderItem(query.getSortBy(),query.isAsc()));
}else{
page.addOrder(new OrderItem("update_time",false));
}
//1.2构造条件查询
Page<User> pages = lambdaQuery().like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);

//2.构造结构实体实例
QueryDTO<UserVO> userVOPageDTO = new QueryDTO<>();

List<UserVO> userVOS = BeanUtil.copyToList(pages.getRecords(), UserVO.class);
userVOPageDTO.setTotal(pages.getTotal());
userVOPageDTO.setPages(pages.getPages());
userVOPageDTO.setData(userVOS);
return userVOPageDTO;
}

编写对应的Controller层方法

1
2
3
4
5
6
7
8
//根据条件进行分页查询
@ApiOperation("根据条件分页查询")
@GetMapping("/page")
public JsonResult<QueryDTO<UserVO>> queryUsersByPage(UserQuery userQuery){
QueryDTO<UserVO> userVOPageDTO = userService.queryUsersByPage(userQuery);
return new JsonResult<>(userVOPageDTO,200,"查询成功");

}

优化:函数封装

service层中page分页条件的构建和根据查询结果构造分页查询结果实体的代码与业务无关,可以单独抽取出来进行封装

  • 在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
  • 在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@ApiModel("查询通用实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("页大小")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private boolean isAsc;

public <PO> Page<PO> toMybatisPage(){
Page<PO> page = Page.of(pageNo, pageSize);
if(StrUtil.isNotEmpty(sortBy)){
page.addOrder(new OrderItem(sortBy,isAsc));
}else{
page.addOrder(new OrderItem("update_time",false));
}
return page;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@ApiModel("通用查询结果实体类")
public class QueryDTO<T> {
@ApiModelProperty("数据总数")
private Long total;
@ApiModelProperty("页面总数")
private Long pages;
@ApiModelProperty("当前页面数据")
private List<T> data;

public static <PO,VO> QueryDTO<VO> toPageDTO(Page<PO> pages,Class<VO> clazz){
QueryDTO<VO> userVOPageDTO = new QueryDTO<>();

List<VO> userVOS = BeanUtil.copyToList(pages.getRecords(),clazz);
userVOPageDTO.setTotal(pages.getTotal());
userVOPageDTO.setPages(pages.getPages());
userVOPageDTO.setData(userVOS);
return userVOPageDTO;
}
}

service层方法改写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public QueryDTO<UserVO> queryUsersByPage(UserQuery query) {

String name = query.getName();
Integer status = query.getStatus();
//1.1构造分页查询条件
Page<User> page = query.toMybatisPage();
//1.2构造条件查询
Page<User> pages = lambdaQuery().like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);

//2.构造结构实体实例
return QueryDTO.toPageDTO(pages, UserVO.class);
}