Mybatis Plus 深入使用及代码生成器(新)

HYF Lv3

之前那篇关于代码生成器的博客的只适用于 mybatis-plus-generator 3.5.1 以下版本,本文将讲解新的代码生成器的使用及 MP(Mybatis Plus , 下文通称 MP) 的深入使用 。

关于引入依赖,本文不再赘述,有需要请翻阅以往博客,或前往官网:MP安装

表结构

User 表及 User_id_card 表:

表结构

代码生成器

##代码生成器启动前:
Before Generator

代码生成器实现:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.youfeng.MybatisPlus.utils;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Column;

import java.sql.Types;
import java.util.Collections;

/**
* @author -侑枫
* @date 2023/9/5 22:27:26
*/
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://47.120.5.13:3306/blog" +
"?useUnicode=true&characterEncoding=utf-8" +
"&useSSL=true&serverTimezone=Asia/Shanghai",
"<your database username>", "<your database password>")
// 全局配置
.globalConfig(builder -> {
builder.author("侑枫")
// 代码生成后不打开文件夹
.disableOpenDir()
// 时间日期格式,默认为 "yyyy-MM-dd"
.commentDate("yyyy-MM-dd HH:mm:ss")
// 文件生成位置
.outputDir(System.getProperty("user.dir") + "\\src\\main\\java");
})
// 数据库配置
.dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
int typeCode = metaInfo.getJdbcType().TYPE_CODE;
if (typeCode == Types.SMALLINT) {
// 自定义类型转换
return DbColumnType.INTEGER;
}
return typeRegistry.getColumnType(metaInfo);
}))
// 包配置
.packageConfig(builder -> {
// 包路径
builder.parent("com.youfeng.MybatisPlus")
// entity 命名(controller/service/mapper 一样适用)
.entity("pojo")
// 路径配置信息
.pathInfo(Collections.singletonMap(OutputFile.xml,
System.getProperty("user.dir") + "\\src\\main\\resources\\com\\youfeng\\MybatisPlus"));
})
// 策略配置
.strategyConfig(builder -> {
// 精准添加生成代码的表(也可以进行模糊匹配 like/模糊排除表 noLike/精准排除 addExclude)
builder.addInclude("user", "user_id_card")
// 过滤表前缀
// .addTablePrefix("table_")
// 过滤表后缀
// .addTableSuffix("_end")
// 实体类配置
.entityBuilder()
// 开启 lombok 生成 @Getter@Setter
.enableLombok()
// 字段注解
.enableTableFieldAnnotation()
// 雪花算法
.idType(IdType.ASSIGN_ID)
// 表字段自动填充
.addTableFills(new Column("created_time", FieldFill.INSERT))
.addTableFills(new Column("modified_time", FieldFill.INSERT_UPDATE))
.build()
// 控制器配置
.controllerBuilder()
.enableRestStyle()
.mapperBuilder()
// 开启 @Mapper ,建议包扫描模式
// .enableMapperAnnotation()
.build();

})
// 使用默认 Velocity 引擎模板生成代码
.templateEngine(new VelocityTemplateEngine())
.execute();
}
}

该代码为可执行示例代码,具体生成器可依照需求参考官网:代码生成器配置新

执行结果部分示例

目录:

目录

Controller:

Controller

基于RESTful API 风格思想,我们将资源改为复数形式(即将user改为users)

Mapper:

Mapper

记得在 Mapper 层加上 @Mapper 或在 Application.java 加上 @MapperScan("这里填入mapper接口的路径")

Pojo:

Pojo (部分)

Service:

Service

ServiceImpl:

ServiceImpl

完善 application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.120.5.13:3306/blog?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: "<your database username>"
password: "<your database password>"
main:
banner-mode: off
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
global-config:
banner: false
type-aliases-package: com.youfeng.MybatisPlus.pojo
logging:
level:
root: error
server:
port: 221

config 包下创建 MyMetaObjectHandler 完善 insertFill 以及 updateFill

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.youfeng.MybatisPlus.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
* @author -侑枫
* @date 2023/9/5 14:06:04
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createdTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("modifiedTime", LocalDateTime.now(), metaObject);
}

@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("modifiedTime", LocalDateTime.now(), metaObject);
}
}

深入使用

IService

