🌟 后端 | MybatisPlus 功能与应用



JDBC (Java Database Connectivity)

JDBC 是通过 java 连接数据库的一种技术,透过每个数据库对应的 JDBC driver 来与数据库连接,并且可以通过 JDBC 的 API 来向指定的数据库发送想要执行的 SQL 命令。

ORM (Object-Relational Mapping)

对象关系映射,使 java 对象和数据库表建立映射关系,可以在开发程序时无须操作许多繁琐的 SQL 语句。但如果对于需要做复杂表操作时,像多表之间的 JOIN 操作或查询,相比还是直接用 SQL 语句方便。

JPA (Java Persistence API)

JPA 就是「Java 持久化的 API」,持久化即资料数据”存储“与”读取“的过程,通过 Java 将数据存储到数据库的 API。实际上 JPA 是一种 “映射对象与持久层 API 的实现方式” 的规范,可以通过 JPA 来映射 【对象的字段】 与 【表的栏位】 之间的对应关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.persistence.*;

@Entity
@Table(name = "PRODUCT")
public class Product {

@Id
@Column(name = "ID")
private Long id

@Column(name = "NAME")
private String name;

@Column(name = "REMAIN")
private Integer remain;

/* constructors ... */

/* getter and setter ... */
}

面對一致的 JPA 規範時,開發者也就可以隨時切換想要使用的 ORM 框架了。

使用 JPA 還有一個好處,那就是可以使用 JPQL (Java Persistence Query Language) 的命令語句向資料庫下命令語句,但 JPQL 與 SQL 最大的差異在於:

  • SQL 在不同的資料庫中,有不同的 SQL 命令語句
  • JPQL 操作的對象不是著重在資料庫,而是著重在 JPA 的 Entity Object 下類似 SQL 的命令語句

也就是說當使用 JPQL 的時候,並不會隨著不同的資料庫而需要做對應 SQL 語句修改

目前在 Java 中,相對常見或常用的 ORM 框架有:

Hibernate
Spring Data JPA
Open JPA
目前在 Java 中,相對常見或常用的 ORM 框架有:

Hibernate
Spring Data JPA
Open JPA
jQQQ
GraghQL
MyBatis

主流还是 Mybatis 更通用些,操作更方便

MyBatis 简单 CRUD

连接数据库

application.yaml 中配置

1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: MySQL123
logging:
level:
com.itheima: debug
pattern:
dateformat: HH:mm:ss

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

定义 Mapper

MybatisPlus 提供了一个基础的 BaseMapper 接口,其中已经实现了单表的 CRUD 。

存在 delete insert select update / deleteById selectById updateById 等方法。

因此我们自定义的 Mapper 只要实现了这个 BaseMapper,就无需自己实现单表CRUD了。

  • BaseMapper
    • delete(Wrapper): int
    • deleteBatchlds(Collection<?>): int
    • deleteByld (Serializable): int
    • deleteByld (T): int
    • deleteByMap(Map<String, Object>): int
    • exists(Wrapper): boolean
    • insert(T): int
    • selectBatchlds(Collection<? extends Serializable>): List
    • selectByld(Serializable): T
    • selectByMap(Map<String, Object>): List
    • selectCount(Wrapper): Long
    • selectList(Wrapper): List
    • selectMaps(Wrapper): List<Map<String, Object>>
    • selectMapsPage(P, Wrapper): P
    • selectObjs(Wrapper): List
    • selectOne(Wrapper): T
    • selectPage(P, Wrapper): P
    • update(T, Wrapper): int
    • updateByld (T): int
1
2
3
4
5
6
7
8
package com.itheima.mp.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;

// 指定自己的实体类(表名)—— 泛型中的 User 就是与数据库对应的 PO
public interface UserMapper extends BaseMapper<User> {
}

MybatisPlus 就是根据 PO 实体的信息来推断出表的信息,从而生成 SQL 的。默认情况下:

  • MybatisPlus 会把 PO 实体的类名驼峰转下划线作为表名
  • MybatisPlus 会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
  • MybatisPlus 会把名为 id 的字段作为主键

注解

@TableName

表名注解,标识实体类对应的表

1
2
3
4
5
@TableName("user")
public class User {
private Long id;
private String name;
}

