《SpringBoot 3:入门与应用实战》第 13 章 整合 MyBatis 使用 MyBatis-Plus 阅读笔记 40

《SpringBoot 3:入门与应用实战》第 13 章 整合 MyBatis 使用 MyBatis-Plus 阅读笔记 40

13.5 使用 MyBatis-Plus

MyBatis-Plus 作为 MyBatis 的增强框架,官方给出一个比较有趣的说法:MyBatis 与 MyBatis-Plus 好比《魂斗罗》里的比尔与兰斯,两者搭配效率翻倍。MyBatis-Plus 提供了强大的支撑,构筑了一些实用的特性,本节会列举一些日常使用频率比较高的特性,关于详细的功能特性使用可以参照 MyBatis-Plus 的官方文档。

13.5.1 CRUD 基础接口

MyBatis-Plus 提供的重要基础能力之一就是单表 CRUD 接口,由于 BaseMapper 接口中预先定义的方法足够多,使得在编写具体的业务模块时,单表的 CRUD 几乎不需要编写,继承 BaseMapper 接口后 Mapper 接口就可以自动拥有单表 CRUD 的能力。DepartmentMapper 继承了BaseMapper 后,它已经拥有了包含 insert、update、delete、selectOne、selectList、count 等操作单表数据的能力。

MyBatis-Plus 的 BaseMapper<T> 接口是其核心组件,封装了单表常用的 CRUD 操作。以下是 BaseMapper 中常用方法的分类表格说明:

📥 新增操作 (Insert)

方法签名 说明
int insert(T entity) 插入一条记录。返回受影响的行数。如果实体类主键未赋值,MP 会根据配置的主键策略(如雪花算法)自动生成并回写到实体对象中。

🗑️ 删除操作 (Delete)

方法签名 说明
int deleteById(Serializable id) 根据主键 ID 删除一条记录。
int deleteBatchIds(Collection<? extends Serializable> idList) 根据主键 ID 列表批量删除多条记录。
int deleteByMap(Map<String, Object> columnMap) 根据 Map 中的键值对作为条件(等值匹配)删除记录。
int delete(Wrapper<T> wrapper) 根据条件构造器(Wrapper)删除记录。如果不传 Wrapper 或 Wrapper 为空,会删除全表数据(需配合防全表删除插件使用)。

✏️ 修改操作 (Update)

方法签名 说明
int updateById(T entity) 根据主键 ID 更新记录。注意 :默认策略下,实体类中为 null 的字段不会被更新到数据库。
int update(T entity, Wrapper<T> updateWrapper) 根据条件构造器更新记录。entity 用于设置要更新的字段值,updateWrapper 用于设置 WHERE 条件。若 entity 为 null,则需通过 updateWrapper.set() 方法指定更新字段。

🔍 查询操作 (Select)

方法签名 说明
T selectById(Serializable id) 根据主键 ID 查询单条记录。
List<T> selectBatchIds(Collection<? extends Serializable> idList) 根据主键 ID 列表批量查询多条记录。
T selectOne(Wrapper<T> queryWrapper) 根据条件构造器查询单条记录。如果查询结果多于一条,会抛出异常。
List<T> selectList(Wrapper<T> queryWrapper) 根据条件构造器查询列表。如果 queryWrapper 为 null,则查询全表数据。
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper) 根据条件构造器查询列表,但结果封装为 Map。适用于只查询部分字段或进行统计查询的场景。
List<Object> selectObjs(Wrapper<T> queryWrapper) 根据条件构造器查询列表,但只返回结果集中每一行的第一个字段值。
Integer selectCount(Wrapper<T> queryWrapper) 根据条件构造器查询总记录数。
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper) 根据条件构造器进行分页查询。需要配合分页插件使用。
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, Wrapper<T> queryWrapper) 根据条件构造器进行分页查询,结果封装为 Map