在使用之前,我们先来看一下 MP 的通用接口
通用接口为IService,默认实现为ServiceImpl,其中封装的方法明细如下:

  • save:新增
    • save是新增单个元素
    • saveBatch是批量新增
    • saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
    • saveOrUpdateBatch是批量的新增或修改
  • remove:删除
    • removeById:根据id删除
    • removeByIds:根据id批量删除
    • removeByMap:根据Map中的键值对为条件删除
    • remove(Wrapper<T>):根据Wrapper条件删除
    • removeBatchByIds:根据ids批量删除
  • update:更新
    • updateById:根据id修改
    • update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含setwhere部分
    • update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据
    • updateBatchById:根据id批量修改
  • get:查询单个结果
    • getById:根据id查询1条数据
    • getOne(Wrapper<T>):根据Wrapper查询1条数据
    • getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper
  • list:查询集合结果
    • listByIds:根据id批量查询
    • list(Wrapper<T>):根据Wrapper条件查询多条数据
    • list():查询所有
  • count:计数
    • count():统计所有数量
    • count(Wrapper<T>):统计符合Wrapper条件的数据数量
  • page:分页查询
  • getBaseMapper: 获取service对应的Mapper

LambdaQueryWrapper 及 LambdaUpdateWrapper

QueryWrapperUpdateWrapper 大家用的比较多,故不再赘述。
LambdaQueryWrapper / LambdaUpdateWrapper 相比 QueryWrapper / UpdateWrapper 的改进主要在于使用 Lambda 表达式来构建查询条件, 这样做的好处有几点:

  • Lambda 表达式可以在编译时进行类型检查,相比字符串传入字段名,Lambda 表达式可以更好地避免在运行时出现拼写错误或者字段名错误的情况。
  • 使用 Lambda 表达式可以使代码更加简洁清晰,不需要像传入字符串那样手动拼接 SQL 条件,提高了代码的可读性和可维护性
  • LambdaQueryWrapper / LambdaUpdateWrapper 是对 QueryWrapper / UpdateWrapper 的改进版本,它们在向下兼容的同时保留了原有的特性,包括对条件的灵活构建和链式编程的支持。
    接下来我们用下列案例来快速学习一下如何使用 LambdaQueryWrapper / LambdaUpdateWrapper

例如我们现在有这么两个需求:
需求1(查找):如果用户登录名(字段:username)不为空,则对用户登录名进行模糊匹配;如果状态(字段:status)不为空,则对状态进行精准匹配,且注册(创建)时间在 2023-09-03 16:22:50 到两天后之间

需求2(修改): 将状态(字段:status)为冻结(值:1),且注册(创建)时间为2023-05-20或注册时间为2024-05-20的用户的密码(字段:password)修改为123456,状态修改为正常(值:1
(此处代码仅展示 serviceImpl 层)

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
/**
* MP QueryWrapper 需求1使用演示
*
* @param user
* @return 结果集合
*/
@Override
public List<User> selectVague(User user) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like(ObjectUtils.isNotEmpty(user.getUsername()), "user_name", user.getUsername())
.eq(ObjectUtils.isNotEmpty(user.getStatus()), "status", user.getStatus())
.between("create_time",
LocalDateTime.of(2023, 9, 3, 16, 22, 50),
LocalDateTime.of(2023, 9, 5, 16, 22, 50));
return baseMapper.selectList(queryWrapper);
}

/**
* MP UpdateWrapper 需求2使用演示
*
* @param user
* @return
*/
@Override
public void updateUser(User user) {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("status", 1)
.and(i -> i.eq("create_time", LocalDate.of(2023,5,20))
.or().eq("create_time", LocalDate.of(2024,5,20)))
.set("password", 123456)
.set("status", 0);
update(updateWrapper);
}
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
/**
* MP LambdaUpdateWrapper 需求1使用演示
*
* @param user
* @return 结果集合
*/
@Override
public List<User> selectVague(User user) {
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.like(ObjectUtils.isNotEmpty(user.getUsername()), User::getUsername, user.getUsername())
.eq(ObjectUtils.isNotEmpty(user.getStatus()), User::getStatus, user.getStatus())
.between(User::getCreatedTime,
LocalDateTime.of(2023, 9, 3, 16, 22, 50),
LocalDateTime.of(2023, 9, 5, 16, 22, 50));
return baseMapper.selectList(queryWrapper);
}

