MyBatisPlus

快速入门

入门案例

需求:基于可签资料提供的项目,实现下列功能

  1. 新增用户功能
  2. 根据 id 查询用户
  3. 根据 id 批量查询用户
  4. 根据 id 更新用户
  5. 根据 id 删除用户

步骤:

  1. 引入 MyBatisPlus 的起步依赖

    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
    </dependency>

    MyBatisPlus 官方提供了起步依赖,其中集成了 Mybatis 和 MybatisPlus 的所有功能,并且实现了自动装配效果

    因此我们可以用 MybatisPlus 的 起步依赖代替 Mybatis 的起步依赖

  2. 定义 Mapper

    自定义的 Mapper 继承 MybatisPlus 提供的 BaseMapper 接口:

    public interface UserMapper extends BaseMapper<User> {

    }

    继承的时的泛型为操作的实体类的类型

    继承后,就可以使用父类提供的方法来对数据库单表进行增删改查

常见注解

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

  • 如果不进行配置,则实体类属性和数据库表字段的对应关系如下:

    • 类名驼峰转下划线作为表名
    • 名为 id 的字段作为主键(属性中必须有一个字段作为主键!)
    • 变量名驼峰转下划线作为表的字段名
  • 如果要配置类名与表名、属性与字段的对应关系,则常用到如下几个注解:

    • @TableName:用来指定表名

    • @TableId:用来指定表中的主键字段信息

      属性 type 有三个常用值:

      说明
      idType.AUTO id 的值交由数据库自增长
      idType.INPUT id 的值需要在插入时由程序员自己赋值
      idType.ASSIGN_ID(默认值) id 的值由接口 IdentifierGenerator 的默认实现类 DefaultIdentifiterGenerator 中方法 nextId 通过雪花算法来生成

      自增主键必须加上 @TableId(type = idType.AUTO)

    • @TableField:用来指定表中的普通字段信息

      使用场景:

      • 成员名与数据库字段名不一致

      • 成员变量名以 is 开头,且是布尔类型,必须加上该注解来对应字段

        反射时,is 开头,且是布尔类型的成员变量名的 is 会被去掉,导致无法与数据库字段对应

      • 成员变量名与数据库关键字冲突,如:

        @TableField("'order'") // 加上 '' 作为转义字符
        private Integer order;
      • 成员变量不是数据库字段,如:

        @TableField(exist = false)
        private String address;

常见配置

MyBatisPlus 的配置项继承了 MyBatis 原生配置和一些自己特有的配置,例如:

官网配置说明文档:使用配置 | MyBatis-Plus (baomidou.com)

核心功能

条件构造器

MyBatisPlus 支持各种复杂的 where 条件,可以满足日常开发的所有需求

案例:基于 QueryWrapper 的查询

需求:

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

    // 1.构建查询条件
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
    .select("id", "username", "info", "balance")
    .like("username", "o")
    .ge("balance", 1000);
    // 2.查询
    List<User> users = userMapper.selectList(wrapper);
  2. 更新用户名为 jack 的用户余额为 2000

    // 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.设置需要更新的 id
List<Long> ids = List.of(1L, 2L, 4L);
// 2.构造条件
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", ids);
// 3.更新数据
userMapper.update(null, wrapper);

避免字符串硬编码,所以推荐使用 LambdaWrapper 来动态获取 column,如:

// 1.构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.select(User::getId, User::getUsername. User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);

可以避免因实体类属性名变更而需要大幅改动原代码的情况

自定义 SQL

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

步骤:

  1. 基于 Wrapper 构建 where 条件

    List<Long> ids = List.of(1L, 2L, 4L);
    int amount = 200;
    // 1.构建条件
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
    .in(User:getId, ids);
    // 2.自定义SQL方法调用
    userMapper.updateBalanceByIds(wrapper, amount);
  2. 在 mapper 方法参数中用 @Param 声明 wrapper 变量名称,必须是 ew

    void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
  3. 自定义 SQL,并使用 wrapper 条件

    <update id="updateBalanceByIds">
    UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

Service 接口

基本用法

如果想要在 Service 实现类中减少代码controller 直接调 service 实现类就可以实现基本的增删改查功能,其余的代码都不用写,就可以选择让 service 继承 MyBatisPlus 的一个接口 IService,具体步骤如下:

  1. 让 Service 接口 继承 IService

    public UserService extends IService<User>{}
  2. Service 的实现类继承类 ServiceImpl(实现了 IService 接口的所有方法)

    public UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{}
  • IService 泛型填 Service 要操作的实体类
  • ServiceImpl 泛型第一个填 Service 实现业务功能要调用的 Mapper,第二个填 Service 要操作的实体类

开发基本业务接口

