黑马商城笔记

MybatisPlus

对于 Mybatis 无侵入
引入依赖后,定义 Mapper 接口后继承 BaseMapper<User> 就能自动获得 CRUD 方法
通过反射获取实体类信息,作为数据库表信息

MP 基础功能

配置

不遵循约定时要自己配置信息

  • 可以在类上用注解
    @TableName(“user”): 指定表名
    @TableId(“id”): 指定主键
    @TableField(“username”): 指定字段名

  • 也可以在 yml 文件中添加配置

条件构造器

MP 支持复杂 where 条件

【例子】
查询出名字中带 o 的,存款大于 2000 的用户的 id,username,info,balance

1
2
3
4
5
6
7
8
9
void testQueryWrapper() {
// 1.构造查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 2000)
// 2.执行查询,将构造的条件传入查询方法
List<User> users = userMapper.selectList(queryWrapper);
}

将其金额更新为 1000

1
2
3
4
5
6
7
8
9
10
void testUpdateWrapperByQueryWrapper() {
// 1.将要更新的数据
User user = new User();
user.setBalance(1000);
// 2.更新的条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>()
.eq("username", "jack");
// 3.执行更新
userMapper.update(user, queryWrapper);
}

将 id 为 1、2、4 的用户金额扣两百
用 setSql 可以直接手写 sql 语句

1
2
3
4
5
6
void testUpdateWrapper() {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>()
.setSql("balance = balance - 200");
.in("id", 1, 2, 4);
userMapper.update(null, updateWrapper);
}

Lambda 语法动态构造查询条件

1
2
3
4
5
6
7
8
9
10
void testLambdaQueryWrapper() {
// 1.构造查询条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>()
// 传入实体类的getter方法,动态获取字段名
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 2000)
// 2.执行查询,将构造的条件传入查询方法
List<User> users = userMapper.selectList(queryWrapper);
}

自定义 SQL

利用 MybatisPlus 提供的 Wrapper 来构造复杂的 where 条件,然后自己定义 SQL 剩下的部分

【例子】
将 id 为 1、2、4 的用户金额扣两百

  1. 在业务层基于 Wrapper 构建 where 条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void testCustomSqlUpdate() {
    // 1.更新条件
    List<Long> ids = List.of(1L, 2L, 4L);
    int amount = 200;
    // 2.定义条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<>().in("id", ids);
    // 3.调用自定义SQL方法
    userMapper.updateBalance(queryWrapper, amount);
    }
  2. 在 Mapper 中方法参数中用 Param 注解声明 wrapper 变量名称,必须是 ew

    1
    2
    3
    4
    5
    public interface UserMapper extends BaseMapper<User> {
    List<User> queryUserByIds(@Param("ids") List<Long> ids);

    void updateBalanceByIds(@Param("ew") QueryWrapper<User> queryWrapper, @Param("amount") int amount);
    }
  3. 在 xml 中自定义 SQL 方法,并使用 wrapper 条件

    1
    2
    3
    4
    后面的where条件会被ew.getSqlSegment()替换掉
    <update id="updateBalanceByIds">
    UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

MP 核心功能

Service 接口

Service 接口继承 IService 接口,实现类继承 ServiceImpl 类
然后就有了很多常用的 CRUD 方法,可以直接用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// IService接口是MybatisPlus提供的,里面有基础增删改查的方法
public interface IUserService extends IService<User> {

}

// 实现类,MP提供了实现了IService接口内方法的类,继承后就能直接用了
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

}

// 测试类
class UserServiceTest {
void testSaveUser() {
User user = new User();
user.setUsername("jack");
user.setInfo("hello world");

userService.save(user);
}
}

IService 的 lambda 方法

IService 接口提供了 lambda 方法,可以更方便使用
比如 lambdaUpdate()、lambdaQuery()

更新操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void deductBalance(Long id, Integer amount) {
// 1.查询用户
User user = getById(id);
// 2.检验用户状态,反向校验
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常")
}
// 3.检验是否充足
if (user.getBalance() < amount) {
throw new RuntimeException("用户余额不足")
}
// 4.扣减余额
int remainBalance = user.getBalance() - amount;
lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)// 如果余额为0,则将用户状态改为2
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance())// 判断是否被并发修改,如果刚才查询的余额和现在查询的余额不一致,则说明被并发修改了,则返回null,乐观锁
.update();
}