/**
* MP LambdaUpdateWrapper 需求2使用演示
*
* @param user
* @return
*/
@Override
public void updateUser(User user) {
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.eq(User::getStatus, 1)
.and(i -> i.eq(User::getCreatedTime, LocalDate.of(2023,5,20))
.or().eq(User::getCreatedTime, LocalDate.of(2024,5,20)))
.set(User::getPassword, 123456)
.set(User::getStatus, 0);
update(lambdaUpdateWrapper);
}

通过对比,我们可以发现,使用 Lambda 表达式构建查询条件,使得代码更为简洁,减少了代码字面量,降低了出错的可能性,同时提高了可读性和可维护性。

需要注意的是,在使用 LambdaWrapper 时,需指定类型参数,例如代码中指定该 LambdaWrapperUser 类型;以及 User::getUsername 中的 :: 运算符用于引用方法或构造函数。这种方法引用的方式可以在Lambda表达式中传递方法,而不必显式地编写方法的参数列表和实现体。

LambdaQuery() 及 LambdaUpdate() 及静态工具 Db

Service中对LambdaQueryWrapperLambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuerylambdaUpdate方法。
有的时候Service之间也会相互调用,为了避免出现循环依赖问题,Mybatis Plus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能。

简单使用

我们还是用上述相同案例,通过对比LambdaWrapper来快速学习一下如何简单使用 LambdaQuery() / LambdaUpdate()

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
/**
* MP LambdaQueryWrapper 需求1使用演示
*
* @param user
* @return 结果集合
*/
@Override
public List<User> selectVague(User user) {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(ObjectUtils.isNotEmpty(user.getUsername()), User::getUsername, user.getUsername())
.eq(ObjectUtils.isNotEmpty(user.getStatus()), User::getStatus, user.getStatus())
.between(User::getCreatedTime,
LocalDateTime.of(2023, 9, 3, 16, 22, 50),
LocalDateTime.of(2023, 9, 5, 16, 22, 50));
return baseMapper.selectList(lambdaQueryWrapper);
}

/**
* MP LambdaUpdateWrapper 需求2使用演示
*
* @param user
* @return
*/
@Override
public void updateUser(User user) {
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.eq(User::getStatus, 1)
.and(i -> i.eq(User::getCreatedTime, LocalDate.of(2023,5,20))
.or().eq(User::getCreatedTime, LocalDate.of(2024,5,20)))
.set(User::getPassword, 123456)
.set(User::getStatus, 0);
update(lambdaUpdateWrapper);
}
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* MP lambda 、Db 需求1使用演示
*
* @param user
* @return 结果集合
*/
@Override
public List<User> selectVague(User user) {
// IService 的 lambdaQuery.list()
return lambdaQuery()
.like(ObjectUtils.isNotEmpty(user.getUsername()), User::getUsername, user.getUsername())
.eq(ObjectUtils.isNotEmpty(user.getStatus()), User::getStatus, user.getStatus())
.between(User::getCreatedTime,
LocalDateTime.of(2023, 9, 3, 16, 22, 50),
LocalDateTime.of(2023, 9, 5, 16, 22, 50))
.list();
// Db 的 lambdaQuery.list()
return Db.lambdaQuery(User.class).
like(ObjectUtils.isNotEmpty(user.getUsername()), User::getUsername, user.getUsername())
.eq(ObjectUtils.isNotEmpty(user.getStatus()), User::getStatus, user.getStatus())
.between(User::getCreatedTime,
LocalDateTime.of(2023, 9, 3, 16, 22, 50),
LocalDateTime.of(2023, 9, 5, 16, 22, 50))
.list();
}

/**
* MP lambda 、Db 需求2使用演示
*
* @param user
* @return
*/
@Override
public void updateUser(User user) {
// IService 的 lambdaUpdate.update()
lambdaUpdate().eq(User::getStatus, 1)
.and(i -> i.eq(User::getCreatedTime, LocalDate.of(2023,5,20))
.or().eq(User::getCreatedTime, LocalDate.of(2024,5,20)))
.set(User::getPassword, 123456)
.set(User::getStatus, 0)
.update();
// Db 的 lambdaUpdate.update()
Db.lambdaUpdate(User.class)
.eq(User::getStatus, 1)
.and(i -> i.eq(User::getCreatedTime, LocalDate.of(2023,5,20))
.or().eq(User::getCreatedTime, LocalDate.of(2024,5,20)))
.set(User::getPassword, 123456)
.set(User::getStatus, 0)
.update();
}


