MyBatisPlus

MyBatisPlus(后续简称MP)是国内团队苞米豆(baomidou)开发的一款MyBatis增强工具。MP在不修改MyBatis配置的前提下,通过继承通用Mapper接口的方式内置了大量单表CRUD方法,此外还提供了代码生成器、分页插件、乐观锁插件等实用功能,能显著减少模板代码的编写量,MP也是目前Java生态中最主流的MyBatis增强框架之一。这篇笔记我们基于SpringBoot 3.x学习MyBatis-Plus的使用。

项目主页:https://baomidou.com/

MyBatis-Plus vs TkMyBatis

前一章节我们介绍过TkMyBatis,它和MyBatis-Plus的定位是重叠的,它们都是MyBatis的增强框架。TkMyBatis出现比较早,但功能则一直比较精简,作者更新缓慢,社区生态也非常小;而MyBatis-Plus流行程度远高于前者,参考资料和用户数量都庞大的多,官方维护也更积极。此外,MyBatis-Plus内置了分页插件,因此其实MyBatis-Plus等同于TkMyBatis加上PageHelper。总而言之两个框架都是可用的,国内使用这两种框架的团队都广泛存在,具体使用时我们根据实际情况任选其一即可。

引入Maven依赖和工程配置

MyBatis-Plus提供了SpringBoot的起步依赖,我们直接引入即可。注意如果你使用的是SpringBoot 2.x,artifactId是mybatis-plus-spring-boot-starter,我们这里的基于SpringBoot 3.x的。

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.16</version>
</dependency>

工程的application.properties配置文件中,我们首先配置MySQL数据源,这部分配置前面我们已经多次编写过,这里就不赘述了。我们主要关注mybatis-plus开头的配置,我们会发现其中很多配置项和Spring中配置MyBatis中的配置字段几乎一模一样,只不过是将mybatis换成了mybatis-plus,表达是意思也是相同的,MyBatis-Plus的定位是MyBatis增强框架,它的配置系统也是这样设计的,MyBatis-Plus的配置命名空间继承和扩展了MyBatis而不是全新设计一套,这也降低了我们使用者的心智负担。

spring.datasource.url=jdbc:mysql://localhost:3306/netstore?serverTimezone=Asia/Shanghai&connectTimeout=5000&socketTimeout=60000&tcpKeepAlive=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.idle-timeout=60000
spring.datasource.hikari.keepalive-time=30000

mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.type-aliases-package=com.gacfox.demo.model
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.configuration.lazy-loading-enabled=true
mybatis-plus.global-config.banner=false

配置中,mybatis-plus.global-config.banner用于控制SpringBoot工程启动是否打印MyBatis-Plus的Banner,我们将其关闭避免过多信息干扰控制台,其它配置项都是MyBatis原生的,这里就不重复介绍了。

使用MyBatis-Plus

下面我们直接通过一个完整的例子演示MyBatis-Plus的使用。

创建实体类

MyBatis-Plus中的实体类需要标注MyBatis-Plus注解,类上标注@TableName并指定表明。主键字段标注@TableId,其中的type属性表示使用数据库的自增主键。普通字段需要标注@TableField注解,并指定字段名。

package com.gacfox.demo.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

@Data
@TableName("t_user")
public class User implements Serializable {
    @TableId(value = "user_id", type = IdType.AUTO)
    private Long userId;

    @TableField("username")
    private String username;

    @TableField("password")
    private String password;
}

@TableId注解的type属性有如下几种取值。

策略 说明
IdType.AUTO 使用数据库自增主键,插入时应用层不设置主键值
IdType.NONE 不指定任何策略,完全由用户手动管理
IdType.INPUT 插入前要求用户手动设置主键值或由自动填充插件在插入前统一赋值
IdType.ASSIGN_ID 使用雪花算法生成Long类型主键,默认值
IdType.ASSIGN_UUID 生成UUID字符串主键

如果我们使用雪花算法ID,这还涉及到分布式机器ID的设置,我们需要在application.properties中添加workerId和datacenterId配置项,不过这两个配置项通常都不是固定值,而是启动时由分布式中间件分配的,因此一般通过环境变量传入。

mybatis-plus.global-config.sequence.worker-id=${SNOW_FLAKE_WORKER_ID:1}
mybatis-plus.global-config.sequence.datacenter-id=${SNOW_FLAKE_DATACENTER_ID:1}

另一种动态配置workerId和datacenterId方法是通过JavaConfig指定,这种方式通过代码指定,更加灵活。

package com.gacfox.demo.config;