查询操作

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

批量插入设置(批处理)

  • 假设插入十万条数据,一条一条插会很慢
  • 可以每一千条组成一个 list 批量插入,IService 的批量插入 userService.saveBatch(list)
  • 但是 MySql 执行的语句还是一条一条的 sql,将 yaml 文件中 jdbc 参数rewriteBatchedStatements设置为 true,MySql 就能将一组 sql 变成一条 sql

MP 拓展功能

代码生成

MybatisX 或 MybatisPlus 插件都可以生成代码
MybatisPlus 的代码生成器在工具栏里面的 code generation

静态工具类

同时查两个表,Service 之间相互调用会出现循环依赖
这时候用 Db 静态工具类,可以避免,它的方法和 Service 一样,只是需要多传一个字节码

1
2
3
4
// 假如插User的时候要把地址也查出来
Db.lambdaUpdate(Address.class)
.eq(Address::getUserId, id)
.list()

逻辑删除

在表中添加一个字段标记数据是否被删除
逻辑删除的语句UPDATE address SET deleted = 1 WHERE id = ? AND deleted = 0
MP 提供了逻辑删除功能,无需改变方法调用的方式,只需在 yaml 文件中配置即可

1
2
3
4
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 配置逻辑删除字段

逻辑删除有自己的问题,比如垃圾数据越来越多,每次都要判断,影响效率
所以其实不推荐用逻辑删除,如果数据不能删可以把它迁移到其他表

枚举处理器

如果用 01 等直接代表状态,可能一多就容易混淆
用枚举类的字段来对应 01 状态就更加清晰
但最好实体类的字段类型直接写成枚举类,转换交给 MP 来做,增强可读性

枚举类写入数据库时转成 int 值时是由 MP 的枚举类型转换器来转换的

枚举类型转换器配置

1
2
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler

定义枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum UserStatus {
NORMAL(1, "正常"),
FROZEN(2, "冻结"),
;

@EnumValue // 枚举值写入数据库时,会写入这个注解的值
private final int value;
@JsonValue // 标记返回谁的值,加在desc字段上就返回desc字段的值,默认返回枚举名,就是这里的NORMAL
private final String desc;

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

JSON 处理器

将 JSON 类型的字段转成对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 给info封装成一个类
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}

// User实体类,对info字段进行处理
@Data
@TableName(value = "user", autoResultMap = true)// 自动结果集映射
public class User {

private String username;
private String phone;
// 其他的字段略去

// 指定JSON类型处理器,这里使用Jackson
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}

// setter设置属性,可以直接传入对象而不是字符串了
user.setInfo(new UserInfo(18, "hello world", "男"));// 直接new对象
user.setInfo(UserInfo.of(18, "hello world", "男"));// of方法,静态工厂创建对象

分页插件

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

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

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