// 下面是部分其他简单使用

/**
* 调用 IService 的 save 方法以及静态根据 Db 的 save 方法
* 仅为演示作用,无实际业务逻辑
*
* @return 示例结果
*/
@Override
public boolean insertAny() {
User user = new User();
user.setUsername("张三");
user.setPassword("123");
user.setId("1000000000000000001");
boolean saveUser = save(user);
UserIdCard userIdCard = new UserIdCard();
userIdCard.setUserId(user.getId());
userIdCard.setNumber("99887766");
return saveUser && Db.save(userIdCard);
}

/**
* MP lambda 使用演示
* 调用 IService 的 remove 方法以及静态工具 Db 的 remove 方法修改其他表
* 仅为演示作用,无实际业务逻辑
*
* @return 示例结果
*/
@Override
public boolean deleteById() {
// IService 的 lambda.remove()
boolean removeUser = lambdaUpdate()
.eq(User::getId, 1000000000000000001L)
.remove();
// Db 的 lambda.remove()
boolean removeUserIdCard = Db.lambdaUpdate(UserIdCard.class)
.eq(UserIdCard::getUserId, 1000000000000000001L)
.remove();
return removeUser && removeUserIdCard;
}

/**
* MP lambda 使用演示
* 调用 IService 的 update 方法以及静态工具 Db 的 update 方法修改其他表
* 仅为演示作用,无实际业务逻辑
*
* @return 示例结果
*/
@Override
public boolean updatePassword() {
boolean updateUser = lambdaUpdate()
.set(User::getPassword, "321")
.eq(User::getId, 1000000000000000001L)
.update();
boolean updateUserIdCard = Db.lambdaUpdate(UserIdCard.class)
.set(UserIdCard::getNumber, "66778899")
.eq(UserIdCard::getUserId, 1000000000000000001L)
.update();
return updateUser && updateUserIdCard;
}
Tips
  • lambdaQuery()中的链式编程的最后一个方法决定最终的返回结果,可选的方法有:
    • .one():最多1个结果
    • .list():返回集合结果
    • .count():返回计数结果
  • lambdaUpdate中的链式编程的最后一个方法应为update()
  • LambdaQueryLambdaUpdate 部分常用条件构造方法:
    • eq:等于
    • ne:不等于
    • gt:大于
    • ge:大于等于
    • lt:小于
    • le:小于等于
    • or: 或逻辑
    • apply: 直接拼接 SQL 语句
    • and: 与逻辑
    • like:模糊匹配
    • likeLeft:左模糊匹配
    • likeRight:右模糊匹配
    • in:包含于(in适合于外表大而内表子查询小的情况,exists适合于外表小而内表子查询大的情况)
    • exists: 存在(当in的参数不为已知数值类别时,建议使用exist效率更高)
    • notIn:不包含于
    • isNull:字段为 null
    • isNotNull:字段不为 null
    • last:拼接在 SQL 的最后,用于自定义 SQL 片段
    • ……(还有很多,更多方法请查找官方文档)
  • LambdaQuery 专用方法:
    • groupBy: 分组
    • having:过滤分组的结果
    • order: 排序
    • orderByAsc:升序排序
    • orderByDesc:降序排序
    • distinct:去重
    • ……
  • LambdaUpdate 专用方法:
    • set 赋值
    • setIncrBy(3.5.6版本新特性): 指定字段新增指定数值
    • setDecrBy(3.5.6版本新特性): 指定字段减少指定数值
    • ……

selectColumn 进阶使用(只查询部分字段)

众所周知,MySQL中进行全表查询和仅查询部分字段的效率存在差异。全表查询(SELECT * FROM table_name)意味着MySQL需要检索并返回表中的所有列,这可能导致数据传输量大和查询时间长,特别是在大型表中。相比之下,仅查询部分字段(例如SELECT column1, column2 FROM table_name)时,MySQL只需检索和返回指定的列,从而减少了数据传输量和查询时间,因为MySQL无需检索不需要的列数据。因此,仅查询部分字段通常比全表查询更有效率,特别是当表中包含大量列或某些列的数据量较大时。
另外,许多业务实际上并不需要查询所有字段,有时只需要特定的字段,但却检索出一大堆数据,这不仅影响效率,还显得不够优雅。而在 MP 中,想要查找部分字段,仅需简单的一个select方法就可以解决。例如下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* MP selectColumn 进阶使用
*
* @param id
* @return
*/
@Override
public User queryAnyColumnById(String id) {
// 仅查找一个username字段
return lambdaQuery()
.eq(User::getId, id)
.select(User::getUsername)
.one();
// 也可以查找多个字段
return Db.lambdaQuery(User.class)
.eq(User::getId, id)
.select(User::getUsername, User::getStatus, User::getInfo)
.one();
}

