欢迎光临
我们一直在努力

从 0 学 MyBatis-Plus:从建表到常用插件实战

前言 大家好,我是Chrisco,现在提供一篇MyBatisPlus的新手从0入门教程 ,新手可以跟着上手操作,学过的老手也能用来复习,希望大家点个关注,我会持续更新优质内容

从 0 学 MyBatis-Plus:从建表到常用插件实战

这是一篇可以照着敲一遍的入门教程。它不假设你用过 MyBatis-Plus,甚至不假设你写过原生 MyBatis,只要求你会写 Java、知道 Spring Boot 大概在做什么、机器上有数据库可以连。

本文基于 MyBatis-Plus 3.5.17(官方文档当前展示版本)、JDK 17、Spring Boot 3.x、MySQL 8 编写。若你的项目是 JDK 8,绝大多数代码可以直接用,仅分页插件等可选模块的依赖坐标不同,文中已单独标注。

准备好开始

适合谁读

你的情况这篇教程对你的价值
写得出 Java 类和方法,用过 Spring Boot 的基本注解 最合适。可以完整跟着做完,把 MyBatis-Plus 用进日常开发
写过原生 MyBatis,写过 XML 或注解 SQL 可以重点看条件构造器、分页、自动填充这几节,体会"省掉了什么"
还没写过 Java,或不了解关系型数据库和 SQL 建议先补 Java 基础与 SQL,再回来读,否则第 3 章之后会吃力

学完你能做到什么

把下面这份清单当成验收标准。全部打勾,说明你已经具备在真实项目里使用 MyBatis-Plus 的能力。

  • 能独立搭好一个 Spring Boot + MyBatis-Plus 的工程,知道该引哪个 starter

  • 面对一张新表,不写任何 SQL 就能完成增删改查

  • 能用 Wrapper 拼出带条件、排序、分页的查询,并知道哪些写法有注入风险

  • 能让创建时间、更新时间这类公共字段自动填充

  • 能用逻辑删除替代物理删除,并理解它带来的查询行为变化

  • 能用乐观锁解决并发覆盖问题,用枚举映射处理状态字段

  • 能用代码生成器一次性生成实体、Mapper、Service 和 Controller 骨架

整体学习路径

