
MyBatis Plus 接入项目实战从零到一的全流程记录做后端开发这几年我接手过不少老项目也从头搭过好几个新服务。说到 ORM 框架选型MyBatis 一直是国内团队的绝对主力但有一个绕不开的痛点单表 CRUD 你得手写一大堆 XML 或者注解 SQLMapper 接口里全是重复的insert、selectById、updateById模板方法。后来我把 MyBatis Plus 引入了项目情况立刻就不一样了——单表操作零 SQL复杂查询还能保留 MyBatis 的灵活性。如果你也正在琢磨怎么把 MyBatis Plus 加到自己的项目里这篇博文会是个非常实用的参考我会带你走一遍完整的接入流程从依赖引入、配置项设置到常见坑的排查顺便把几个容易踩雷的细节也一并说清楚。这篇文章适合谁刚接触 MyBatis Plus 的新手、正在做项目框架选型的技术负责人、或者已经在用但想优化现有配置的开发者。最基础的 Spring Boot 项目假设你有剩下的我尽量讲细一些每一步都给出可以直接照做的内容。1. MyBatis Plus 是什么以及为什么要用它1.1 和原生 MyBatis 的本质区别很多人在第一次接触 MyBatis Plus 时会有一个误解觉得它是一个全新的 ORM 框架要学一套新的东西。其实完全不是。MyBatis Plus 是一个 MyBatis 的增强工具它只做增强、不做改变底层走的还是 MyBatis 那套 SqlSession、Mapper 代理机制。打个比方原生 MyBatis 就像一把基础的工具刀能干所有活但每次用之前都得自己磨刀而 MyBatis Plus 相当于给你配好了各种专用刀具日常切菜拿起来就能用需要精细活儿的时候你依然可以把原来那把刀抽出来使。这种增强而非替代的设计思路带来一个非常实际的好处你项目里现有的 MyBatis 代码不用做任何改动XML 映射文件、自定义 SQL、复杂结果映射全部原样保留。MyBatis Plus 只是在你的 Mapper 接口继承BaseMapperT之后自动帮你实现了大部分单表操作方法。这让改造现有项目的成本降到最低也是我当初选择引入它的最重要原因。拿实际代码来对比一下。原生 MyBatis 写一个根据 ID 查询用户的逻辑你需要三步先在 Mapper 接口里声明方法然后在 XML 文件里写select语句最后在 Service 里调用。用了 MyBatis Plus 之后你的 Mapper 接口只需要继承BaseMapperUser然后直接调用userMapper.selectById(1L)就完事了。这种差距在小项目里可能感受不明显但当一个表有几十个字段、十几个常用查询条件时省下的时间和出错概率是非常可观的。1.2 核心能力速览MyBatis Plus 最吸引人的是几个内置能力掌握这几点之后你会立刻理解为什么它在国内 Java 社区这么流行通用 CRUD继承BaseMapperT后用现成的insert、deleteById、updateById、selectById、selectList等方法不需要写任何 SQL。条件构造器QueryWrapper、LambdaQueryWrapper让你用 Java 代码动态拼装查询条件替代 XML 里大量繁琐的if判断。分页插件一个PaginationInnerInterceptor搞定物理分页自动帮你包装 count 查询、生成 limit 语句。代码生成器基于数据库表结构一键生成 Entity、Mapper、Service、Controller 全套代码新项目起步速度提升明显。逻辑删除不用自己写UPDATE ... SET deleted 1 WHERE id ?配置好之后删除操作自动变成更新。自动填充create_time、update_time这类字段在 insert 和 update 时自动填值不用在每个业务代码里手动 set。这些能力不是孤立的它们组合起来能解决日常开发中 80% 以上的数据访问需求。剩下那些复杂的多表联查、子查询、分组统计等场景把 XML 映射文件捡起来写原生 SQL 就行。这就是 MyBatis Plus 的设计哲学——常规路径自动化复杂路径留给你不堵路。2. 环境准备与依赖引入2.1 版本选型别小看这一步引入任何依赖之前版本选型是第一件要做的事。MyBatis Plus 的版本历史上有过一次比较关键的分叉3.5.x 之前和之后包名、配置项、API 都有差异。我给的建议是新项目直接上 3.5.3 以上的稳定版我在多个项目里实测下来3.5.3 在 Spring Boot 2.x 和 3.x 下都能稳定工作而且修复了不少旧版本的分页和逻辑删除问题。后端框架版本也是一个需要配套考虑的因素。如果你用的是 Spring Boot 3.x要特别注意mybatis-plus-boot-starter的版本是否支持 jakarta 命名空间我自己踩过这个坑Spring Boot 3 要求 MyBatis Plus 至少 3.5.3低于这个版本会出现ClassNotFoundException: javax.servlet.*之类的报错。Spring Boot 2.x 的话3.4.x 和 3.5.x 都没问题。另外不要默认排除 MyBatis 本身的依赖。MyBatis Plus starter 里会传递依赖一份特定版本的 mybatis-spring如果你同时手动引入了其他版本的 mybatis可能出现莫名其妙的配置加载异常。最简单的办法只引入mybatis-plus-boot-starter让 Maven 传递依赖自己处理。2.2 Maven 依赖怎么加如果你用的是 Maven 管理项目在pom.xml的dependencies区域加入下面这行dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependencyGradle 项目对应的是implementation com.baomidou:mybatis-plus-boot-starter:3.5.7引入之后先去检查一下依赖树确认是不是正确的版本。Maven 项目在 IDEA 右侧的 Maven 面板里点一下依赖树搜索mybatis相关关键字看看有没有版本冲突。如果之前项目里已经单独加了mybatis-plus的依赖记住一个准则starter 和独立依赖二选一同时存在可能导致多余的配置加载日志会打印两遍 MyBatis Plus 的 banner虽然不一定报错但排查问题时容易混淆。依赖引入后下一步是把 Spring Boot 启动类或者配置类上加上MapperScan注解。这是一个非常关键的动作它告诉 MyBatis Plus 去哪里扫描 Mapper 接口。我习惯把扫描路径指定到具体的包而不是用com.example.**这种通配符写法因为扫描范围过大会把不该代理的接口也注册进去浪费启动时间偶尔还会引起循环依赖问题。SpringBootApplication MapperScan(com.example.project.mapper) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }还有一种方式是在每个 Mapper 接口上加Mapper注解效果等价但你要是和我一样有十几个甚至几十个 Mapper还是用MapperScan一把扫完比较省心。2.3 数据库驱动的配套引入MyBatis Plus 本身不提供数据库驱动这属于你项目的基础依赖。比如项目用的是 MySQL需要额外在 pom 里加dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencySpring Boot 2.x 的mysql-connector-j和 Spring Boot 3.x 的版本命名有区别老一点的版本叫mysql-connector-java从 8.0.31 之后改名为mysql-connector-j。如果你在用其他数据库比如 PostgreSQL、Oracle按对应驱动加上即可。这个环节容易出问题的不是驱动本身而是版本不匹配导致连不上数据库后面排查章节我会详细说。3. 配置文件与核心 Bean 设置3.1 application.yml 基本配置依赖引入之后配置文件是第二步。大部分应用用的是application.yml下面是 MyBatis Plus 的一组最常用配置我加了注释说明每个配置项的作用spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: # Mapper XML 文件的扫描路径对应 resources 下的 mapper 目录 mapper-locations: classpath*:/mapper/**/*.xml # 实体类的别名包XML 里写 resultType 时可以简写类名 type-aliases-package: com.example.project.entity configuration: # 下划线转驼峰默认就是 true列出来让你确认一下 map-underscore-to-camel-case: true # 打印 SQL 到控制台调试期开着上线前关闭 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 主键策略数据库自增 id-type: auto # 逻辑删除字段的全局配置 logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这些配置里面有两个细节我想重点强调。第一mapper-locations配置的路径必须跟你实际放置 XML 文件的目录一致否则运行时会出现Invalid bound statement (not found)异常这个异常是所有 MyBatis 开发者都见过的一张老面孔。第二map-underscore-to-camel-case默认确实是 true但很多老项目改造时影响过大不想全局修改的时候可以选择在 XML 里用resultMap手动映射但能使用全局配置的情况下没必要给自己找麻烦开着它数据库的user_name就能自动映射到实体的userName字段。3.2 分页插件的注册方式MyBatis Plus 的分页功能不会默认生效必须显式注册一个PaginationInnerInterceptor。这一步被很多人忽略了导致写完分页代码后发现 Page 对象返回了所有数据翻页根本没效果。正确的做法是定义一个配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 指定数据库类型为 MySQL interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这里有个细节MybatisPlusInterceptor这个 Bean 注册的是拦截器链以后如果需要加其他功能比如乐观锁插件OptimisticLockerInnerInterceptor、防全表更新插件BlockAttackInnerInterceptor都是在同一个拦截器链上继续addInnerInterceptor。我建议从第一天起就养成集中注册的习惯后续维护起来非常清晰。分页的用法也顺带说一句。Service 层调用 Mapper 的分页方法时传入一个PageT对象它会自动填充records、total、current、size等属性PageUser page new Page(currentPage, pageSize); userMapper.selectPage(page, new LambdaQueryWrapperUser() .eq(User::getStatus, 1) .orderByDesc(User::getCreateTime)); ListUser userList page.getRecords(); long total page.getTotal();直接把 page 对象返回给前端也行但很多人为了接口结构一致会再封装一层自己的 PageResult这个看你项目规范了。3.3 逻辑删除与自动填充逻辑删除属于那种不用的时候觉得多余用了之后回不去的功能。传统的物理删除是DELETE语句而逻辑删除会在每张表加一个deleted字段删除时执行UPDATE。MyBatis Plus 把它做成了全局配置就像上面配置文件里写的那样声明了logic-delete-field: deleted之后所有继承BaseMapper的 Mapper 的deleteById都会自动改写为更新语句普通查询也会自动追加WHERE deleted 0条件。这个功能在很多业务系统里几乎必用因为它保证了数据可追溯误删还能恢复。但是要记住逻辑删除只对 MyBatis Plus 自己生成的方法生效你手写的原生 SQL 是不受影响的。如果你在 XML 里写了DELETE FROM user WHERE id #{id}该物理删还是会物理删这点必须让团队所有人都知道否则就是个潜在的数据事故。自动填充的典型场景是create_time和update_time。你当然可以在插入和更新数据时手动 set但一旦某处代码忘了时间字段就变成 null等到排查数据问题时才追悔莫及。MyBatis Plus 的做法是先定义实体类字段的填充策略Data public class User { TableId(type IdType.AUTO) private Long id; private String username; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; private Integer deleted; }然后实现一个 MetaObjectHandler 来处理填充逻辑Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { 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()); } }注意strictInsertFill方法的一个行为特性如果实体对象的字段已经手动赋值它不会覆盖。这个设计其实挺合理因为你可以在某些特殊场景里强制指定 createTime比如数据迁移时保留原时间。如果你希望无论如何都填充当前时间那需要改成setFieldValByName但一般不建议这么干保留灵活性更好。4. 实体类、Mapper 与 Service 层的完整落地4.1 实体类的注解约定Map 好配置后就可以开始写实体类了。MyBatis Plus 的实体类除了是一张表的映射之外还需要用几个关键注解告诉框架如何映射到数据库表。第一个是TableName标注实体对应的表名。如果你的表名是t_user而实体类叫User就必须显式加这个注解否则 MyBatis Plus 会默认寻找user表运行时直接报错。第二个是TableId标注主键字段。如果主键字段名和实体属性名不一致比如数据库主键叫uid而实体里是id也需要显式指定TableId(value uid, type IdType.AUTO) private Long id;第三个是TableField用于非主键字段的映射和特殊行为标注。常见场景包括字段名不一致时用TableField(user_name)指定列名字段在数据库表中不存在时用TableField(exist false)标注比如有些临时计算字段不需要查询返回时用TableField(select false)注解比如大文本字段可能在列表页用不到。这里我给出一个完整的 User 实体示例Data TableName(t_user) public class User { TableId(value uid, type IdType.AUTO) private Long id; TableField(username) private String username; private String email; private Integer status; private Integer deleted; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableField(exist false) private String extraInfo; }Data注解是 Lombok 提供的项目里一般都会引入它能帮你自动生成 getter/setter、toString、equals 等方法。需要注意的是实体类不要手写带参构造器而省略了无参构造器MyBatis 做对象映射时依赖无参构造器要是写了个全字段构造器而不留空的运行时会报反序列化异常。4.2 Mapper 接口继承 BaseMapperMapper 层是 MyBatis Plus 最省心的地方。什么都不写继承BaseMapperT就拥有了完整的单表 CRUD 能力。以 User 为例Mapper public interface UserMapper extends BaseMapperUser { }然后直接在 Service 里注入并使用比如Autowired private UserMapper userMapper; // 插入 User user new User(); user.setUsername(zhangsan); user.setEmail(zhangsanexample.com); userMapper.insert(user); // 根据 ID 查询 User found userMapper.selectById(1L); // 条件查询 ListUser users userMapper.selectList(new LambdaQueryWrapperUser() .eq(User::getStatus, 1) .like(StringUtils.hasText(name), User::getUsername, name)); // 更新 User updateUser new User(); updateUser.setId(1L); updateUser.setEmail(newemailexample.com); userMapper.updateById(updateUser); // 删除逻辑删除配置下实为更新 userMapper.deleteById(1L);写业务比较多的同学可能注意到了LambdaQueryWrapper里面那个StringUtils.hasText(name)的写法很实用它表示只有 name 非空时才把这个条件拼进 SQL。这是条件构造器最核心的价值——动态 SQL 不再需要 XML 的if判断代码看起来干净得多。Maven 惯例中Maven 项目通常会分为 controller、service、mapper、entity 几个包这个结构 MyBatis Plus 是天然适配的。Service 层一般是继承IServiceT、实现类继承ServiceImplM, T这样一来连 Service 层的常用方法都帮你实现好了。public interface UserService extends IServiceUser { } Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { }这样一个 Service 就能直接用userService.save(user)、userService.getById(1L)、userService.list(new LambdaQueryWrapper())。IService 的封装比 BaseMapper 更贴近业务还带了saveBatch批量插入、updateChain链式更新等高级用法。我给团队定的规范就是Controller 调 ServiceService 调 MapperService 里按业务逻辑组合多个 Mapper 方法避免 Controller 直接操作数据库对象抵抗后期业务复杂化的压力。4.3 代码生成器新项目起飞神器提到 MyBatis Plus 就绕不开它的代码生成器。以前写一个模块的 CRUD 少说也要半天从建表到写 Entity、Mapper、Service、Controller 全是手工活。MyBatis Plus 的代码生成器可以根据数据库表结构把这一整套代码一次性生成出来而且生成的质量很高能直接跑通。老版本用的是AutoGenerator新版本推荐使用FastAutoGenerator。这里我给一个简化版的使用示例FastAutoGenerator.create(jdbc:mysql://localhost:3306/demo_db, root, password) .globalConfig(builder - builder.author(你的名字).outputDir(/Users/you/codegen/output)) .packageConfig(builder - builder.parent(com.example.project) .entity(entity) .mapper(mapper) .service(service) .serviceImpl(service.impl) .controller(controller)) .strategyConfig(builder - builder.addInclude(t_user) // 要生成的表 .entityBuilder().enableLombok() .controllerBuilder().enableRestStyle()) .execute();跑完之后指定输出目录里就会生成一套完整的 CRUD 代码。我自己用下来的体会是代码生成器和条件构造器是 MyBatis Plus 效率提升的最大两个功臣。但要注意生成的代码只是个起点复杂的业务逻辑还得自己手写别指望完全自动化尤其是不带enableRestStyle的话Controller 会生成很多传统风格的接口喜欢 RESTful 风格的需要提前把开关开好。5. 常见问题与排查技巧实录5.1 Mapper 的 XML 文件扫描不到现象启动成功但是一调用某个 Mapper 方法就报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。这个异常的本质是 MyBatis Plus 只在接口上注入了代理方法但找不到对应的 SQL 映射语句因为接口里手写的抽象方法和 XML 里的select/insert等没有对应上。排查顺序按下面几步来确认 XML 文件的确在 resources 目录下并且后缀是.xml。很多人把 XML 放到了java目录下默认构建时不会把它打包到 classpath。确认application.yml里mapper-locations指向的路径正确比如classpath*:/mapper/**/*.xml是匹配mapper目录及子目录下的所有 XML。确认 XML 文件里的 namespace 跟 Mapper 接口的全限定名一致。这一条极其重要namespace 写错是新手最常见的问题。确认接口方法和 XML 的 id 一致参数类型和返回类型也要匹配。有个细节是方法没有参数时 XML 里不要写parameterType写了反而可能报错。最后有一条容易被忽略在 Spring Boot 默认打包spring-boot-maven-plugin下resources 目录以外的文件不会被打进去如果你把 XML 放在源码目录又忘记在 pom 里配置resources路径也会出现这个异常。我不建议这么搞统一放 resources 目录下就好搞特殊化只会给团队添乱。5.2 分页失效数据全部返回现象调用selectPage后返回的records里是全部数据total也不对。这个问题的 95% 概率是分页插件没有成功注册。你需要回过去检查两件事一是PaginationInnerInterceptor是否加到了MybatisPlusInterceptor里二是这个配置类是否被 Spring 扫描到了。很多项目把配置类放在启动类同级的包之外Spring 默认扫描不到你就得把配置类挪进启动类所在包或者子包里或者在启动类上用ComponentScan显式指定。另一种可能的原因是你手写了拦截器覆盖了 MyBatis Plus 内部机制。比如自己实现了Interceptor接口并注册为 Bean或者定义了一个PaginationInterceptor旧版本方案而不是MybatisPlusInterceptor新版本方案。新旧版本的分页插件实现方式不一样混用会导致信息错乱。如果项目之前用的是老版本 MyBatis Plus从PaginationInterceptor迁移到新版本的MybatisPlusInterceptor时不要把它们俩同时注册只保留新写法即可。Druid 连接池之类的配置不会直接导致分页失效但如果你开启了 SQL 防火墙有些数据库方言的 limit 写法会被拦截这是另一个层面的问题跟 MyBatis Plus 无关排查时注意区分。5.3 主键生成策略踩坑现象插入数据后返回的主键 ID 是 null或者数据库报主键重复的错误。MyBatis Plus 默认的主键策略是ASSIGN_ID也就是基于雪花算法生成一个分布式 ID。如果你的数据库主键是自增的那就要把全局配置里id-type改为auto或者在实体的TableId注解上单独指定type IdType.AUTO。两种做法效果一样但全局配置适合新项目统一指定注解适合老项目里表结构差异较大的情况。另一个踩坑场景是用了ASSIGN_ID后ID 超出了数据库字段类型范围。雪花算法生成的 ID 是 19 位 long 类型数据库主键如果你建的是 int插入时直接 MySQL 报错Data truncation: Out of range value for column id。解决办法是把主键字段改为 bigint。这个话题跟 MyBatis Plus 配置无关但我见得太多了顺手提一句。5.4 自动填充时间字段不生效现象insert 或 update 之后时间字段依然为 null没有任何填充。先检查实体类上的TableField的fill值是不是配置对了INSERT和INSERT_UPDATE是两种不同策略别混。再检查MetaObjectHandler的实现类是否被 Spring 管理这个Component注解必须加不然框架找不到它。还有一个容易忽略的点严格方法strictInsertFill在实体的字段值已经被设置过的情况下不会覆盖。所以你如果在代码里把这些时间字段的 setter 都写了那填充逻辑就失效了。解决方式就是不要在业务代码里手动 set createTime 和 updateTime把这活全部交给 fill 机制做责任边界才清晰。5.5 实体字段和数据库列的映射问题现象查询返回的数据里某些字段是 null但数据库表里明明有值。先回想一下map-underscore-to-camel-case是不是开着。如果数据库列是user_name实体字段是userName只要打开这个开关MyBatis Plus 就能自动映射。但如果你的列名是大写或者有特殊符号比如USER_NAME自动映射也会失败这时就要用TableField(USER_NAME)显式指定列名。还有一种较隐蔽的情况使用了TableField(exist false)标注的字段MyBatis Plus 在生成 SQL 时会忽略它。也许前一个同事本来想标注临时字段结果后面代码重构了名字看似跟数据库列一样实际上永远不会被查询出来。遇到这种诡异问题时我一般会先去实体类里翻一遍看看有没有字段被exist false标注这能省掉很多盲猜的时间。6. 实操总结与团队落地建议最后把我个人接入 MyBatis Plus 项目的一些体会整理成几句话。我从最早的 MyBatis 时代写起XML 里贴 SQL 贴到手酸后来换到 MyBatis Plus单表操作确实轻便了很多但它不是你什么都不用学了。相反正因为单表 CRUD 变简单了你的精力要花在多表联查、索引优化、SQL 性能调优等更核心的地方。建议你接入 MyBatis Plus 之后把团队规范尽快定下来比如单表操作用条件构造器、复杂查询必须用 XML、逻辑删除字段统一命名、主键策略按数据库类型统一选择。这些规矩不在第一天定后面就会混乱。我给自己定的使用原则很简单能用LambdaQueryWrapper就用LambdaQueryWrapper它比QueryWrapper多一层编译期类型检查字段名写错了编译直接报错不会等到运行时才告诉你 unknown column。XML 文件尽量保留给复杂 SQL别为了省事把简单 SQL 也写进 XML那就失去了用 MyBatis Plus 的意义。如果你正准备在一个老项目里引入 MyBatis Plus建议先小步试水挑一张业务不复杂的表把实体、Mapper、Service 按上面的步骤接好跑通一个查询和一条更新再逐步扩展到其他模块。不用想着一次性把所有功能都用上分页和逻辑删除可以优先配置自动填充看表结构情况代码生成器等你熟悉了基本用法之后再用也不迟。这种渐进式接入的表现非常稳定我在这几个项目里都是这么操作的实测下来对业务开发速度的提升非常明显。