需求:基于 Restful 风格实现下面的接口:

  1. 新增用户

    @ApiOperation("新增用户接口")
    @PostMapping
    public void saveUser(@RequestBody UserFormDTO userDTO) {
    // 1.把DTO拷贝到PO
    User user = BeanUtil.copyProperties(userDTO, User.class);
    // 2.新增
    userService.save(user);
    }
  2. 删除用户

    @ApiOperation("删除用户接口")
    @PostMapping
    public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
    // 删除
    userService.removeById(id);
    }
  3. 根据id查询用户

    @ApiOperation("根据id查询用户接口")
    @PostMapping
    public UserVO deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
    // 1.查询
    User user = userService.getById(id);
    // 2.把PO拷贝到VO
    return BeanUtil.copyProperties(user, UserVO.class)
    }
  4. 根据id批量查询

    @ApiOperation("根据id批量查询用户接口")
    @PostMapping
    public List<UserVO> deleteUserByIds(@ApiParam("批量用户id") @RequestParam List<Long> ids) {
    // 1.查询
    List<User> users = userService.listByIds(ids);
    // 2.把PO拷贝到VO
    return BeanUtil.copyToList(users, UserVO.class)
    }

开发复杂业务接口

  1. 根据id扣减余额

    // controller
    @ApiOperation("扣减用户余额接口")
    @PutMapping("/{id}/deduction/{money}")
    public void deduceBalanceById(
    @ApiParam("用户id") @PathVariable("id") Long id
    @ApiParam("扣减金额") @PathVariable("money") Integer money) {
    // 删除
    userService.deduceBalanceById(id, money);
    }
    // service interface
    public UserService extends IService<User>{
    public void deduceBalanceById(Long id, Integer money);
    }
    // serviceimpl
    public UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
    @Override
    public void deduceBalanceById(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.deduceBalanceById(id, money);
    }
    }
    // mapper
    @Update("UPDATE tb_user SET balance = balance - #{money} WHERE id = #{id}")
    public interface UserMapper extends BaseMapper<User> {
    void deduceBalanceById(@Param("id") Long id, @Param("money") Integer money);
    }

IService 中的 Lambda 方法 —— 复杂条件查询或复杂更新

IService 中的 Lambda 查询

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

  • name:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minBalance:最小余额,可以为空
  • maxBalance:最大余额,可以为空
// serviceimpl
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();
}
IService 中的 Lambda 更新

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

  1. 完成对用户状态校验
  2. 完成对用户余额校验
  3. 如果扣减口余额为 0,则将用户 status 修改为冻结状态(2)
// serviceimpl
public UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
@Override
public void deduceBalanceById(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.扣减余额
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)
// 前面的语句只是在构建SQL语句,最后一定要加上update方法,这样SQL语句才会执行
.update();
}
}

补充:业务中 vo、dto 等的关系

IService 批量新增

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

  • 普通 for 循环插入:耗时200秒左右

    • 该方式一条一条数据插入,总共提交了 10万 次的请求,十分低效
  • IService 的批量插入:耗时20秒左右

    • 该方式每 1000 条数据插入一次,IService 的 saveBatch 方法会对这一千条数据进行 SQL 预编译,编译为 1000 条 SQL 语句,这样,总共提交的请求次数减少为 100000/1000 = 100 次,大大提高了效率

    • 现在我们想进一步优化时间,于是我们想要 IService 预编译的 1000 SQL 语句合并为一条,这样,MySQL 总共会处理 100 条 SQL 语句,处理 100 次请求,相较于 100000 条 SQL语句和 100 次请求,耗时又可以减少,两种办法:

      • 手写 SQL 语句,使用 foreach 标签,将 1000 条数据写在一条 SQL 语句中

      • 启用 MySQL 的重写批处理语句属性,在 application.properties 的数据库连接区域配置

        spring.datasource.url=jdbc:mysql://localhost:3306/tlias?rewriteBatchedStatements=true
    • 启用重写批处理语句属性后,执行时间只需要6秒,大大提高了批量插入效率

扩展功能

代码生成

  1. 使用插件——MyBatisPlus
  2. 在顶部工具栏的 Other 选项中配置数据库信息
  3. 在 Other 选项卡中选择 code generator,配置代码生成信息
    • module 填所属模块,若只有一个模块则空出
    • TablePrefix 处填表名前缀,如表名为 tb_user,想要生成的实体类名为User,则填入 tb_
    • Entity、Mapper等表单中填写想要对应生成类所属包的包名

Db 静态工具

用法与 IService 接口完全一致,区别在于 Db静态类,无法使用泛型,所以在方法中需要给出实体类的 class 对象。

Db静态工具的作用:避免 Service 实现类中的循环依赖问题

用法:

Db.methodName(parameters);

逻辑删除

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

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

  • 当删除数据时把标记设置为 1,即将删除语句改为更新语句

    UPDATE user SET deleted = 1 WHERE id = #{id} AND deleted = 0;
  • 查询时只查询标记为 0 的数据

    SELECT * FROM user WHERE deleted = 0;

但如果采用逻辑删除,则 MyBatisPlus 提供的方法将无法实现需求。