💡 核心要点补充

  1. 条件构造器 (Wrapper)BaseMapper 中大部分高级查询和更新方法都依赖 Wrapper 条件构造器(如 QueryWrapperLambdaQueryWrapper)。它允许你以面向对象的方式链式构建 SQL 的 WHERE 条件、ORDER BY 排序等。
  2. 主键策略insert 方法会自动处理主键生成。默认使用雪花算法(ASSIGN_ID),也可以在实体类上通过 @TableId(type = IdType.AUTO) 指定为数据库自增。
  3. 逻辑删除 :如果配置了逻辑删除(@TableLogic),deleteById 等方法会自动转换为 UPDATE 语句,将逻辑删除字段的值置为已删除状态,而不是物理删除数据。
  4. 字段自动填充 :配合 @TableField(fill = FieldFill.INSERT) 等注解和 MetaObjectHandler 实现类,可以在插入或更新时自动填充创建时间、更新时间等字段。

13.5.2 Wrapper 机制

Wrapper 是 MyBatis-Plus 编程式查询、修改数据的重要特性。MyBatis-Plus 提供的 Wrapper 机制拥有对单表查询的灵活条件构造、投影查询、聚合查询等能力,而且根据操作的场景分为查询类 Wrapper 和更新类 Wrapper,使用方式基本一致。下面通过几个简单示例来了解查询类Wrapper 的使用。

1.简单使用

先通过一个简单的模糊查询来演示 Wrapper 的基本使用。代码使用 Wrappers 的静态方法 query,构造一个 Department 类的条件查询器,并模糊查询 name 属性包含"分公司"的数据。随后调用这个 /dept/test3 接口,浏览器中可以成功得到一条数据,证明条件查询已经生效。

2.Lambda 式使用

对于代码中 Wrapper 的使用,存在一个问题:当 Department 类的属性发生变化时,Wrapper 中的条件构造也需要相应地修改,但是 IDE 并不会感知到这些属性被使用。为了更易维护可能变化的实体类属性,MyBatis-Plus 提供了 LambdaWrapper,使用这种类型的 Wrapper 将属性的字符串变量改为 Lambda 表达式,以此实现代码的高可维护性。代码使用 lambdaQuery 方法,可以生成一个传入 Lambda 表达式的LambdaQueryWrapper,效果完全一致。

java 复制代码
package com.yangjunbo.springboot.mybatisplus;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/dept")
public class DepartmentController {
    
    @Autowired
    private DepartmentService departmentService;
    
    @GetMapping("/test1")
    public List<Department> test1() {
        return departmentService.list();
    }
    
    @GetMapping("/test2")
    public Long test2() {
        return departmentService.count();
    }

    @GetMapping("/test3")
    public List<Department> test3() {
        return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
    }

    @GetMapping("/test4")
    public List<Department> test4() {
        return departmentService.list(Wrappers.lambdaQuery(Department.class)
                .like(Department::getName, "分公司"));
    }

}

3.投影查询

如果一个表的列特别多,而只需要查询其中的几列数据时,可以使用投影查询,即通过指定需要查询的列来达到节省数据库流量带宽的目的。代码中指定了 Department 类的查询结果中只提取 id 和 name,其他属性都不需要。编写完成后重新编译代码,并在浏览器中访问 /dept/test5 接口,发现返回的数据中 pid 和 del 属性都为 null,达到了预期目标。

java 复制代码
package com.yangjunbo.springboot.mybatisplus;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/dept")
public class DepartmentController {
    
    @Autowired
    private DepartmentService departmentService;
    
    @GetMapping("/test1")
    public List<Department> test1() {
        return departmentService.list();
    }
    
    @GetMapping("/test2")
    public Long test2() {
        return departmentService.count();
    }

    @GetMapping("/test3")
    public List<Department> test3() {
        return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
    }

    @GetMapping("/test4")
    public List<Department> test4() {
        return departmentService.list(Wrappers.lambdaQuery(Department.class)
                .like(Department::getName, "分公司"));
    }

    @GetMapping("/test5")
    public List<Department> test5() {
        return departmentService.list(Wrappers.lambdaQuery(Department.class).select(Department::getId, Department::getName));
    }

}

4.聚合查询

对于单表查询来讲,聚合查询也是一个常见的查询场景。虽然 MyBatis-Plus 没有对聚合函数提供 API 的定义,但是可以传入 SQL 片段来间接实现聚合查询。代码是一个查询 tbl_dept 表中最大 id 值的例子。需要注意的是,在使用聚合函数查询时无法使用 LambdaQueryWrapper 来实现,而只能使用普通的 QueryWrapper,也正是由于只能传入 SQL 片段进行查询,因此它的弊端与 1.简单使用 是一样的,都要考虑后期数据库列名发生变动所带来的影响。