教程按下面的顺序推进,每一步都有可以自己验证的结果。建议边读边敲,不要只读不写。

  • 认识 MyBatis-Plus 的定位与它增强的部分

  • 准备一张带公共字段的用户表

  • 建工程、引依赖、写最小配置

  • 跑通第一个查询,建立信心

  • 掌握通用 CRUD、Service 层与条件构造器

  • 按需开启分页、自动填充、逻辑删除、乐观锁、枚举映射

  • 用代码生成器把重复劳动一次做掉

  • 认识 MyBatis-Plus 是什么

    一句话:**MyBatis-Plus 是 MyBatis 的增强工具,只做增强、不做改变。**引入它之后,你原有的 MyBatis 写法(XML、注解 SQL、自定义 SQL)全部照常可用,同时多出来一整套"不用写 SQL"的能力。

    它解决的是开发中最枯燥的那部分工作:单表的增删改查、条件查询、分页、公共字段填充、逻辑删除、乐观锁。这些代码结构高度重复,手写既慢又容易出错,而 MyBatis-Plus 把它们变成了内置方法和注解。

    和原生 MyBatis 的差别

    要做的事只用原生 MyBatis用 MyBatis-Plus
    单表增删改查 每个方法写 SQL 或 XML 片段 Mapper 继承 BaseMapper,内置方法直接调用
    条件查询 在 XML 里堆 if 标签判断参数 条件构造器 Wrapper 链式拼装,字段可空时用重载参数跳过
    分页 自己写 limit,再写一条 count 语句 配置分页插件后,一次调用拿到数据和总数
    创建时间、更新时间 每个 insert 和 update 手动 set 注解标记字段,处理器自动填充
    删除数据 物理删除,数据无法找回 逻辑删除:删除变成更新标记位,查询自动过滤
    并发更新覆盖 自己写 version 条件判断 乐观锁插件加 @Version 注解
    生成模板代码 手抄或自己写脚本 代码生成器按表结构一键生成

    它在系统里的位置

    先建立这张整体图,后面每一节都只是往图上的某一段填内容。MyBatis-Plus 的增强集中在两处:一是 Mapper 层的通用方法,二是夹在 Mapper 与数据库之间的拦截器链。

    跟着做:从建表到第一个查询

    准备数据库

    为了让后面的每个特性都有地方用,这张表一次性加上状态、版本号、逻辑删除标记和两个时间字段。它们现在看着多余,学到第 8、9 节就会全部用上。

    CREATE DATABASE IF NOT EXISTS mp_demo DEFAULT CHARSET utf8mb4;

    USE mp_demo;

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name` VARCHAR(30) DEFAULT NULL COMMENT '姓名',
    `age` INT DEFAULT NULL COMMENT '年龄',
    `email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1 启用,0 禁用',
    `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
    `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0 未删除,1 已删除',
    `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
    `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

    INSERT INTO `user` (`name`, `age`, `email`) VALUES
    ('张三', 18, 'zhangsan@example.com'),
    ('李四', 20, 'lisi@example.com'),
    ('王五', 28, 'wangwu@example.com');

    执行完成后先确认一下数据:

    SELECT id, name, age, email, status, deleted FROM `user`;

    看到 3 条记录即准备完成。注意此时 create_time、update_time 都是 NULL,这是故意的:稍后要观察自动填充是否生效。

    创建工程与引入依赖

    用 IDEA 的 Spring Initializr 建一个空工程即可,需要勾选的只有 Spring Web、MySQL Driver 和 Lombok(Lombok 可选,不习惯就用 IDE 生成 getter/setter)。关键在依赖坐标:Spring Boot 版本不同,starter 名字也不同,引错了会在启动时报找不到类。

    你的环境应该引入的依赖
    Spring Boot 2.x mybatis-plus-boot-starter
    Spring Boot 3.x mybatis-plus-spring-boot3-starter
    Spring Boot 4.x(3.5.13 起支持) mybatis-plus-spring-boot4-starter
    非 Spring Boot 的 Spring 项目 mybatis-plus,并按需自行引入 mybatis-spring

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

    <!– 分页、乐观锁、防全表更新等插件依赖它,3.5.9 起需要单独引入 –>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
    <version>3.5.17</version>
    </dependency>

    <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
    </dependency>

    <!– 可选:用 Lombok 省掉 getter/setter –>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>

    引入 MyBatis-Plus 后,不要再引入 mybatis、mybatis-spring-boot-starter 或 mybatis-spring,版本差异会导致难以排查的冲突。

    3.5.9 起插件模块被拆成可选依赖。没引 mybatis-plus-jsqlparser,分页插件的类根本找不到,这是升级后最常见的报错。

    如果你的项目还在 JDK 8,把 jsqlparser 那一项换成下面这个坐标,它对应不支持 JDK 8 的 jsqlparser 5.x 之前的版本:

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

    依赖变多之后建议用官方 BOM 统一版本:引入 mybatis-plus-bom(3.5.9 及以上版本)到 dependencyManagement,后面各模块就不用再写 version 了。

    完成最小配置

    Spring Boot 会自动装配 MyBatis-Plus,你只需要告诉它数据库在哪,以及在哪里找 Mapper 接口。

    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mp_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

    mybatis-plus:
    configuration:
    # 数据库的 user_name 自动映射到实体的 userName,强烈建议开启
    map-underscore-to-camel-case: true
    # 学习阶段把执行的 SQL 打到控制台,上线前记得关掉或换成日志框架
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    global-config:
    banner: false
    db-config:
    id-type: auto

    @SpringBootApplication
    @MapperScan("com.example.demo.mapper")
    public class Application {
    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }

    注意第二行的 @MapperScan,参数是 Mapper 接口所在的包名。忘了写它,启动时不会报错,但运行时调用 Mapper 会抛出 “Invalid bound statement (not found)”,这个坑后面还会在速查表里出现。

    跑通第一个查询

    先写实体类和 Mapper 接口。实体类用注解把 Java 字段和数据库列对应起来,Mapper 接口只需要继承 BaseMapper,一行方法都不用写。

    @Data
    @TableName("user")
    public class User {

    /** 主键,数据库自增 */
    @TableId(type = IdType.AUTO)
    private Long id;

    private String name;
    private Integer age;
    private String email;

    /** 状态,用枚举映射数据库的 status 字段 */
    @EnumValue
    private StatusEnum status;

    /** 乐观锁版本号 */
    @Version
    private Integer version;

    /** 逻辑删除标记 */
    @TableLogic
    private Integer deleted;

    /** 插入时自动填充 */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /** 插入和更新时自动填充 */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    }

    public interface UserMapper extends BaseMapper<User> {
    }

    注解里已经提前埋了枚举、乐观锁、逻辑删除和自动填充的写法,暂时不影响查询,后面会逐个解释。现在先写测试验证链路是否打通:

    @SpringBootTest
    class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void selectAll() {
    List<User> users = userMapper.selectList(null);
    users.forEach(System.out::println);
    Assertions.assertEquals(3, users.size());
    }
    }

    selectList 的参数是条件构造器,传 null 表示查询全部。

    **第一处检查点:**控制台打印出三条 User、断言通过。如果报 “Invalid bound statement”,先检查 @MapperScan 的包名写对没有。

    核心能力逐个击破

    不写 SQL 的通用 CRUD

    BaseMapper 已经内置了单表所需的全部常用操作。下面这张表按使用频率排列,日常开发中八成以上的数据库操作都在这里。

    方法作用与说明
    insert(entity) 插入一条记录,返回影响行数;主键会回写到实体对象里
    deleteById(id) 按主键删除。配了逻辑删除后,实际执行的是 update
    delete(wrapper) 按条件删除,条件由 Wrapper 提供
    updateById(entity) 按主键更新,只更新实体中非 null 的字段
    update(entity, wrapper) 按条件更新,实体设置新值,Wrapper 设置 where 条件
    selectById(id) 按主键查询单条
    selectOne(wrapper) 按条件查询单条,结果超过一条会抛异常
    exists(wrapper) 判断满足条件的数据是否存在,比 count 更直观
    selectList(wrapper) 按条件查询列表,参数为 null 即查全部
    selectCount(wrapper) 按条件统计总数
    selectPage(page, wrapper) 分页查询,需要先配置分页插件
    selectMaps / selectObjs 直接返回 Map 或首个字段的值,适合只取部分列的场合

    @Test
    void crud() {
    // 插入:主键回写到 user 对象
    User user = new User();
    user.setName("赵六");
    user.setAge(25);
    user.setEmail("zhaoliu@example.com");
    userMapper.insert(user);
    System.out.println("新生成的主键:" + user.getId());

    // 查询单条
    User found = userMapper.selectById(user.getId());
    System.out.println(found);

    // 更新:只更新 name,其他字段保持原值
    User update = new User();
    update.setId(user.getId());
    update.setName("赵六六");
    userMapper.updateById(update);

    // 统计
    Long total = userMapper.selectCount(null);
    System.out.println("总记录数:" + total);

    // 删除
    userMapper.deleteById(user.getId());
    }

    主键策略怎么选

    主键生成由 @TableId 的 type 属性控制,常用取值如下。不确定时,自增表用 AUTO,分布式或分库分表场景用 ASSIGN_ID。

    取值含义适用场景
    IdType.AUTO 数据库自增 单库单表,主键列是 AUTO_INCREMENT
    IdType.ASSIGN_ID 由框架分配,默认雪花算法 分布式、需要提前拿到 id 的场景
    IdType.ASSIGN_UUID 分配 UUID,适用于 String 主键 不需要有序主键的表
    IdType.INPUT 插入前由自己设置主键 主键来自外部系统或业务规则
    IdType.NONE 不指定,跟随全局配置 默认值,等价于交给全局 db-config 决定

    旧版本里的 ID_WORKER、UUID、ID_WORKER_STR 已经废弃,新代码统一用 ASSIGN_ID 或 ASSIGN_UUID 代替。

    Service 层与链式调用

    Mapper 已经够省事,但业务代码通常写在 Service 层。让 Service 继承 IService,可以再省掉一层转发代码,并且多出批量操作和链式查询。

    public interface UserService extends IService<User> {
    }

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

    Service 层方法名有清晰的前缀规律:get 查单条、list 查集合、page 分页、save 保存、remove 删除、update 更新、count 统计。

    方法作用
    save / saveBatch 插入一条或批量插入,可指定批次大小控制单次 SQL 体量
    saveOrUpdate 主键有值则更新,没有则插入
    getById / getOne 按主键或条件查单条
    list / listByIds 按条件或主键集合查列表
    page 分页查询,返回含 records 与 total 的分页对象
    count 按条件统计
    updateById / update 按主键或条件更新
    removeById / remove 按主键或条件删除

    @Autowired
    private UserService userService;

    @Test
    void serviceDemo() {
    // 批量插入
    List<User> list = List.of(buildUser("A"), buildUser("B"));
    userService.saveBatch(list, 500);

    // 链式查询:一行写完条件与结果
    List<User> adults = userService.lambdaQuery()
    .ge(User::getAge, 18)
    .orderByDesc(User::getAge)
    .list();

    // 链式更新
    userService.lambdaUpdate()
    .set(User::getEmail, "new@example.com")
    .eq(User::getName, "A")
    .update();

    // 单条查询
    User one = userService.getById(1L);
    System.out.println(one);
    }

    在工具类、静态方法里拿不到 Service 注入时,可以用官方提供的 Db Kit 静态工具类直接执行 CRUD,用法与 IService 基本一致,详细用法见官方《持久层接口》文档。另外从 3.5.9 起官方把 service 模块重构为 repository,提供了语义更清晰的 IRepository,用法与 IService 相同,新项目可以按团队规范二选一。

    条件构造器 Wrapper

    Wrapper 是这套框架里最需要花时间熟悉的部分。它把 where 条件变成链式方法调用,让你在 Java 里拼 SQL,而不是去 XML 里堆 if。

    类用途
    QueryWrapper 查询条件,字段用字符串写数据库列名
    LambdaQueryWrapper 查询条件,字段用方法引用写,推荐优先使用
    UpdateWrapper 更新条件,可以只设置 where 而不传实体
    LambdaUpdateWrapper 更新条件的 lambda 版本

    为什么要强调 lambda 版本?看下面两行的区别就明白了。字符串写错字段名,编译期不会报错,运行时才炸;方法引用写错,IDE 立刻标红。

    // 普通写法:字段名写错要到运行时才发现
    QueryWrapper<User> q1 = new QueryWrapper<>();
    q1.eq("name", "张三").ge("age", 18);

    // lambda 写法:字段名写错编译不过,推荐
    LambdaQueryWrapper<User> q2 = Wrappers.lambdaQuery();
    q2.eq(User::getName, "张三").ge(User::getAge, 18);

    常用条件方法

    方法生成的 SQL 片段
    eq / ne column = value / column <> value
    gt / ge / lt / le 大于 / 大于等于 / 小于 / 小于等于
    between / notBetween column BETWEEN v1 AND v2
    like / notLike column LIKE ‘%value%’
    likeLeft / likeRight 以 value 结尾 / 以 value 开头
    isNull / isNotNull column IS NULL / IS NOT NULL
    in / notIn column IN (…),集合为空时该条件自动跳过
    groupBy / having 分组与分组后过滤
    orderByAsc / orderByDesc 升序 / 降序排序
    select 指定查询列,避免 select *
    and / or / nested 组合条件,nested 用于给一组条件加括号
    apply / last / exists 拼接自定义 SQL 片段,能力最强也最危险
    set / setSql 更新场景下指定要改的字段和新值

    两个必须掌握的习惯

    **习惯一:用 condition 参数让条件可选。**几乎所有方法都有带 boolean 的重载,第一个参数为 false 时整个条件不会拼进 SQL。这样就不用写一堆 if,也不会有动态 SQL 的拼接烦恼。

    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
    wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
    .ge(age != null, User::getAge, age)
    .in(CollectionUtils.isNotEmpty(ids), User::getId, ids);

    这里的语义是:只有 name 不为空、age 不为 null、ids 集合非空,对应条件才会生效。集合或 Map 参数为空时,相关条件也会被自动忽略。

    **习惯二:or 一定要跟括号一起用。**不加 nested,or 会把前面的条件也一起"或"掉,这是业务查询结果诡异的常见原因。

    // 错误:实际 SQL 是 status = 1 AND name = '张三' OR age = 30
    wrapper.eq(User::getStatus, 1).eq(User::getName, "张三").or().eq(User::getAge, 30);

    // 正确:用 nested 把"或者"的部分包起来
    wrapper.eq(User::getStatus, 1)
    .and(w -> w.eq(User::getName, "张三").or().eq(User::getAge, 30));
    // 生成:status = 1 AND (name = '张三' OR age = 30)

    **安全红线:**apply、last,以及排序字段、字段名这些位置,绝不要把前端传入的字符串直接拼进去。Wrapper 的参数绑定机制只保护"值",不保护"SQL 片段"。涉及字段名、排序字段时务必做白名单校验,否则就是标准的 SQL 注入入口。

    Wrapper 是查询构造工具,不是数据传输对象。不要把它当作接口参数在服务之间传递,正确做法是定义 DTO 传业务参数,由被调用方自己构造 Wrapper。

    分页插件

    分页是本教程第一个真正需要"配置"的能力。它靠拦截器改写 SQL 实现:你在代码里写普通查询,插件帮你加上 limit,并自动生成 count 语句。

    @Configuration
    @MapperScan("com.example.demo.mapper")
    public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 配置多个插件时,分页插件要放在最后
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
    }
    }

    @Test
    void pageDemo() {
    Page<User> page = new Page<>(1, 2); // 第 1 页,每页 2 条
    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
    wrapper.ge(User::getAge, 18).orderByDesc(User::getId);

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

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

    Page 对象的主要属性:

    属性含义
    current / size 当前页与每页条数,size 默认 10
    total / pages 总记录数与总页数
    records 当前页的数据列表
    orders 排序信息
    searchCount 是否执行 count 查询,设为 false 可省一次查询
    optimizeCountSql 是否自动优化 count 语句,默认开启
    maxLimit 单页条数上限,防止前端传入超大 size

    分页最容易踩的三个坑

  • **分页完全不生效,返回全部数据。**先看有没有注册 PaginationInnerInterceptor;3.5.9 之后还要看有没有引入 mybatis-plus-jsqlparser。

  • **count 结果不准。**多个插件共存时,分页插件必须放在拦截器链的最后。另外带 left join 的 SQL 建议给表加别名,否则自动优化 count 语句时可能把参与条件的表优化掉。

  • **排序字段被前端控制导致注入。**Page 的 orders 和 Wrapper 的排序方法都不要直接使用外部传入的列名,必须做白名单校验。

  • 如果需要在自定义 SQL 里分页,把 IPage 作为第一个参数传给 Mapper 方法即可,返回类型写 IPage 或 List 都可以,XML 里照常写你自己的查询语句。

    自动填充与逻辑删除

    这两个功能都属于"一旦用上就回不去"的类型,它们把最枯燥的重复代码消掉了。

    自动填充公共字段

    第一步,在实体字段上标记填充时机。第二步,实现 MetaObjectHandler 告诉框架填什么值。字段上的 fill 属性有三种常用取值:INSERT(仅插入)、UPDATE(仅更新)、INSERT_UPDATE(两者都填)。

    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
    // strictInsertFill 只在字段当前为 null 时填充,避免覆盖业务手动设置的值
    this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
    this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
    }

    真实项目里这两个值常常还要填当前登录人,同样在这个处理器里通过上下文获取即可。注意 strict 系列方法只在字段为空时填充;如果你的业务需要每次更新都强制覆盖时间,用非 strict 的重载。

    逻辑删除

    逻辑删除把 delete 变成 update。配置方式有两种,选一种就好:在实体字段上加 @TableLogic 注解,或者用全局配置统一指定字段名。

    mybatis-plus:
    global-config:
    db-config:
    logic-delete-field: deleted # 实体属性名,不是数据库列名
    logic-delete-value: 1 # 已删除,默认值就是 1
    logic-not-delete-value: 0 # 未删除,默认值就是 0

    配置生效后行为会发生变化,这是初学者最容易困惑的地方:

    你调用的方法实际执行的 SQL
    deleteById(1) UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
    selectById(1) SELECT … WHERE id = 1 AND deleted = 0
    selectList(null) SELECT … WHERE deleted = 0

    **别忘了插入时的初始值。**新插入的数据必须带上 deleted = 0,否则这条数据在所有查询里都"不存在"。三种常见处理方式:给数据库列设默认值(本教程建表语句已经这么做)、插入前手动 set、用自动填充。另外,如果你希望删除时也填充"删除人、删除时间",直接用 deleteById 配合自动填充即可,老版本里那些额外的注入器已经废弃。

    乐观锁与枚举映射

    乐观锁解决并发覆盖

    两个人同时打开同一条数据、先后保存,后保存的人会覆盖前一个人的修改。乐观锁用版本号解决这个问题:更新时带上版本条件,版本对不上就是 0 行受影响,你的代码自己决定是重试还是报错。

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    // 分页插件放在最后
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
    }

    // 1. 先查出来,拿到当前版本号
    User user = userMapper.selectById(1L);

    // 2. 改字段
    user.setEmail("new@example.com");

    // 3. 更新,SQL 会自动带上 version 条件
    int rows = userMapper.updateById(user);
    if (rows == 0) {
    // 说明这条数据在本次操作期间被别处改过,按业务决定重试或提示
    throw new IllegalStateException("数据已被修改,请刷新后重试");
    }
    // 更新成功后,新版本号会自动回写到 user 对象

    要点:支持 int、Integer、long、Long、Date、Timestamp、LocalDateTime 类型;newVersion 会自动回写到实体;只对 updateById、update(entity, wrapper)、saveOrUpdate 这类内置更新生效;同一个 wrapper 不要在多次更新中复用。

    枚举映射

    数据库里的 status 是数字,代码里希望是枚举,此时用 @EnumValue 标出"哪个属性对应数据库里存的值"即可,不需要写 TypeHandler。

    @Getter
    @AllArgsConstructor
    public enum StatusEnum {

    ENABLED(1, "启用"),
    DISABLED(0, "禁用");

    /** 这个值会被写进数据库 */
    @EnumValue
    private final int code;

    private final String desc;
    }

    实体里正常声明 StatusEnum status 字段即可,插入和查询都会自动在枚举与数字之间转换。如果枚举实现了 IEnum 接口,也是同样效果;官方目前推荐使用 @EnumValue 注解的方式。对于没有做任何声明的枚举,框架会退回 MyBatis 默认的按名称映射,结果往往不是你要的,因此状态类字段建议一律显式声明。

    代码生成器

    前面所有代码其实是高度模式化的。当项目里有几十张表时,应该用代码生成器一次性把实体、Mapper、Service、Controller 的骨架全部生成出来。

    先加两个依赖,starter 之外还需要生成器本体和一个模板引擎。生成器用到了模板引擎,但官方不强制绑定某一个,Velocity、Freemarker、Beetl、Enjoy 任选其一,下面以 Velocity 为例。

    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.17</version>
    </dependency>

    <dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
    </dependency>

    public static void main(String[] args) {
    FastAutoGenerator.create(
    "jdbc:mysql://localhost:3306/mp_demo?serverTimezone=Asia/Shanghai",
    "root",
    "123456")
    .globalConfig(builder -> builder
    .author("your-name")
    .outputDir("D://mp-demo/src/main/java")
    .disableOpenDir())
    .packageConfig(builder -> builder
    .parent("com.example.demo")
    .entity("entity")
    .mapper("mapper")
    .service("service")
    .serviceImpl("service.impl")
    .controller("controller"))
    .strategyConfig(builder -> builder
    .addInclude("user")
    .addTablePrefix("t_")
    .entityBuilder().enableLombok().enableTableFieldAnnotation()
    .controllerBuilder().enableRestStyle())
    .templateEngine(new VelocityTemplateEngine())
    .execute();
    }

    这套全新的代码生成器在 3.5.1 版本加入,与更早版本的 API 不兼容。生成之后请把代码当作自己项目的一部分来维护,别每次改表都重新生成覆盖业务逻辑。如果想让生成的实体带注释,MySQL 连接串上可以加 remarks 相关参数,具体以官方文档说明为准。

    常见坑速查

    下面这些是入门阶段最常遇到的报错与困惑,遇到问题时先来这里对一眼。

    现象原因解决
    Invalid bound statement (not found) Mapper 接口没有被扫描到 启动类加 @MapperScan 指向 Mapper 所在包,或给接口加 @Mapper
    分页不生效,查出全部数据 没有注册分页插件,或 3.5.9 之后没引 jsqlparser 注册 PaginationInnerInterceptor,并补上 mybatis-plus-jsqlparser 依赖
    找不到 PaginationInnerInterceptor 类 插件已从核心包拆出 引入 mybatis-plus-jsqlparser(JDK 8 用 mybatis-plus-jsqlparser-4.9)
    启动报 NoSuchMethodError 之类的方法缺失 同时引入了 mybatis 或 mybatis-spring 的旧版本 用依赖树排查并移除重复的 MyBatis 依赖
    查询结果字段为 null 下划线与驼峰没有自动映射 开启 map-underscore-to-camel-case,或用 @TableField 指定列名
    updateById 没有更新某些字段 实体中这些字段是 null,默认策略会忽略 null 用 UpdateWrapper.set 显式设置,或调整字段的更新策略
    deleteById 之后数据还在表里 逻辑删除在起作用,这是预期行为 确认这就是你要的效果;确实需要物理删除时写自定义 SQL
    新插入的数据查不到 逻辑删除字段没有初始值 给数据库列设默认值、插入前赋值,或使用自动填充
    更新误伤了整张表 Wrapper 忘了加 where 条件 加 BlockAttackInnerInterceptor 插件,让全表更新直接抛异常
    or 条件把其他条件也带偏了 缺少括号分组 用 nested 或 and(w -> …) 包住"或"的部分
    生产环境日志刷屏 log-impl 打开了控制台输出 上线前移除该配置,改用日志框架按需打印 SQL

    上线前强烈建议加上 BlockAttackInnerInterceptor(防全表更新与删除插件)。它会在没有 where 条件的 update 或 delete 执行前直接抛异常,属于低成本、高收益的保险。生产环境还可以进一步考虑非法 SQL 拦截、多租户、数据权限等插件。

    完成清单与下一步

    回头看一遍开头的清单,逐条确认:

    • 工程能启动,UserMapper 的查询返回了数据库里的真实数据

    • 不写 SQL 完成了 insert、updateById、deleteById、selectList

    • 能用 lambdaQuery() 或 Wrapper 组合条件,并知道 or 要加括号

    • 分页查询能拿到正确的 total 和 records

    • 新增数据时 createTime 与 updateTime 自动有值

    • 删除数据后表里仍有记录,但查询查不到

    • 并发更新时,旧版本号的更新返回 0 行

    • 状态字段在数据库里是数字,在代码里是枚举

    • 用代码生成器成功生成了一套 CRUD 骨架

    全部打勾之后,可以按兴趣往下走:

    • 多租户与数据权限插件:SaaS 场景下按租户或按角色自动给 SQL 加过滤条件

    • 动态表名插件:按时间分表后,让同一套代码读写不同月份的表

    • 流式查询与批量操作:数据量大时避免一次性把结果集读进内存

    • SQL 分析与执行日志:结合 p6spy 等工具观察真实执行的 SQL 与耗时

    • MyBatisX 插件:在 IDEA 里实现 Mapper 接口与 XML 之间的相互跳转

    最后一句经验之谈:MyBatis-Plus 的价值在于省掉重复劳动,而不是让你彻底远离 SQL。遇到多表关联、复杂聚合、性能敏感的场景,直接写 SQL 往往更清晰、更好优化,框架也完全允许你这么做。

    参考资料

    • MyBatis-Plus 官方文档 · 安装:starter 坐标、版本要求与 Maven BOM 说明

    • MyBatis-Plus 官方文档 · 快速开始:本文第 3 章的示例来源

    • 官方文档 · 条件构造器:全部 Wrapper 方法与生成 SQL 对照

    • 官方文档 · 持久层接口:IService、IRepository、链式调用与 Db Kit

    • 官方文档 · 注解配置:@TableName、@TableId、@TableLogic 等全部属性

    • 官方文档 · 使用配置:application.yml 中所有可配置项

    • 官方文档 · 分页插件与乐观锁插件:插件配置与注意事项

    赞(0)
    未经允许不得转载:171主机测评 » 从 0 学 MyBatis-Plus:从建表到常用插件实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址