Mybatis-Plus


Mybatis-Plus

快速入门

首先,引入 maven 依赖:

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

接着,让需要使用的 Mapper 接口继承 mybatis-plus 提供的 BaseMapper<T> 接口:

@Mapper
public interface AccountMapper extends BaseMapper<Account> {
	// 注意BaseMapper中需要指定泛型
}

继承之后,就可以使用 BaseMapper<T> 提供的基础单表增删改查方法,提高开发效率。

常见注解

MyBatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。默认的约定如下:

  • 类名驼峰转下划线为默认映射表名。
  • 名为 id 的字段默认映射主键。
  • 变量名驼峰转下划线映射为表的字段名。

如果需要自主配置,则需要利用注解,常见的注解如下(下述的三个注解是指定在实体类上的):

  • @TableName:用来指定表名。
  • @TableId:用来指定表中的主键字段信息。
  • @TableField:用来指定表中的普通字段信息。

@TableField 注解的使用场景需要特别注意:

  1. 成员变量名与数据库字段名不一致。
  2. 成员变量名以 is 开头,且是布尔值。
  3. 成员变量名与数据库关键字冲突,此时需要使用转义字符。如:@TableField("`order`")
  4. 成员变量不是数据库字段,此时需要主动标识其不存在。如:@TableField(exist = false)

示例如下:

@TableName("tb_account")
public class Account {

    @TableId(value = "id", type = IdType.AUTO)
    /**
     * IdType:
     *      AUTO-数据库自增长
     *      INPUT-通过set方法自行输入
     *      ASSIGN_ID-雪花算法分配id
     *	    NONE-无特定生成策略(默认)
     *		ASSIGN_UUID-UUID分配id
     */
    private Integer id;

    @TableField("username")
    private String name;

    @TableField("money")
    private Integer money;

}

常见配置

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml    # Mapper.xml地址
  type-aliases-package: cn.hnu.mybatisplus.entity   # 别名包
  configuration:
    map-underscore-to-camel-case: true  # 自动驼峰映射
    cache-enabled: true # 开启二级缓存
  global-config:
    db-config:
      id-type: auto # 全局id生成模式
      update-strategy: not_null # 更新策略:只更新非空字段

核心功能

条件构造器

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

基于 QueryWrapper 的查询

void testQueryWrapper() {
    /**
     * 示例sql:
     *  select id, name, money
     *  from account
     *  where name like ? and money >= ?
     */

    // 构建查询条件
    QueryWrapper<Account> wrapper = new QueryWrapper<Account>()
            .select("id", "name", "money")
            .like("name", "o")
            .ge("money", 1000);

    // 查询
    List<Account> accounts = accountMapper.selectList(wrapper);
    accounts.forEach(System.out::println);
}
void testUpdateByQueryWrapper() {
    /**
     * 示例sql:
     *      update account
     *      set money = 3000
     *      where name = '张三'
     */

    // 要更新的数据,直接赋值
    Account account = Account.builder().money(3000).build();

    // 更新条件
    QueryWrapper<Account> wrapper = new QueryWrapper<Account>()
            .eq("name", "张三");

    // 执行更新
    accountMapper.update(account, wrapper);
}

基于 UpdateWrapper 的更新

void testMybatisPlus() {
    /**
     * 示例sql:
     *  update account
     *  set money = money - 200
     *  where id in (1, 2)
     */
    UpdateWrapper<Account> wrapper = new UpdateWrapper<Account>()
            .setSql("money = money - 200")
            .in("id", List.of(1, 2));

    accountMapper.update(null, wrapper);
}

基于 LambdaWrapper 的查询

Lambda 类型的 Wrapper 目的是为了让我们尽量少写硬编码的代码,采用函数式编程的思想,使用方法代替字段的硬编码,底层可以通过反射获取方法对应字段。为了方便,这里只演示 LambdaQueryWrapper 的使用:

void testMybatisPlus() {
    /**
     * 示例sql:
     *  select id, name, money
     *  from account
     *  where name like ? and money >= ?
     */

    // 构建查询条件
    LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper<Account>()
            .select(Account::getId, Account::getName, Account::getMoney)
            .like(Account::getName, "o")
            .ge(Account::getMoney, 1000);

    // 查询
    List<Account> accounts = accountMapper.selectList(wrapper);
    accounts.forEach(System.out::println);
}