java 复制代码
package com.yangjunbo.springboot.mybatisplus;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/dept")
public class DepartmentController {
    
    @Autowired
    private DepartmentService departmentService;
    
    @GetMapping("/test1")
    public List<Department> test1() {
        return departmentService.list();
    }
    
    @GetMapping("/test2")
    public Long test2() {
        return departmentService.count();
    }

    @GetMapping("/test3")
    public List<Department> test3() {
        return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
    }

    @GetMapping("/test4")
    public List<Department> test4() {
        return departmentService.list(Wrappers.lambdaQuery(Department.class)
                .like(Department::getName, "分公司"));
    }

    @GetMapping("/test5")
    public List<Department> test5() {
        return departmentService.list(Wrappers.lambdaQuery(Department.class).select(Department::getId, Department::getName));
    }

    @GetMapping("/test6")
    public Department test6() {
        return departmentService.getOne(Wrappers.query(Department.class).select("max(id) as id"));
    }

}

5.更灵活的条件构造

在实际的项目开发中,可能存在一些条件构造的场景:只有满足 ××× 条件后,才构造 Wrapper 中的查询条件。代码中展示了构造 Wrapper 条件的两种条件判断,传统的条件判断需要使用 if 结构设计,而 Wrapper 的方法中提供了一系列扩展方法,这些方法的共性是第一个参数传入一个boolean 类型的值,当这个 boolean 值为 true 时当前构造的查询条件才会生效。换句话说,上下两种写法是等价的。

java 复制代码
package com.yangjunbo.springboot.mybatisplus;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/dept")
public class DepartmentController {
    
    @Autowired
    private DepartmentService departmentService;
    
    @GetMapping("/test1")
    public List<Department> test1() {
        return departmentService.list();
    }
    
    @GetMapping("/test2")
    public Long test2() {
        return departmentService.count();
    }

    @GetMapping("/test3")
    public List<Department> test3() {
        return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
    }

    @GetMapping("/test4")
    public List<Department> test4() {
        return departmentService.list(Wrappers.lambdaQuery(Department.class)
                .like(Department::getName, "分公司"));
    }

    @GetMapping("/test5")
    public List<Department> test5() {
        return departmentService.list(Wrappers.lambdaQuery(Department.class).select(Department::getId, Department::getName));
    }

    @GetMapping("/test6")
    public Department test6() {
        return departmentService.getOne(Wrappers.query(Department.class).select("max(id) as id"));
    }

    @GetMapping("/test7")
    public List<Department> test7(Department qo) {
        LambdaQueryWrapper<Department> wrapper = Wrappers.lambdaQuery(Department.class);
        if (qo.getId() != null) {
            wrapper.eq(Department::getId, qo.getId());
        }
        wrapper.like(StringUtils.hasText(qo.getName()), Department::getName, qo.getName());
        return departmentService.list(wrapper);
    }

}

13.5.3 主键策略与 ID 生成器

考虑到在项目开发中可能会用到的几种主键类型,MyBatis-Plus 给予了一些基础实现和配置。

  • AUTO:数据库主键自增。
  • ASSIGN_ID:雪花算法 ID。
  • ASSIGN_UUID:不带短横线的 UUID。
  • INPUT:程序手动设置的 ID(或配合序列填充,Oracle、SQL Server 等使用)。
  • NONE:逻辑主键,数据库表中没有定义主键。

默认情况下,MyBatis-Plus 使用的主键策略是使用了雪花算法的 ASSIGN_ID 策略。

13.5.4 逻辑删除

逻辑删除是代替 DML 语句中 delete(物理删除)的一种更适合项目开发的数据删除机制,通过设置一个特殊的标记位,将需要删除的数据设置为 "不可见"​,并在每次查询数据时只查询标记位数据值为 "可见" 的数据,这样的设计就是逻辑删除。MyBatis-Plus 实现逻辑删除非常简单,只需要两步。

1.application.yaml 中配置

MyBatis-Plus 在全局配置文件中提供了 3 个配置属性,分别是全局逻辑删除字段名、逻辑删除的数据标记值、没有逻辑删除的数据标记值。