@TableId

主键注解,标识实体类中的主键字段

1
2
3
4
5
6
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
}

@TableField

普通字段注解

一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:

  • 成员变量名与数据库字段名不一致
  • 成员变量是以 isXXX 命名,按照 JavaBean 的规范,MybatisPlus 识别字段时会把 is 去除,这就导致与数据库不符。
  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用 @TableField 注解给字段名添加转义字符:
1
2
3
4
5
6
7
8
9
10
11
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
@TableField("is_married")
private Boolean isMarried;
@TableField("`concat`")
private String concat;
}

MyBatisPlus 配置

1
2
3
4
5
6
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 实体类的别名扫描包
global-config:
db-config:
id-type: auto # 全局id类型为自增长
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。

MyBatisPlus 也支持手写 SQL 的,而 mapper 文件的读取地址可以自己配置,例如新建一个 UserMapper.xml 文件:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper">

<select id="queryById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

MyBatisPlus Wrapper 编写 SQL 拓展

BaseMapper 中的方法提供 Wrapper 泛型对象的查询体。

  • BaseMapper
    • delete(Wrapper): int
    • deleteBatchlds(Collection<?>): int
    • deleteByld (Serializable): int
    • deleteByld (T): int
    • deleteByMap(Map<String, Object>): int
    • exists(Wrapper): boolean
    • insert(T): int
    • selectBatchlds(Collection<? extends Serializable>): List
    • selectByld(Serializable): T
    • selectByMap(Map<String, Object>): List
    • selectCount(Wrapper): Long
    • selectList(Wrapper): List
    • selectMaps(Wrapper): List<Map<String, Object>>
    • selectMapsPage(P, Wrapper): P
    • selectObjs(Wrapper): List
    • selectOne(Wrapper): T
    • selectPage(P, Wrapper): P
    • update(T, Wrapper): int
    • updateByld (T): int

Wrapper 实现复杂 SQL

Wrapper 就是条件构造的抽象类,Wrapper 就是条件构造的抽象类。Wrapper 的子类 AbstractWrapper 提供了 where 中包含的所有条件构造方法。

Wrapper 是提供查询条件,而 Mapper 则是对应自定义的 SQL

AbstractWrapper 基础上,又增加了查询 QueryWrapper、更新 UpdateWrapper 、AbstractLambdaWrapper 的子类方便操作。

  • AbstractWrapper
    • getEntity): T Wrapper
    • setEntity(T): Children
    • getEntityClass): Class
    • setEntityClass(Class): Children
    • allEq(boolean, Map<R, V>, boolean): Children
    • allEq(boolean, BiPredicate<R, V>, Map<R, V>, boolean): Children
    • eq (boolean, R, Object): Children
    • ne(boolean, R, Object): Children
    • gt(boolean, R, Object): Children
    • ge(boolean, R, Object): Children
    • It(boolean, R, Object): Children
    • le(boolean, R, Object): Children
    • like(boolean, R, Object): Children
    • notLike(boolean, R, Object): Children
    • likeLeft(boolean, R, Object): Children
    • likeRight(boolean, R, Object): Children
    • notLikeLeft(boolean, R, Object): Children
    • notLikeRight(boolean, R, Object): Children
    • between(boolean, R, Object, Object): Children
    • notBetween(boolean, R, Object, Object): Children
    • and (boolean, Consumer): Children
    • or(boolean, Consumer): Children
    • nested (boolean, Consumer): Children
    • not(boolean, Consumer): Children

QueryWrapper

利用 Wrapper 构建查询条件,再利用 UserMapper 的方法进行查询、更新

QueryWrapper 在 AbstractWrapper 的基础上拓展了 select 方法,允许指定查询字段

  • QueryWrapper
    • select(String…): QueryWrapper
    • select(List): QueryWrapper
    • select(Class, Predicate): QueryWrapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 查询名字中带 c ,余额大于 1000 的用户
@Test
void testQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "c")
.ge("balance", 1000);
// 2.查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

// 更新其中一个用户的余额为 800
@Test
void testUpdateByQueryWrapper() {
// 1.构建查询条件 where name = "Jack"
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");
// 2.更新数据,user中非null字段都会作为set语句
User user = new User();
user.setBalance(800);
userMapper.update(user, wrapper);
}