selectFunc 进阶使用(SQL函数)

在实际业务中,我们经常需要使用 GROUP BYSUM 函数来进行统计和汇总计算。然而,在目前的 MP(SINCE 3.5.6) 中,除了.count()构造方法外,它暂时没有提供直接支持 SQL 函数的构造方法。那么,我们应该如何在 MP 中实现对 SQL 函数(selectFunc)的使用呢?
接下来我们通过几个案例来学习一下如何使用 selectFunc

需求1:统计注册(创建)时间在2023-5-202024-5-20之间状态(字段:status)为正常(值:1)的用户的考核分(字段:score)的总合。

需求2:统计注册(创建)时间在2023-5-202024-5-20之间状态(字段:status)为正常(值:1)的用户的考核分(字段:score)的平均值。

需求3: 查找注册(创建)时间在2023-5-202024-5-20之间状态(字段:status)为正常(值:1)的用户的考核分(字段:score)的最大值和最小值。

首先在 user 实体类中加以下字段 sumScore,aveScore,maxScore,minScore,并使用 @TableField 注解来指定相应的聚合函数表达式,并将 select 属性设置为 false,告诉 MP 在默认查询时忽略该字段,这是我们自定义的聚合运算,目的是作为辅助属性来使用。

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
// User.java 部分代码:
......

/**
* 考核分
*/
@TableField("score")
private Integer score;

/**
* 考核分总合
*/
@TableField(value = "SUM(score)", select = false)
private Integer sumScore;

/**
* 考核分平均值
*/
@TableField(value = "AVG(score)", select = false)
private Integer aveScore;

/**
* 考核分最大值
*/
@TableField(value = "MAX(score)", select = false)
private Integer maxScore;

/**
* 考核分最小值
*/
@TableField(value = "MIN(score)", select = false)
private Integer minScore;

/**
* 用户信息
*/
@TableField("info")
private String info;

......

接下来,我们要通过 lambdaQuery() 依次调用 betweeneqgroupBy方法来构建查询条件,使用 select 方法来使用我们的聚合函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 一行语句完成需求1~3
*
* @return SumScore: 考核分总合;AveScore: 考核分平均分;MaxScore: 考核分最大值;MinScore: 考核分最小值
*/
public User queryFuncScore () {
return lambdaQuery()
.between(User::getCreatedTime,
LocalDate.of(2023, 5, 20),
LocalDate.of(2024, 5, 20))
.eq(User::getStatus, "1")
.select(User::getSumScore, User::getAveScore, User::getMaxScore, User::getMinScore)
.groupBy(User::getStatus)
.having("status = 1")
.one();
}

通过这种方式,我们可以完成我们所需要的selectFunc查询操作。

setIncrBy/setDecrBy 进阶使用(自增自减)