yaml 复制代码
mybatis-plus.global-config.db-config.logic-delete-field=del
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

2.在实体类上标注 @TableLogic

在具体的实体类中,只需要在逻辑删除的属性上标注 @TableLogic,即可完成逻辑删除的使用示。如此标注后实现的效果主要有两个:(1) 在使用 QueryWrapper 查询数据时,发送到数据库的 SQL 中会追加一个条件,在本示例中追加的条件为 del=0;(2) 在使用 delete 系列方法删除数据时,发送的 SQL 会由原来的 delete from xxx where id=xxx 改为 update xxx set del=1 where id=xxx。

java 复制代码
@Data
@TableName("tbl_dept")
public class Department {
    
    @TableId(type = IdType.AUTO)
    private Integer id;
    // ......
    @TableLogic
    private Boolean del;
}

13.5.5 乐观锁插件

乐观锁是高并发下的控制手段,它假设多用户并发事务在处理时不会互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。换句话说,乐观锁希望一条即将被更新的数据没有被其他用户操作过。乐观锁的实现方式如下:

  • 给数据添加 version 属性;
  • 当查询数据时,把 version 数据一并输出;
  • 更新数据时,将查询的 version 数据值一并传入;
  • 执行 update/delete 语句时,在 where 条件中额外添加 version=? 语句;
  • 如果 version 数据与数据库中的不一致,则更新/删除失败。

1.注册乐观锁插件

乐观锁在 MyBatis-Plus 中的设计以插件 (Interceptor) 的形式实现,所以需要声明一个配置类,在其中注册一个 MyBatis-Plus 的插件OptimisticLockerInnerInterceptor。需要注意的是,MyBatis-Plus 提供的插件大多需要集成到 MybatisPlusInterceptor 中组合实现,而不是一个个地直接注册到 IOC 容器中。

java 复制代码
@Configuration(proxyBeanMethods = false)
public class MyBatisPlusConfiguration {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

2.实体类上添加 @Version

随后可以在实体类中声明一个 Integer 类型的字段 version,并标注 @Version。如此编写完毕后,在操作 tbl_dept 表的单表数据时,乐观锁就会介入处理。

java 复制代码
@Data
@TableName("tbl_dept")
public class Department {
    
    @TableId(type = IdType.AUTO)
    private Integer id;
    
    @Version // 此处使用乐观锁注解
    private Integer version;
    // ......
}

MyBatis-Plus 支持的乐观锁适用于以下数据类型:

int、long(基本数据类型的数值型)​;

Integer、Long(包装数据类型的数值型)​;

Date、Timestamp、LocalDateTime(时间戳型)​。

13.5.6 分页插件

分页查询是项目开发中非常常见的业务场景,对于 MyBatis 的分页插件而言,比较常见的是 PageHelper,MyBatis-Plus 已经考虑到了分页查询的场景,它提供了一个专门用于分页的插件,通过简单的配置就可以使用分页的特性。它的使用方式分为两步。

1.添加分页插件

13.5.5 节中提到 MyBatis-Plus 的插件大多需要集中组合到 MybatisPlusInterceptor 中,所以在之前代码的基础上再注册一个PaginationInnerInterceptor 即可。注意,每个数据库的分页语法不尽相同,所以在创建分页插件时需要指定数据库的方言。

java 复制代码
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加乐观锁插件
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    // 添加分页插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(new MySqlDialect()));
    return interceptor;
}

2.使用分页模型

MyBatis-Plus 的 ServiceImpl 中已经提供了一些单表查询的分页方法,它们的名称都以 page 开头,可以直接使用;除此之外编写的 SQL 语句也有需要分页的场景(如关联多表的分页查询)​,这种情况下编写的 Mapper 接口要遵循一定的规则,即分页查询的方法需要传入 IPage 对象,并返回 IPage 模型或 List 集合。代码提供了几个简单的示例。

java 复制代码
    /**
     * 使用IPage作为入参和返回值
     * @param query
     * @return
     */
    IPage<User> page(IPage<Department> query);
    
    /**
     * 使用集合作为返回值
     * @param query
     * @return
     */
    List<User> pageList(IPage<Department> query);
    
    /**
     * 使用IPage和其他参数共同作为入参
     * @param page
     * @param params
     * @return
     */
    IPage<User> pageParams(@Param("page") IPage<Department> page, @Param("params") Map<String, Object> params);