UpdateWrapper

userMapper.update() 基于 BaseMapper 中的 update 方法更新时只能直接赋值,对于一些复杂的需求就难以实现。

UpdateWrapper 在 AbstractWrapper 的基础上拓展了一个 set 方法,允许指定 SQL 中的 SET 部分

  • UpdateWrapper
    • getSqlSet(): String
    • set(boolean, String, Object, String): UpdateWrapper
    • setSql(boolean, String): UpdateWrapper

例如:更新 id 为 1,2,4 的用户的余额,扣 200。SET 的赋值结果是基于字段现有值的,所以只能先查出用户账户的余额,再在余额基础上减去 200。 这个时候就要利用 UpdateWrapper 中的 setSql 功能

1
2
3
4
5
6
7
8
9
10
11
@Test
void testUpdateWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
// 1.生成SQL
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200") // SET balance = balance - 200
.in("id", ids); // WHERE id in (1, 2, 4)
// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
// 而是基于UpdateWrapper中的setSQL来更新
userMapper.update(null, wrapper);
}

LambdaQueryWrapper

QueryWrapper/UpdateWrapper 在构造条件的时候都需要写死字段名称,这会出现字符串魔法值。

所以 MyBatisPlus 提供基于变量的 getter 方法结合反射技术,来计算出对应的变量名,相应的方法即为基于 Lambda 的 Wrapper:LambdaQueryWrapper/LambdaUpdateWrapper

以上边 QueryWrapper 的例子,查询名字中带 c ,余额大于 1000 的用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 写死字段:
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.select("id", "username", "info", "balance")
.like("username", "c")
.ge("balance", 1000);

// ---------------------------

@Test
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "c")
.ge(User::getBalance, 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

自定义 SQL —— 使 SQL 语句维护在持久层

UpdateWrapper.setSql("balance = balance - 200") 中 SQL 语句最好都维护在持久层,而不是业务层。

MybatisPlus 提供了自定义 SQL 功能,可以让我们利用 Wrapper 生成查询条件,再结合 Mapper 编写 SQL

1
2
3
4
5
6
7
8
9
@Test
void testCustomWrapper() {
// 1.准备自定义查询条件
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);

// 2.调用 mapper 的自定义方法,直接传递 Wrapper
userMapper.deductBalanceByIds(200, wrapper);
}

在 UserMapper 中定义 deductBalanceByIds 方法:

1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.mp.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;

public interface UserMapper extends BaseMapper<User> {
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}

多表关联

理论上来讲 MyBatisPlus 是不支持多表查询的,不过我们可以利用 Wrapper 中自定义条件结合自定义 SQL 来实现多表查询的效果。

想要实现如下查询:user 表作为 u,address 表作为 a,从 user 中筛选指定 id 用户,且 address 对应指定的 city

1
2
3
4
5
6
7
8
9
10
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM user u
INNER JOIN address a ON u.id = a.user_id
WHERE u.id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#{id}
</foreach>
AND a.city = #{city}
</select>

可以利用 Wrapper 生成查询条件,再结合 Mapper 编写 SQL

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testCustomJoinWrapper() {
// 1.准备自定义查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("u.id", List.of(1L, 2L, 4L))
.eq("a.city", "北京");

// 2.调用 mapper 的自定义方法
List<User> users = userMapper.queryUserByWrapper(wrapper);

users.forEach(System.out::println);
}

使用 QueryWrapper 传入 user 和 address 信息,接着将构建的查询体传入 UserMapper 中定义 queryUserByWrapper 方法:

1
2
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);

也可以在UserMapper.xml中写SQL:

1
2
3
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>

Service 接口

MybatisPlus 不仅提供了 BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的 service 模板方法。通常 SpringBoot 框架中操作数据库会用此方式书写。

通用接口为 IService,默认实现为 ServiceImpl,其中封装的方法可以分为以下几类:

  • save:新增
  • remove:删除
  • update:更新
  • get:查询单个结果
  • list:查询集合结果
  • count:计数
  • page:分页查询