于是,MyBatisPlus 提供了逻辑删除功能,无需改变方法调用的方式,而是在底层自动修改 CRUD 语句。

我们需要做的,就是在 application.properties 文件中配置逻辑删除的字段名称和值即可:

# 全局逻辑删除的实体字段名,字段类型可以是Boolean和Integer 
mybatis-plus.global-config.db-config.logic-delete-field=flag
# 表明逻辑已删除的值(默认为1)
mybatis-plus.global-config.db-config.logic-delete-value=1
# 表明逻辑未删除的值(默认为0)
mybatis-plus.global-config.db-config.logic-not-delete-value=0

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

  • 会导致数据库表垃圾数据越来越多,影响查询效率
  • SQL 中全都需要对逻辑删除字段做判断,影响查询效率

因此,逻辑删除不太推荐,如果实在需要,可以考虑将删除数据迁移到其他表来实现

枚举处理器

在用户实体类中,我们常常会用定义表示 状态 的整数类型的字段,但这样做难以记忆,且不直观,又因为枚举类型可以直接用 == 进行比较,所以我们通过用枚举类型来代替整数类型来表示用户状态。

// 用户实体类
public class User {
// ... 其他字段
private UserStatus status;
}
// 用户状态枚举类
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");

private final int value;
private final String description;

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

这样做虽然可以直观地表示用户的状态了,但是在将用户实体类插入数据库时又会遇到问题——枚举类型无法转换为数据库中的表示状态的数据类型(一般为 int),所以,为了解决这一问题,MyBatisPlus 为我们提供了枚举处理器 MybatisEnumTypeHandler 来实现枚举类型和数据库类型的转换。

使用步骤:

  1. 在枚举类定义中指明与数据库字段值对应的属性,如 UserStatus 类型中 value 属性对应数据库中的 status 值:

    // 用户状态枚举类
    @Getter
    public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结");
    @EnumValue
    private final int value;
    private final String description;

    UserStatus(int value, String description) {
    this.value = value;
    this.description = description;
    }
    }
  2. 使枚举处理器生效

    mybatis-plus.configuration.default-enum-type-handler=com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

以上配置后,查询返回的值是枚举成员名 NORMAL 或 FREEZE,如果想要返回枚举类中的某个属性的值,则可以在想要返回的属性上加上 @JsonValue 注解,如:

// 用户状态枚举类
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");
@EnumValue
private final int value;
@JsonValue
private final String description;

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

JSON 处理器

数据库数据类型中有 JSON 类型,保存为 JSON 字符串,插入数据时我们只需要将插入的数据声明为 String 类型,``Mybatis会自动将String` 类型转换为 JSON 类型。

但如果要从数据库中取出 JSON 类型的数据并做业务处理,由于取出的是字符串而不是一个对象,就要转换为对象才能做业务处理,十分繁琐,为了解决这个问题,``MyBatisPlus为我们提供了 JSON 处理器AbstractJsonTypeHandler` 抽象类,其有三个子类

  • GsonTypeHandler
  • JacksonTypeHandler:SpringMVC 底层使用的 JSON 处理器,推荐使用
  • FastjsonTypeHandler

使用步骤:

  1. 在需要从 JSON 转换为对象的属性上加上注解 @TableField(typeHandler = JacksonTypeHandler.class)

  2. 在该属性所属实体类的 @TableName 注解中补充 autoResultMap 属性为true

    @Data
    @TableName(value = "user", autoResultMap = true)
    public class User {
    private Long id;
    private String username;
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
    }
    @Data
    public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
    }
    {
    "age": 20,
    "intro": "..",
    "gender": "male"
    }

如此,即可完成 JSON 到对象的转换

插件功能

分页插件

步骤:

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

    @Configuration
    public class MyBatisConfig {
    @Bean
    public MyBatisPlusInterceptor myBatisPlusInterceptor() {
    // 1.初始化核心插件
    MyBatisPlusInterceptor interceptor = new MyBatisPlusInterceptor();
    // 2.添加分页插件
    PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
    pageInterceptor.setMaxLimit(1000L); // 设置分页上限
    interceptor.addInnerInterceptor(pageInterceptor);
    return interceptor;
    }
    }
  2. 接着,就可以使用分页的 API 了:

    int pageNo = 1, pageSize = 5;
    // 分页参数
    Page<User> page = Page.of(pageNo, pageSize);
    // 排序参数,true升序,false降序
    page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));
    // 分页查询
    Page<User> p = userService.page(page);
    // 总条数
    System.out.println("total = " + p.getTotal());
    // 总页数
    System.out.println("total = " + p.getPages());
    // 分页数据
    List<User> records = p.getRecords();
    records.forEach(System.out::println);

通用分页实体

// 通用分页实体
// 其他需要分页查询的实体类继承该类即可完成分页查询
// 可以将
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("每页条数")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Integer isAsc;
}

可以进行如下优化,封装重复的逻辑,提高代码复用性