13.5.7 代码生成器

日常开发中不乏会编写很多相似度较高的代码,包括但不限于单表的 Mapper、Service、Controller 等代码,以及其中的 CRUD 代码等。为了方便项目开发,MyBatis-Plus 提供了一个代码生成器,利用这个代码生成器可以生成实体模型类、Mapper 接口、mapper.xml、Service、Controller 等代码。

使用代码生成器时,需要在引入 MyBatis-Plus 核心包的基础上,再依赖一个 mybatis-plus-generator 以及模板引擎 FreeMarker,导入的坐标如代码所示。

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.5</version>
</dependency>
 
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>

之后只需要编写简单的测试代码,指定代码生成器连接的数据源、生成代码的文件位置、代码所处的根包名和模块名以及需要生成的表名,如代码所示。由于编写的是一个普通的 main 方法驱动的类,因此直接运行 main 方法即可,最终生成的代码相对简洁,读者可以根据开发项目的需要自行制作基于 FreeMarker 的代码模板,并让 MyBatis-Plus 根据模板内容生成代码。

java 复制代码
public class CodeGenerator {
    
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/sring-dao?characterEncoding=utf8", "springboot", "123456")
            .globalConfig(builder -> {
                builder.author("LinkedBear") // 设置代码的author
                    .outputDir("D://codegenerator"); // 指定输出目录
            })
            .packageConfig(builder -> {
                builder.parent("com.linkedbear.springboot.mybatisplus") // 设置父包名
                    .moduleName("user") // 设置父包模块名
                    .pathInfo(Collections.singletonMap(OutputFile.xml, "D://codegenerator")); // 设置mapperXml生成路径
            })
            .strategyConfig(builder -> {
                builder.addInclude("tbl_user") // 设置需要生成的表名
                    .addTablePrefix("tbl_"); // 设置过滤表前缀
            })
            // 使用FreeMarker引擎模板,默认的是Velocity引擎模板
            .templateEngine(new FreeMarkerTemplateEngine()) 
            .execute();
    }
}

13.6 小结

MyBatis 是目前主流的数据层应用框架,它基于半自动 ORM 的思想,以 SQL 为中心操纵数据库。作为 MyBatis 的增强工具,MyBatis-Plus 使基于 MyBatis 的项目开发效率更高。

MyBatis 的常用机制包括配置属性设置、Mapper 接口与 mapper.xml 的动态 SQL 编写、缓存和插件的应用等,合理灵活地使用它可以大大提升开发效果。

MyBatis-Plus 在原有 MyBatis 的基础上进行功能特性的增强,而没有对原有特性进行修改和移除。MyBatis-Plus 提供的特性大多和实际的项目开发相关,可以使用 Wrapper 机制方便地进行单表 CRUD 操作,使用逻辑删除、乐观锁等插件完成功能字段的设计,此外它还提供了分页机制和提高开发效率的代码生成器。

相关推荐
INS_KF10 小时前
【编程笔记】成员函数中两个 const 的区别(const Data &getData() const;)
前端·javascript·笔记
tju新生代魔迷11 小时前
Verilog HDL 学习笔记(十三)| 第13章 编程语言接口(PLI)
笔记·学习
行百里er12 小时前
Gateway 上别硬塞 MVC:DeferredImportSelector 看菜下锅
spring boot·后端·开源
点心的游戏开发世界13 小时前
UnityShaderGraph滚动UV笔记
笔记·uv
瑞码空间13 小时前
git知识点黄金笔记
笔记·git·elasticsearch
黑马程序员毕设13 小时前
基于Java的医院药品管理系统的优化设计与实现
java·开发语言·spring boot·小程序·架构·课程设计·毕设
天若有情67314 小时前
Node+MySQL小型全栈笔记项目实战课程分享
数据库·笔记·mysql
xcl092514 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·spring boot·需求分析
wxwx_bscxy32215 小时前
基于springboot宠物领养系统的设计与实现
数据库·spring boot·后端·spring·宠物