接着就可以使用分页的 API 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void testPageQuery() {
// 1.查询
int pageNo = 1, pageSize = 10;
// 1.1.分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 1.2.排序参数,通过OrderItem来指定
page.addOrder(new OrderItem("balance", true));// 后面指定是否升序
page.addOrder(new OrderItem("id", 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 = p.getRecords();
records.forEach(System.out::println)
}

通用分页实体

定义一个实体类存放分页的参数,其他类只需要继承这个类就行,不用每次都写分页参数了

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
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty(value = "页码")
private Integer pageNo;
@ApiModelProperty(value = "页大小")
private Integer pageSize;
@ApiModelProperty(value = "排序字段")
private String sortBy;
@ApiModelProperty(value = "是否升序")
private Boolean isAsc;
}

// 继承分页查询实体
@Data
@ApiModel(description = "用户查询条件实体")
class UserQuery extends PageQuery {
@ApiModelProperty(value = "用户名")
private String username;
// 其他字段略去
}

// DTO存放分页结果
@Data
@ApiModel(description = "分页查询结果")
public class PageDTO<T> {
@ApiModelProperty(value = "总条数")
private Long total;
@ApiModelProperty(value = "总页数")
private Long pages;
@ApiModelProperty(value = "分页数据")
private List<T> records;
}

每次分页都要写的逻辑可以直接写在分页查询实体里

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
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty(value = "页码")
private Integer pageNo = 1;
@ApiModelProperty(value = "页大小")
private Integer pageSize = 5;
@ApiModelProperty(value = "排序字段")
private String sortBy;
@ApiModelProperty(value = "是否升序")
private Boolean isAsc = true;

public <T> Page<T> toMpPage(OrderItem ... items) {// 可变参数
// 1.分页条件
Page<T> page = new Page<>(pageNo, pageSize);
// 2.排序条件
if (StrUtil.isNotBlank(sortBy)) {
// 不为空
page.addOrder(new OrderItem(sortBy, isAsc));
} else if (items != null) {
// 为空,默认参数
page.addOrder(items);
}
return page;
}

public <T> page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {
return toMpPage(defaultSortBy, defaultAsc);
}
// 默认按创建时间排序
public <T> Page<T> toMpPageDefaultSortByCreateTime() {
return toMpPage(new OrderItem("create_time", false));
}
public <T> Page<T> toMpPageDefaultSortByUpdateTime() {
return toMpPage(new OrderItem("update_time", false));
}
}

每次把查询结果封装成 DTO 的逻辑也能直接写在 DTO 类里

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
@Data
@ApiModel(description = "分页查询结果")
public class PageDTO<T> {
@ApiModelProperty(value = "总条数")
private Long total;
@ApiModelProperty(value = "总页数")
private Long pages;
@ApiModelProperty(value = "分页数据")
private List<T> records;

// of 方法
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Class<VO> clazz) {// clazz代表调用时传入的VO的字节码
PageDTO<VO> dto = new PageDTO<>();
// 1.总条数
dto.setTotal(p.getTotal());
// 2.总页数
dto.setPages(p.getPages());
// 3.分页数据
List<PO> records = p.getRecords();
if (CollUtil.isEmpty(records)) {
dto.setList(Collections.emptyList())
return dto;
}
// 4.拷贝user的BO
dto.setList(BeanUtil.copyToList(records, clazz));
// 5.返回
return dto;
}

// 如果封装的VO类型和PO不一致,就需要自己转换
// 这里用函数式接口,来传递转换的行为
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Function<PO, NO> convertor) {// clazz代表调用时传入的VO的字节码
PageDTO<VO> dto = new PageDTO<>();
// 1.总条数
dto.setTotal(p.getTotal());
// 2.总页数
dto.setPages(p.getPages());
// 3.分页数据
List<PO> records = p.getRecords();
if (CollUtil.isEmpty(records)) {
dto.setList(Collections.emptyList())
return dto;
}
// 4.拷贝user的BO (转换自己写)
dto.setList(records.stream().map(convertor).collect(Collectors.toList()));
// 5.返回
return dto;
}
}

使用示例:

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
public PageDTO<UserVO> query(UserQuery query) {
Strign name = query.getUsername();
Integerr status = query.getStatus();
// 1.构建分页条件
Page<User> page = query.toMpPageDefaultSortByUpdateTime();

// 2.分页查询
Page<User> p = lambdaQuery()// 继承ServiceImpl<M, T>获得的方法
.like(name != null, User::getName, name)
.eq(status != null, User::getStatus, status)
.page(page);

// 3.封装成VO结果
return PageDTO.of(p, UserVO.class)

// 3.1 如果是自己转换
return PageDTO.of(p,user -> BeanUtil.copyProperties(user, UserVO.class))

// 3.2 如果还要对数据做处理
return PageDTO.of(p,user -> {
// 1.拷贝基础属性
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 2.处理特殊逻辑,将用户名后2位替换成**
vo.setUsername(vo.getUsername().substring(0, vo.getUsername().length() - 2) + "**")
})
}

Docker


黑马商城笔记
http://www.981928.xyz/2026/01/09/黑马商城笔记/
作者
981928
发布于
2026年1月10日
许可协议