import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public IdentifierGenerator identifierGenerator() {
        // 假设这里从Redis、ZooKeeper等地方读取分配的workerId和datacenterId
        long workerId = getWorkerId();
        long dataCenterId = getDataCenterId();
        return new DefaultIdentifierGenerator(workerId, dataCenterId);
    }

    // ...
}

对于@TableField注解,如果某个字段不参与数据库字段映射,必须标注exist属性为false

@TableField(exist = false)
private String token;

此外,当数据库字段名和Java类的成员变量名相同时我们可以省略@TableField注解,不过全部显式写出更直观。

创建Mapper接口

MyBatis-Plus中的Mapper接口需要继承BaseMapper<T>,其中泛型参数传入对应的实体类,继承后我们就自动拥有了一系列通用CRUD方法,不需要再编写已有功能的SQL语句了。如果内置通用CRUD方法能完全满足我们的需求,我们可以不创建任何Mapper XML,但如果我们有特殊业务逻辑的SQL需要执行,那么也完全可以创建Mapper XML并对Mapper接口添加更多方法进行扩展。

package com.gacfox.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gacfox.demo.model.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

实现CRUD操作

下面代码中,我们实现了获取所有用户、根据ID获取用户、保存用户、删除用户、更新用户方法,所有方法都使用的是MyBatis-Plus的内置实现。

package com.gacfox.demo.service.impl;

import com.gacfox.demo.mapper.UserMapper;
import com.gacfox.demo.model.User;
import com.gacfox.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    private final UserMapper userMapper;

    @Autowired
    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public List<User> list() {
        return userMapper.selectList(null);
    }

    @Override
    public User getById(Long id) {
        return userMapper.selectById(id);
    }

    @Override
    public void save(User user) {
        userMapper.insert(user);
    }

    @Override
    public void removeById(Long id) {
        userMapper.deleteById(id);
    }

    @Override
    public void updateById(User user) {
        userMapper.updateById(user);
    }
}

条件构造器

除了按主键的简单CRUD,实际业务中大量查询都需要根据动态条件筛选数据。MyBatis-Plus提供了条件构造器,能让我们以链式调用的方式构建WHERE子句,省去原生MyBatis用XML标签手动拼SQL字符串的麻烦。

QueryWrapper

QueryWrapper是MyBatis-Plus中最常用的条件构造器,它适用于查询、删除、更新操作的条件构建。下面例子代码中,我们查询usernamealice并且email匹配%example.com%的用户。

QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", "alice")
       .like("email", "example.com");
List<User> users = userMapper.selectList(wrapper);

常用的条件方法如下表。

方法 对应 SQL 说明
eq(col, val) col = val 等于
ne(col, val) col != val 不等于
gt(col, val) col > val 大于
ge(col, val) col >= val 大于等于
lt(col, val) col < val 小于
le(col, val) col <= val 小于等于
like(col, val) col LIKE '%val%' 模糊查询
likeLeft(col, val) col LIKE '%val' 左模糊查询
likeRight(col, val) col LIKE 'val%' 右模糊查询
in(col, vals) col IN (...) 在集合中
notIn(col, vals) col NOT IN (...) 不在集合中
isNull(col) col IS NULL 为空
isNotNull(col) col IS NOT NULL 不为空
between(col, v1, v2) col BETWEEN v1 AND v2 区间查询
orderByAsc(col) ORDER BY col ASC 升序排序
orderByDesc(col) ORDER BY col DESC 降序排序

条件方法之间默认使用AND连接,如果需要OR连接需要调用.or()方法。

QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", "alice")
       .or()
       .eq("username", "bob");

UpdateWrapper

更新操作除了updateById()方法还可以使用UpdateWrapper指定更新条件和需要更新的字段,这在批量更新或不按主键更新的场景中很常用。下面例子中,我们实现了对所有邮箱为test@example.com的用户密码更新更新为newpassword

UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("email", "test@example.com")
       .set("password", "newpassword");
userMapper.update(null, wrapper);

update()方法的第一个参数是实体对象,设为null时表示只使用Wrapper中set()方法指定的字段更新;如果传入实体对象,则实体对象中非null的字段也会参与更新。

LambdaQueryWrapper和LambdaUpdateWrapper

QueryWrapperUpdateWrapper中的列名都是字符串,如果我们笔误写错在编译期也不会报错。而LambdaQueryWrapperLambdaUpdateWrapper改用Lambda方法引用来引用字段,这样设计额外获得了编译期类型检查的能力,因此实际开发中推荐优先使用LambdaQueryWrapperLambdaUpdateWrapper

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, "alice")
       .like(User::getEmail, "example.com");