在我们的一些业务中,经常需要对某些字段进行复杂的自增或自减操作。在过去的 MP 版本中,要实现这样的操作,通常需要使用 setSql 方法来指定更新的字段拼接相应的 SQL 表达式来实现自增或自减操作。或者,我们可能需要先查询字段的当前值,然后根据需要增加或减少特定数量。然而,在 MP 3.5.6 版本中,新增了两个非常方便的特性:setIncrBy(自增)和 setDecrBy(自减)。接下来,让我们通过对比来学习如何使用这两个新特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 根据userId为考核分score自增increment
*
* @param userId
* @param increment
*/
@Override
public void increaseScore(String userId, int increment) {
lambdaUpdate().eq(User::getId, userId)
.setSql("`score` = `score` + " + increment)
.update();
}
/**
* 根据userId为考核分score自减decrement
*
* @param userId
* @param decrement
*/
@Override
public void decreaseScore(Long userId, int decrement) {
lambdaUpdate().eq(User::getId, userId)
.setSql("`score` = `score` - " + decrement)
.update();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 根据userId为考核分score自增increment
*
* @param userId
* @param increment
*/
@Override
public void increaseScore(String userId, int increment) {
lambdaUpdate().eq(User::getId, userId)
.setIncrBy(User::getScore, increment)
.update();
}
/**
* 根据userId为考核分score自减decrement
*
* @param userId
* @param decrement
*/
@Override
public void decreaseScore(Long userId, int decrement) {
lambdaUpdate().eq(User::getId, userId)
.setDecrBy(User::getScore, decrement)
.update();
}

枚举类字段处理器

在我们的User表中,有一个表示状态的字段 status,其取值仅限于0(表示正常)和1(表示冻结)。通常情况下,针对这种具有多个固定取值的字段,我们会定义一个枚举类型,这样在进行业务判断时可以直接基于枚举进行比较。然而,我们的数据库采用的是int类型来存储这个字段,对应的持久化对象也是Integer类型。因此,在进行业务操作时,我们必须手动进行枚举与整数之间的转换,这一过程相当繁琐。
MP 提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。

首先我们需要定义枚举,在 enums 包下创建一个UserStatus 枚举类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author -侑枫
* @date 2023/9/5 14:29:36
*/
@Getter
public enum UserStatus {
/**
* 正常状态
*/
NORMAL((byte)0,"正常"),
/**
* 冻结状态
*/
FREEZE((byte)1,"冻结");

private final byte status;
private final String desc;

UserStatus(byte status, String desc) {
this.status = status;
this.desc = desc;
}
}
接下来将修改 User 实体类中 status 的类型为 UserStatus 类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// User.java 部分代码:
......
/**
* 状态(0:正常/1:冻结)
*/
@TableField("status")
private String status;

/**
* 用户信息
*/
@TableField("info")
private String info;
......
要让MP处理枚举与数据库类型自动转换,我们必须告诉MP,枚举中的哪个字段的值作为数据库值。使用MP提供了@EnumValue注解来标记枚举属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author -侑枫
* @date 2023/9/5 14:29:36
*/
@Getter
public enum UserStatus {
/**
* 正常状态
*/
NORMAL((byte)0,"正常"),
/**
* 冻结状态
*/
FREEZE((byte)1,"冻结");
@EnumValue
private final byte status;
@JsonValue
private final String desc;

UserStatus(byte status, String desc) {
this.status = status;
this.desc = desc;
}
}
最后,我们需要在 application.yaml 中配置一下枚举处理器
1
2
3
4
5
6
7
8
9
10
11
# 部分 applycation.yaml 配置
......
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
map-underscore-to-camel-case: true
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
banner: false
type-aliases-package: com.youfeng.MybatisPlus.pojo
......
至此,我们的枚举类字段处理器配置完成了,接下来我们来测试一下是否成功。
首先看一下我们的数据库当前的数据:

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
/**
* <p>
* 前端控制器
* </p>
*
* @author 侑枫
* @since 2023-09-05 13:53:42
*/
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;

@GetMapping
public List<User> getList() {
List<User> list = userService.lambdaQuery().list();
list.forEach(System.out::println);
return list;
}

@PostMapping
public void saveUser() {
User user = new User();
user.setUsername("wangwu");
user.setPassword("123");
user.setStatus(UserStatus.NORMAL);
userService.save(user);
}
}
使用 PostMan 调用查询接口,并在返回结果前打断点得到数据:

查询结果

可以发现,我们查询出的User类的status字段会是枚举类型。
接下来继续使用 PostMan 测试一下保存接口:

保存

查询数据库:

保存结果

可以看到枚举类处理器成功实现了我们的查询和插入。

JSON 字段类型处理器

数据库的user表中的info字段,是JSON类型(MariaDB 数据库无 JSON 类型字段,为longtext)。一般 JSON格式类似这样:

1
{"age": 24, "sex": "male", "address": "xiamen"}

但实体类中的 infoString类型。这样一来,我们要读取info中的属性时就非常不方便。而MP提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。下面我们来实现一下这个 JSON 字段类型处理器。