CRUD

  • 新增

    • save是新增单个元素
    • saveBatch是批量新增
    • saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
    • saveOrUpdateBatch是批量的新增或修改
  • 删除

    • removeById:根据id删除
    • removeByIds:根据id批量删除
    • removeByMap:根据Map中的键值对为条件删除
    • remove(Wrapper):根据Wrapper条件删除
  • 修改

    • updateById:根据id修改
    • update(Wrapper):根据UpdateWrapper修改,Wrapper中包含set和where部分
    • update(T,Wrapper):按照T内的数据修改与Wrapper匹配到的数据
    • updateBatchById:根据id批量修改
  • GET

    • getById:根据id查询1条数据
    • getOne(Wrapper):根据Wrapper查询1条数据
    • getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper
  • List

    • listByIds:根据id批量查询
    • list(Wrapper):根据Wrapper条件查询多条数据
    • list():查询所有
  • Count

    • count():统计所有数量
    • count(Wrapper):统计符合Wrapper条件的数据数量

IService

定义我们自己的接口,然后扩展 IService 接口

1
2
3
4
5
6
7
8
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {
// 拓展自定义方法
}

IServiceImpl

编写 UserServiceImpl 类,继承 ServiceImpl ,实现 UserService

1
2
3
4
5
6
7
8
9
10
11
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.service.IUserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;

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

接口实现

上边已经定义了 User(PO) 对应的 Service 和 ServiceImpl,接下来实现 User 的 DTO 和 VO

UserFormDTO:代表新增时的用户表单

User 表的字段更为全面,PO 记录详细字段,但是新增时则不需要主动收集创建人、创建时间之类的信息。只需收集用户填入的信息即可。

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;
}

UserVO:代表查询的返回结果

同理,VO 只返回用户所需查询的字段即可,无需将 User 表 (PO) 所有字段返回

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;
}

UserController:用户相关接口

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
package com.itheima.mp.controller;

import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO; // DTO 对应新建账户字段
import com.itheima.mp.domain.po.User; // PO 对应数据库表
import com.itheima.mp.domain.vo.UserVO; // VO 对应查询返回字段
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Api(tags = "用户管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("users")
public class UserController {

private final IUserService userService;

@PostMapping
@ApiOperation("新增用户")
public void saveUser(@RequestBody UserFormDTO userFormDTO){
// 1.转换DTO为PO
User user = BeanUtil.copyProperties(userFormDTO, User.class);
// 2.新增
userService.save(user);
}

@DeleteMapping("/{id}")
@ApiOperation("删除用户")
public void removeUserById(@PathVariable("id") Long userId){
userService.removeById(userId);
}

@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 1.查询用户
User user = userService.getById(userId);
// 2.处理vo
return BeanUtil.copyProperties(user, UserVO.class);
}

@GetMapping
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
// 1.查询用户
List<User> users = userService.listByIds(ids);
// 2.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
}

Service 中自定义复杂接口

部分接口功能由 Service 提供的基础方法可以完成,如基础方法无法直接实现,那么需要在上边 IService IServiceImpl 进行自定义方法

UserController 中新增一个方法

1
2
3
4
5
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){
userService.deductBalance(id, money);
}

IUserService 接口新增方法

deductBalance 方法只需在 IUserService 中定义方法名、参数名即可,如有其他部分可以抽象也可提出来,否则只在 IUserServiceImpl 实现具体方法即可。因为有时同个方法可能在不同情况下有不同的处理方式,因此这个 IUserServiceImpl 中可以继承 IUserService 再实现自定方法,其他的 ServiceImpl 也有可能会需要继承这个 IUserService

1
2
3
4
5
6
7
8
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {
void deductBalance(Long id, Integer money);
}

IUserServiceImpl 类实现新增方法

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
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;

@Service
public class IUserServiceImpl extends IServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductBalance(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.判断用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常");
}
// 3.判断用户余额
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足");
}
// 4.扣减余额
baseMapper.deductMoneyById(id, money);
}
}

补充自定义 SQL 的 Mapper

1
2
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);

Lambda 功能

创建查询条件实体 UserQueryDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itheima.mp.domain.query;

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

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQueryDTO {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}

UserController 中使用 lambdaQuery 代替手动创建 Wrapper 步骤