自定义 SQL

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

  1. 基于 Wrapper 构建 where 条件:

    void testMybatisPlus() {
        /**
         * 示例sql:
         *  update account
         *  set money = money - #{value}
         *  where id in
         *  <foreach collection="ids" separator="," item="id" open="(" close=")">
         *      #{id}
         *  </foreach>
         */
    
        // 更新条件
        List<Integer> ids = List.of(1, 2);
        int value = 200;
    
        // 构造where条件
        QueryWrapper<Account> wrapper = new QueryWrapper<Account>()
                .in("id", ids);
        // 调用自定义方法
        accountMapper.updateMoneyByIds(wrapper, value);
    }
  2. 在 mapper 方法参数中用 Param 注解声明 wrapper 变量名称(必须是 ew):

    @Mapper
    public interface AccountMapper extends BaseMapper<Account> {
    
        void updateMoneyByIds(@Param("ew") QueryWrapper<Account> wrapper, int value);
    
    }
  3. 在 xml 文件中手写 sql 剩下部分的语句,使用 ${ew.customSqlSegment} 进行 sql 拼接:

    <update id="updateMoneyByIds">
        update account
        set money = money - #{value}
        ${ew.customSqlSegment}
    </update>

Service 接口

IService 接口提供了一系列在 Service 层执行的单表 CRUD 方法,方便我们更高效地进行单表增删改查。使用的时候,基本的 Service 接口需要继承 ISerivce,然后实现类需要集成 ServiceImpl,并指定相关的泛型:

// service
public interface IAccountService extends IService<Account> {}

// impl
@Service
public class IAccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {}

// 这里的ServiceImpl泛型指定了AccountMapper,所以直接使用ServiceImpl自带的baseMapper也可以

接着使用的时候只需要调用 Service 层的对应方法即可:

void testService() {
	// 新增
    Account account = Account.builder().name("赵六").money(100).build();
    accountService.save(account);
    
    // 查询
    List<Account> accounts = accountService.listByIds(List.of(1, 2));
    accounts.forEach(System.out::println);
}

在 Service 层中,也可以使用 lambda 类型方法进行查询:

/**
 * 多条件复杂查询
 * @param account
 * @return
 */
@Override
public List<Account> queryAccounts(Account account) {
    // 构建条件
    return lambdaQuery()   // 三个参数,第一个参数等价于where当中的动态if条件,第二个参数是操作的数据库字段,第三个参数是实际的数据
            .like(account.getName() != null, Account::getName, account.getName())
            .eq(account.getMoney() != null, Account::getMoney, account.getMoney())
            .list();
}

扩展功能

静态工具

在处理多表分步查询的时候,有时候为了防止依赖的循环注入问题,可以使用静态工具 Db,结合对应表实体类的字节码文件可以在一个 Service 中操作另一张表:

/**
 * 根据员工id查询部门名称
 * @param id
 * @return
 */
@Override
public String queryEmpAndDept(Integer id) {
    // 查询员工
    Emp emp = getById(id);
    if (emp == null) {
        throw new RuntimeException("员工不存在");
    }

    // 多表查询
    Dept dept = Db.lambdaQuery(Dept.class).eq(Dept::getId, emp.getDeptId()).one();
    return dept.getName();
}

逻辑删除

MybatisPlus 提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改 CRUD 的语句。我们要做的就是在 application.yml 文件中配置逻辑删除的字段名称和值即可(删除时还是调用 remove 方法,不过此时需要保证对应的实体类有逻辑删除的字段,此时就会更改这个字段而不是实际删除):

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名,类型可以是boolean或integer
      logic-delete-value: 1	   # 逻辑已删除值(默认为1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为0)

不过,逻辑删除本身也有自己的问题,例如:

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

逻辑删除的另一种平替方案是把垃圾数据定时迁移到其他表中。

枚举处理器