首先定义一个与 info字段与属性匹配的实体类:
1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author -侑枫
* @date 2023/9/5 14:36:14
*/
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String sex;
private String address;
}
接下来,将 User 类的 info 字段修改为 UserInfo 类型,并声明为 JSON 类型处理器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@TableName(value = "user", autoResultMap = true)
public class User implements Serializable {

private static final long serialVersionUID = 1L;

// 省略其余源码
......

/**
* 状态(0:正常/1:冻结)
*/
@TableField("status")
private UserStatus status;

/**
* 用户信息
*/
@TableField(value = "info",typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}

请注意,为了实现对象查询的嵌套,我们必须采用 ResultMap。因此,在User实体类中,需要在@TableName注解中添加 autoResultMap = true 参数。

到这里,JSON 字段类型处理器已经配置完成了,我们来测试一下是否配置成功。
我们在 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
/**
* <p>
* 前端控制器
* </p>
*
* @author 侑枫
* @since 2023-09-05 13:53:42
*/
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;

@PostMapping
public void saveUser() {
User user = new User();
user.setUsername("zhaoliu");
user.setPassword("123");
user.setStatus(UserStatus.NORMAL);
UserInfo userInfo = new UserInfo();
userInfo.setAge(24);
userInfo.setSex("male");
userInfo.setAddress("xiamen");
userService.save(user);
}

@GetMapping("/{id}")
public User getUserById(@PathVariable String id) {
User user = userService.lambdaQuery()
.eq(User::getId, id)
.one();
System.out.println(user);
return user;
}
}
当前数据库表:

保存结果

我们依旧使用 PostMan,先来请求一下保存接口:

调用接口

数据库查询结果:

保存结果

接下来继续使用 PostMan 测试一下查询接口:

查询结果

可以看到 JSON 处理器成功实现了我们的查询和插入。

配置加密

在我们的配置文件中,很多参数都是以明文形式存储的,这可能会导致敏感信息的泄露问题,MP 中提供了配置文件加密和解密的功能。

我们以数据库的参数为例来演示如何使用:

首先使用 AESgenerateRandomKey()encrypt() 方法分别获取密钥和参数对应的密文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ApplicationTests {
@Test
void encrypt() {
// 生成 16 位随机 AES 密钥
String randomKey = AES.generateRandomKey();
System.out.println("randomKey = " + randomKey);

// 利用密钥对url加密
String url = AES.encrypt("<your url>", randomKey);
System.out.println("url = " + username);

// 利用密钥对用户名加密
String username = AES.encrypt("<your username>", randomKey);
System.out.println("username = " + username);

// 利用密钥对密码加密
String password = AES.encrypt("<your password>", randomKey);
System.out.println("password = " + password);
}
}
将用户和密码输入后执行,可以得到密钥和加密过后的用户名密码,例如:
1
2
3
4
randomKey = 4576******dfabca
url = j2H2dMKcpKzGWyaCNDCzGWSskCFAcLhasUBgjOGxKTc1/Pvr+HPCk3uN+7Gz2aqMDXX4uKlqdcsy6Xi2vwwAuP6SZb8WiSf4mXAv3bAiqWKo1AebEaUlFYPwPobM3Mc7ZEcIk2Z32Vx12cuJx+ZrJNMvKF2HI+9/3rXZlATLWH8=
username = GmfwxaA8LojLrjsvC7g+qw==
password = qa2K2V/toVHF4A41N+2chg==

接下来修改 application.yaml 文件,把对应的属性修改为刚刚加密生成的密文:

1
2
3
4
5
6
spring:
datasource:
url: mpw:j2H2dMKcpKzGWyaCNDCzGWSskCFAcLhasUBgjOGxKTc1/Pvr+HPCk3uN+7Gz2aqMDXX4uKlqdcsy6Xi2vwwAuP6SZb8WiSf4mXAv3bAiqWKo1AebEaUlFYPwPobM3Mc7ZEcIk2Z32Vx12cuJx+ZrJNMvKF2HI+9/3rXZlATLWH8=
driver-class-name: com.mysql.cj.jdbc.Driver
username: mpw:GmfwxaA8LojLrjsvC7g+qw==
password: mpw:qa2K2V/toVHF4A41N+2chg==

请注意:密文需要以 mpw:开头

最后,在项目启动时,把刚才生成的秘钥添加到启动参数中,例如:

1
--mpw.key=4576******dfabca

添加启动参数


END

  • 标题: Mybatis Plus 深入使用及代码生成器(新)
  • 作者: HYF
  • 创建于 : 2024-05-21 23:26:32
  • 更新于 : 2024-07-27 21:21:51
  • 链接: https://youfeng.ink/mybatis-plus-a6852d4f0389/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。