userService.lambdaQuery() 代替了 userService.list(new QueryWrapper().lambda())

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQueryDTO query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
// 2.查询用户
List<User> users = userService.lambdaQuery()
.like(username != null, User::getUsername, username)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance)
.list();
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}

lambdaQuery 方法使用链式编程组成查询条件,最后使用 .list() 返回符合条件的集合。可选的方法有:

  • .one():最多1个结果
  • .list():返回集合结果
  • .count():返回计数结果

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。

IUserServiceImpl 中使用 lambdaUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3.校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4.扣减余额 update tb_user set balance = balance - ?
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance) // 更新余额
.set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance()) // 乐观锁
.update();
}

之后将会在接口中使用该方法

1
2
3
4
5
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){
userService.deductBalance(id, money);
}

MybatisPlus 批处理

MybatisPlus 的批处理是基于 PrepareStatement 的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
// ...SqlHelper
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
int size = list.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
for (E element : list) {
consumer.accept(sqlSession, element);
if (i == idxLimit) {
sqlSession.flushStatements();
idxLimit = Math.min(idxLimit + batchSize, size);
}
i++;
}
});
}

处理后会生成类似如下这样:

1
2
3
4
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01

最佳性能则是合并多条 SQL ,插入时只执行一条语句。我们需要 MySQL 的客户端连接参数 修改 rewriteBatchedStatements: true

在 application.yml 的jdbc url 后增加该参数

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: MySQL123

在最后执行插入时,变成:

1
2
3
4
5
6
7
INSERT INTO user 
( username, password, phone, info, balance, create_time, update_time )
VALUES
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);

逻辑删除

MybatisPlus 添加了对逻辑删除的支持。对于重要的数据,删除只是将删除标记更改(例如 true),在查询时过滤掉该字段为 true 的数据。但也存在问题,会使得数据库表垃圾数据越来越多。

例如给 Address 实体增加 deleted 字段

1
2
3
4
5
6
7
@TableName("address")
public class Address {
@TableId
private Long id;
// ---
private Boolean deleted;
}
1
2
3
4
5
6
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

执行删除操作,届时仅会执行 update 变更 deleted 字段

1
2
3
4
5
@Test
void testDeleteByLogic() {
// 删除方法与以前没有区别
addressService.removeById(59L);
}

通用枚举

MybatisPlus 提供了 @EnumValue 注解来标记枚举属性

分页

定义分页实体

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@ApiModel(description = "分页查询实体")
public class PageQueryDTO {
@ApiModelProperty("页码")
private Long pageNo;
@ApiModelProperty("页码")
private Long pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}

UserQueryDTO 继承分页实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itheima.mp.domain.query;

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

@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQueryDTO extends PageQueryDTO {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}

分页实体 PageDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itheima.mp.domain.dto;

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

import java.util.List;

@Data
@ApiModel(description = "分页结果")
public class PageVO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
}

UserController 中定义分页查询用户的接口

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
package com.itheima.mp.controller;

import com.itheima.mp.domain.dto.PageVO;
import com.itheima.mp.domain.dto.UserQueryDTO;
import com.itheima.mp.domain.query.PageQuery;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UserController {

private final IUserService userService;

@GetMapping("/page")
public PageVO<UserVO> queryUsersPage(UserQueryDTO query){
return userService.queryUsersPage(query);
}

// 。。。 略
}

IUserService 中创建 queryUsersPage 方法

1
PageVO<UserVO> queryUsersPage(PageQuery query);

在 IUserServiceImpl 中实现该方法

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
@Override
public PageVO<UserVO> queryUsersPage(PageQuery query) {
// 1.构建条件
// 1.1.分页条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
// 1.2.排序条件
if (query.getSortBy() != null) {
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
}else{
// 默认按照更新时间排序
page.addOrder(new OrderItem("update_time", false));
}
// 2.查询
page(page);
// 3.数据非空校验
List<User> records = page.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return new PageVO<UserVO>(page.getTotal(), page.getPages(), Collections.emptyList());
}
// 4.有数据,转换
List<UserVO> list = BeanUtil.copyToList(records, UserVO.class);
// 5.封装返回
return new PageVO<UserVO>(page.getTotal(), page.getPages(), list);
}