List<User> users = userMapper.selectList(wrapper);

条件的动态拼接

实际业务中很多查询条件是动态的,例如搜索接口中用户可能只传了部分条件,未传的条件不应出现在WHERE子句中。MyBatis-Plus的条件方法普遍支持在第一个参数传入boolean condition来动态控制该条件是否生效,下面是一个例子。

String username = "alice";
String email = null;

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(username != null, User::getUsername, username)
       .like(email != null, User::getEmail, email);

List<User> users = userMapper.selectList(wrapper);

有了condition参数,我们就不需要在外部写大量if判断了,这样代码可以变得更简洁也更不容易出错。

MyBatis-Plus分页插件

MyBatis-Plus 3.5.x中分页插件被独立了出来,我们需要单独引入Maven依赖。MyBatis-Plus分页插件和之前介绍过的PageHelper不同,它仅适用于MyBatis-Plus框架。

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
    <version>3.5.16</version>
</dependency>

引入依赖后,我们还需要使用JavaConfig注册分页插件相关的Spring Bean,MyBatis-Plus的扩展机制需要通过注册这些Bean来加载。

package com.gacfox.demo.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        // 页码溢出后是否返回首页(默认false)
        paginationInnerInterceptor.setOverflow(false);
        // 单页最大条数限制
        paginationInnerInterceptor.setMaxLimit(500L);
        mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
        return mybatisPlusInterceptor;
    }
}

注册插件后,我们使用Page对象即可完成分页查询,MyBatis-Plus会自动生成COUNT查询和LIMIT语句。下面例子代码中,我们查询第1页,每页10条,按user_id降序返回。

Page<User> page = new Page<>(1, 10);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(User::getUserId);

Page<User> result = userMapper.selectPage(page, wrapper);

System.out.println("总记录数:" + result.getTotal());
System.out.println("总页数:" + result.getPages());
System.out.println("当前页数据:" + result.getRecords());

之前PageHelper章节我们曾介绍过低性能COUNT语句问题,MyBatis-Plus的分页插件也支持关闭自动COUNT查询,如下创建Page对象后,MyBatis-Plus将只执行LIMIT查询,不会自动生成COUNT查询。

Page<User> page = new Page<>(current, size, false);

MyBatis-Plus乐观锁插件

在并发更新场景中,为避免多个请求同时修改同一条记录导致数据覆盖造成类似超卖等问题,一种解决方案是采用乐观锁机制。乐观锁更新中,代码会先查询数据记录的version字段再做WHERE version = ?条件更新,如果受影响行数为0表示有其它人更新这条记录了,我们需要重新读取原数据执行更新逻辑。MyBatisPlus通过@Version注解和乐观锁插件直接实现了乐观锁功能,它会在更新时自动在WHERE子句中加入版本号校验,并在更新成功后自动递增version版本号。

乐观锁插件也需要通过JavaConfig手动注册,注册方式和分页插件类似。

package com.gacfox.demo.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

在需要被乐观锁控制的数据模型类中,我们需要使用@Version注解标注version字段。

package com.gacfox.demo.model;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serializable;

@Data
@TableName("t_user")
public class User implements Serializable {
    @TableId(value = "user_id", type = IdType.AUTO)
    private Long userId;

    @TableField("username")
    private String username;

    @TableField("password")
    private String password;

    @Version
    @TableField("version")
    private Long version;
}

下面例子代码我们实现了一个根据ID更新用户名的逻辑。具体的更新实现中,我们先根据ID查询了用户信息,这一步不能省略,它用于确保持久化对象中的version字段,具体更新还是调用Mapper的updateById()方法,这一过程中乐观锁插件会拦截SQL并进行处理,添加乐观锁更新逻辑。代码中,我们还对更新行数进行了判断,如果更新函数是0表示更新失败,此时我们会进行最大3次的重试,如果一直失败则抛出异常。

package com.gacfox.demo.service.impl;

import com.gacfox.demo.mapper.UserMapper;
import com.gacfox.demo.model.User;
import com.gacfox.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    private final UserMapper userMapper;

    @Autowired
    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public void updateUsername(Long id, String username) {
        int maxRetry = 3;
        for (int i = 0; i < maxRetry; i++) {
            User user = userMapper.selectById(id);
            user.setUsername(username);
            if (userMapper.updateById(user) > 0) {
                return;
            }
        }
        throw new RuntimeException("乐观锁更新失败,已重试" + maxRetry + "次");
    }
}
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。