当一个类中包含了枚举类型的时候,可以借助枚举处理器进行 Java 枚举类型到 mysql INT 类型自动转换。

  1. 在枚举类型中对应值添加 @EnumValue:

    @Getter
    public enum UserStatus {
        NORMAL(1, "正常"),
        FREEZE(2, "冻结");
        
        @EnumValue	// 枚举处理,实现类型的自动转换
        @JsonValue	// 指示到时候返回哪个值
        private final int value;
        private final String desc;
        
        UserStatus(int value, String desc) {
            this.value = value;
            this.desc = desc;
    }
  2. 在配置文件中配置全局枚举处理器:

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

JSON 处理器

在 mysql 中,存在 JSON 数据类型,而在 Java 实体类中,我们一般使用内部类来处理类似的 JSON 字段。但是,这就意味着我们需要进行 JSON 字符串到内部类的自动映射。我们可以使用 JSON 处理器来进行对应的操作:

@Data
@TableName(value = "user", autoResultMap = true)	// 需要开启自动属性映射
public class User {
    private Long id;
    
    private String username;
    
    @TableField(typeHandler = JacksonTypeHandler.class)	// 指定JSON处理器
    private UserInfo info;	// 这一个字段是JSON对应的内部类
}

插件功能

分页插件

MybatisPlus 提供了很多不同的插件供我们使用,其中,最常用的便是分页插件。MyBatisPlus 的分页插件 PaginationInnerInterceptor 提供了强大的分页功能,支持多种数据库,使得分页查询变得简单高效。

要使用这个插件需要先配置一个拦截器(这意味着 MybatisPlus 的分页插件功能是基于拦截器来制作的):

@Configuration
public class MybatisPlusConfig {

    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 添加分页插件
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        pageInterceptor.setMaxLimit(1000L); // 设置分页上限

        interceptor.addInnerInterceptor(pageInterceptor); // 如果配置多个插件, 切记分页最后添加
        // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
        return interceptor;
    }
}

接下来就可以使用分页插件了:

void testPageQuery() {
    // 准备数据
    int pageNo = 1, pageSize = 2;
    Page<Emp> page = Page.of(pageNo, pageSize);

    // 添加排序条件
    page.addOrder(OrderItem.desc("emp_date"));

    // 分页查询
    page = empService.page(page);
    // 可以使用lambdaQuery构造过滤条件
    // page = empService.lambdaQuery(...).page(page);

    // 解析数据
    long total = page.getTotal();
    List<Emp> emps = page.getRecords();

    System.out.println(total);
    emps.forEach(System.out::println);
}

实际开发中可以在 PageQueryDTO 中定义方法,将自身对象转换为 MyBatisPlus 中的 Page 对象;在 PageResult 中定义方法,将 MyBaitsPlus 中的 Page 对象转换为 VO 对象:

@Data
public class PageQueryDTO implements Serializable {
    // 属性
    private Integer pageNo = 1;		// 给默认值,防止前端没传数据,导致空指针
    private Integer pageSize = 5;
    private String sortBy;
    private Boolean isAsc = true;
    
    public <T> Page<T> toMpPage(OrderItem ... items) {
        // 分页条件
        Page<T> page = Page.of(pageNo, pageSize);
        // 排序条件
        if (sortBy != null && !sortBy.isEmpty()) {
            page.addOrder(isAsc == Boolean.TRUE ? OrderItem.asc(sortBy) : OrderItem.desc(sortBy));
        } else if (items != null) {
            // 默认排序
            page.addOrder(items);
        }
        return page;
    }

    public <T> Page<T> toMpPageDefaultSortByTime(String time) {
        // 根据时间字段排序
        return toMpPage(OrderItem.desc(time));
    }
}
@Data
public class PageResult<T> implements Serializable {

    private Long total;
    private List<T> records;

    public static <T> PageResult<T> of(Page<T> page) {
        PageResult<T> pageResult = new PageResult<>();

        pageResult.setTotal(page.getTotal());

        List<T> list = page.getRecords();
        if (list != null && !list.isEmpty()) {
            pageResult.setRecords(list);
        } else {
            pageResult.setRecords(Collections.emptyList());
        }

        return pageResult;
    }

}

这样一来,Service 层的分页代码只需要两行即可搞定:

/**
 * 员工分页查询
 * @param empPageQueryDTO
 * @return
 */
@Override
public PageResult<Emp> empPageQuery(EmpPageQueryDTO empPageQueryDTO) {
    // 得到分页查询Page对象
    Page<Emp> dtoPage = empPageQueryDTO.toMpPageDefaultSortByTime("emp_date");
    return PageResult.of(page(dtoPage));	// 注意这里需要调用page方法
}

文章作者: 热心市民灰灰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 热心市民灰灰 !
  目录