Mybatis-Plus与MybatisX组合实战指南
作者:自我修炼的小石头
1. 项目概述:为什么我们需要MybatisX和Mybatis-Plus?
如果你和我一样,常年泡在Java后端开发里,尤其是和数据库打交道的业务层,那对MyBatis一定不会陌生。它把SQL和Java代码解耦的设计,给了我们很大的灵活性,但这份“自由”的代价就是,当项目规模起来后,那些无穷无尽的Mapper接口、XML文件、实体类,还有为了一个简单查询也要手写半天的重复代码,简直成了效率的“头号杀手”。我经历过一个老项目,光是Mapper XML文件就有上百个,找个字段映射都得靠全局搜索,更别提那些因为手误导致的字段名拼写错误,调试起来真是苦不堪言。
所以,当团队里有人开始用Mybatis-Plus和MybatisX时,我一开始是持观望态度的,觉得不过是些“奇技淫巧”。但真正上手后,才发现这俩组合拳,实实在在地把我们从繁琐的CRUD(增删改查)泥潭里拉了出来。简单来说, Mybatis-Plus是一个强大的MyBatis增强工具包 ,它内置了通用Mapper、分页插件、代码生成器等,让你能用极少的代码完成大部分数据操作。而 MybatisX则是一个专注于IntelliJ IDEA的插件 ,它通过智能提示、代码跳转、一键生成等功能,极大地提升了我们在IDE里编写MyBatis相关代码的体验和准确性。
它们解决的痛点非常明确: 消除重复劳动、降低出错率、统一开发规范、提升编码和调试效率 。无论是刚入门的新手,还是追求极致效率的老鸟,这套组合都能让你在数据库操作这一环上,跑得又快又稳。接下来,我就结合自己这几年在真实项目中的使用和踩坑经验,带你彻底搞懂怎么让它们成为你的开发利器。
2. 核心工具选型与定位解析
在开始深入之前,我们必须先厘清Mybatis-Plus和MybatisX各自的角色和边界。很多人容易混淆,以为它们是一个东西,或者功能重叠,其实不然。它们一个作用于运行时和代码层,一个作用于开发时的IDE环境,是相辅相成的关系。
2.1 Mybatis-Plus:运行时与代码层的“增强引擎”
你可以把Mybatis-Plus理解为MyBatis的一个“超级补丁包”或“框架层增强”。它通过继承、注解和内置插件的方式,对原生MyBatis进行了深度封装和扩展。它的核心价值不在于替代MyBatis,而是让使用MyBatis变得更简单。
它的核心定位是:
- 提供通用CRUD操作 :通过继承 BaseMapper 接口,你的实体类Mapper立刻拥有了数十个常用的单表操作方法,无需编写任何XML或注解SQL。
- 强大的条件构造器 :提供 QueryWrapper 、 UpdateWrapper 、 LambdaQueryWrapper 等,支持链式调用,以Java代码的方式安全、灵活地构建查询条件,有效防止SQL注入,也避免了XML中拼接SQL的混乱。
- 内置分页插件 :无需自己手写分页逻辑,配置一个分页插件(PaginationInterceptor),就能轻松实现物理分页,并且与条件构造器完美结合。
- 代码生成器(MyBatis-Plus Generator) :这是它的“大杀器”。可以根据数据库表结构,一键生成Entity、Mapper、Service、Controller等全套代码,极大提升项目初始搭建和后期表结构变更时的效率。
- 其他增强功能 :如乐观锁插件、SQL性能分析插件、多租户支持等,可以根据项目需要选择性启用。
注意 :Mybatis-Plus的强大也意味着一定的学习成本和“侵入性”。一旦决定使用,特别是代码生成器,就意味着你的项目结构会很大程度上遵循它的约定。在引入前,需要评估团队的技术栈和项目架构是否匹配。
2.2 MybatisX:开发阶段的“智能导航仪”
如果说Mybatis-Plus是武装你的代码,那么MybatisX就是武装你的IDE(特指IntelliJ IDEA和基于它的产品,如DataGrip)。它是一个纯粹的开发辅助插件,不参与任何运行时逻辑。
它的核心定位是:
- 智能代码提示与导航 :在Mapper接口的方法上,能直接提示并跳转到对应的XML中的SQL语句;在XML的
id上,也能跳回接口方法。这解决了MyBatis开发中最头疼的“找对应关系”问题。 - 一键生成代码 :在IDEA中,对着数据库表节点右键,可以直接通过MybatisX生成Entity、Mapper和XML文件,比使用Mybatis-Plus的代码生成器更加轻量和快捷,适合快速创建单个实体。
- SQL语句智能检测 :能对XML中的SQL进行简单的语法高亮和错误检测(如未使用的参数、结果映射问题等),虽然不如专业数据库客户端,但在编写阶段能避免一些低级错误。
- 图形化界面支持 :提供了数据库树形视图,可以直接在IDE内连接数据库,查看表结构,并进行上述的生成操作。
两者的关系与协作:
- Mybatis-Plus负责“做什么”和“怎么做” :它定义了数据操作的API和实现。
- MybatisX负责“更快、更准地写出来” :它帮助开发者快速创建和准确导航这些代码文件。
- 典型工作流 :使用MybatisX插件快速生成Entity和Mapper接口骨架 -> 在Mapper接口中继承Mybatis-Plus的
BaseMapper-> 利用MybatisX的跳转功能,在需要自定义复杂SQL时,快速定位并编写XML文件 -> 利用Mybatis-Plus的条件构造器在Java代码中完成简单查询。
理解这个分工,能帮助你在不同场景下选择正确的工具,或者将它们组合使用,达到效率最大化。
3. 环境准备与基础配置实战
理论说再多,不如动手配一遍。这里我以创建一个Spring Boot项目为例,演示如何一步步集成Mybatis-Plus并安装配置MybatisX插件。我会说明每个配置项的作用,以及我踩过的一些坑。
3.1 项目初始化与依赖引入
首先,通过Spring Initializr创建一个标准的Spring Boot项目,选择必要的依赖如Web、Lombok(强烈推荐,简化实体类)。然后,在 pom.xml 中引入Mybatis-Plus的Starter依赖。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version> <!-- 请使用当时最新稳定版 -->
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>关键点解析:
- 使用 mybatis-plus-boot-starter 而不是原生的 mybatis-spring-boot-starter 。这个Starter已经自动集成了MyBatis和Mybatis-Plus,无需再单独引入MyBatis依赖。
- 版本号尽量选择官方发布的最新稳定版,可以在中央仓库查看。版本不一致可能导致一些API不兼容。
- 数据库驱动根据你使用的数据库选择,这里以MySQL 8.x为例。
3.2 核心配置详解(application.yml)
接下来是配置文件,这里面的门道不少,配置得当能省去很多麻烦。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: yourpassword
mybatis-plus:
configuration:
# 控制台打印完整带参数SQL,开发环境必备,便于调试
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 开启驼峰命名自动映射。数据库字段 user_name 会自动映射到实体属性 userName
map-underscore-to-camel-case: true
global-config:
db-config:
# 全局主键类型。AUTO表示数据库自增,INPUT表示手动输入,ASSIGN_ID表示雪花算法,ASSIGN_UUID表示UUID
id-type: ASSIGN_ID
# 逻辑删除字段名(非物理删除)
logic-delete-field: isDeleted
# 逻辑已删除值(默认为 1)
logic-delete-value: 1
# 逻辑未删除值(默认为 0)
logic-not-delete-value: 0
# MyBatis的mapper.xml文件位置。如果和Mapper接口在同一个目录且同名,可省略。但建议统一放在resources下管理。
mapper-locations: classpath*:/mapper/**/*.xml配置经验与避坑指南:
- log-impl :开发阶段务必开启 StdOutImpl ,这样执行的SQL和参数会直接打印在控制台,是调试SQL最直接的方式。生产环境务必关闭或切换为文件日志。
- map-underscore-to-camel-case :建议始终设为 true 。这是Java属性命名(驼峰)和数据库字段命名(下划线)之间的标准映射约定,开启后能省去大量 @TableField 注解。
- id-type : ASSIGN_ID (雪花算法)是分布式场景下的首选,能保证全局唯一且有序。如果是单机小应用,用 AUTO (数据库自增)更简单。 这里有个大坑 :如果你的数据库主键是 BIGINT 类型,用 ASSIGN_ID 没问题;如果是 VARCHAR 或者你想用 UUID ,就需要设置为 ASSIGN_UUID ,并且实体类主键字段类型为 String 。
- 逻辑删除配置 :这是一个非常实用的功能。配置后,调用 deleteById 方法实际上执行的是 UPDATE table SET is_deleted = 1 WHERE id = ? 。而所有的 select 方法会自动加上 WHERE is_deleted = 0 条件。 注意 :一旦启用,你需要确保对应的数据库表有这个字段(如 is_deleted tinyint),并且初始值为0。
3.3 MybatisX插件安装与基础使用
打开你的IntelliJ IDEA,进入 File -> Settings -> Plugins ,在Marketplace中搜索“MybatisX”,找到后点击Install安装,重启IDEA即可。
安装成功后,你会看到几个明显变化:
- 在IDEA的右侧边栏,会出现一个蓝色的“Database”工具窗口(如果之前没有的话)。你可以在这里配置数据库连接。
- 在Mapper接口的方法名左侧,会出现一个绿色的箭头图标。点击这个箭头,可以直接跳转到XML中对应的SQL语句。
- 在XML文件的SQL语句 <select id="..."> 的id上,会出现一个蓝色的箭头图标。点击可以跳回Mapper接口。
第一个效率提升点 :连接你的开发数据库。在Database窗口点击“+”号,选择你的数据库类型,填入连接信息。连接成功后,你会看到所有的表和视图。这时,右键点击一张表,选择“MybatisX-Generator”,就会弹出代码生成对话框。你可以选择生成路径、实体类名、是否生成 Lombok 注解等。这是快速创建单个实体类和Mapper的最快方式,比运行代码生成器主程序要灵活方便得多。
4. Mybatis-Plus核心功能深度应用与避坑
配置好环境,我们终于可以深入Mybatis-Plus的核心功能了。我会按照从易到难的顺序,结合具体场景和代码,讲解如何正确、高效地使用它们。
4.1 实体类(Entity)与Mapper接口构建
假设我们有一张用户表 sys_user ,结构如下:
CREATE TABLE `sys_user` ( `id` bigint NOT NULL COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `age` int DEFAULT NULL COMMENT '年龄', `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` tinyint DEFAULT '0' COMMENT '逻辑删除', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
使用MybatisX生成或手动创建实体类 User.java :
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_user") // 指定表名,如果类名和表名遵循驼峰转下划线规则且一致,可省略
public class User {
@TableId(type = IdType.ASSIGN_ID) // 主键注解,类型为雪花算法
private Long id;
private String username;
private String email;
private Integer age;
private Integer status;
@TableField(fill = FieldFill.INSERT) // 插入时自动填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充
private LocalDateTime updateTime;
@TableLogic // 逻辑删除注解,配合全局配置使用
private Integer isDeleted;
}注解详解与避坑:
- @TableName : 实体类对应的表名。只有当你的类名(User)不能通过驼峰规则正确映射到表名(sys_user)时才需要。通常规则是 SysUser -> sys_user ,所以这里我们需要显式指定。
- @TableId : 这是最容易出错的地方之一 。 type 属性必须和你的全局配置及数据库主键类型匹配。如果配置了 id-type: ASSIGN_ID ,但数据库主键是 VARCHAR ,插入时会报类型转换错误。
- @TableField :
- value : 指定对应的数据库字段名,非必须,默认根据驼峰转换。
- exist : 标记是否为数据库表字段, false 表示不是,Mybatis-Plus会忽略它。常用于实体类中的辅助字段。
- fill : 自动填充功能的核心 。需要配合一个元对象处理器(MetaObjectHandler)使用,用于自动填充 createTime 、 updateTime 这类字段。后面会讲如何配置。
- @TableLogic : 逻辑删除标记。字段名和全局配置中的 logic-delete-field 要一致,或者注解里用 value 指定。
创建Mapper接口 UserMapper.java :
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper // Spring管理,也可在启动类用@MapperScan批量扫描
public interface UserMapper extends BaseMapper<User> {
// 此时,UserMapper已经拥有了BaseMapper提供的数十个通用方法,如:
// insert(T entity), deleteById(Serializable id), updateById(T entity),
// selectById(Serializable id), selectList(Wrapper<T> queryWrapper) 等
}关键点 :只需简单继承 BaseMapper<User> 并指定泛型为你的实体类,CRUD魔法就生效了。无需任何XML。
4.2 Service层封装的最佳实践
虽然可以直接注入Mapper使用,但按照分层架构,我们通常会有Service层。Mybatis-Plus也提供了通用的 IService 接口和 ServiceImpl 实现类。
创建 IUserService.java :
import com.baomidou.mybatisplus.extension.service.IService;
public interface IUserService extends IService<User> {
// 可以在此定义非通用的、业务相关的复杂查询方法
User getByUsername(String username);
}
创建 UserServiceImpl.java :
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public User getByUsername(String username) {
// 这里可以使用baseMapper(即UserMapper)进行更复杂的操作
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
return baseMapper.selectOne(wrapper);
// 更推荐使用Lambda方式,见下一节
}
}
这样做的好处 :
- ServiceImpl 已经实现了 IService 中所有的通用方法(批量操作、链式查询等),功能比直接使用 BaseMapper 更丰富。
- 保持了Service层的接口定义,便于后续扩展和Mock测试。
- 在Service实现类中,可以通过 this.getOne(wrapper) 或 this.list(wrapper) 调用通用方法,也可以通过 this.baseMapper 调用你自定义在 UserMapper 中的方法,非常灵活。
4.3 条件构造器(Wrapper)的灵活运用与Lambda表达式
条件构造器是Mybatis-Plus的灵魂功能之一,它让你用Java代码安全地构建查询条件。主要有 QueryWrapper 、 UpdateWrapper 和它们的Lambda版本 LambdaQueryWrapper 、 LambdaUpdateWrapper 。
场景一:复杂查询
// 1. 传统QueryWrapper (容易写错字段名的字符串)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id", "username", "email") // 指定查询字段
.like("username", "张") // username like '%张%'
.between("age", 18, 30) // age between 18 and 30
.eq("status", 1) // status = 1
.isNull("email") // email is null
.orderByDesc("create_time") // 按创建时间倒序
.last("limit 10"); // 拼接SQL(谨慎使用!)
List<User> userList = userMapper.selectList(queryWrapper);
// 2. LambdaQueryWrapper (推荐!编译期安全,重构友好)
LambdaQueryWrapper<User> lambdaQuery = new LambdaQueryWrapper<>();
lambdaQuery.select(User::getId, User::getUsername, User::getEmail)
.like(User::getUsername, "张")
.between(User::getAge, 18, 30)
.eq(User::getStatus, 1)
.isNull(User::getEmail)
.orderByDesc(User::getCreateTime)
.last("limit 10");
List<User> userList2 = userMapper.selectList(lambdaQuery);
Lambda版本的优势 :使用 User::getUsername 方法引用,而不是字符串 "username" 。这样在IDE中可以有代码提示,并且当你重构实体类字段名时,这里的引用会自动更新,避免运行时错误。 强烈建议始终使用Lambda版本 。
场景二:更新操作
// 1. 根据ID更新整个实体
User user = new User();
user.setId(1L);
user.setEmail("new@email.com");
userMapper.updateById(user); // 更新id=1的记录的email字段
// 2. 使用UpdateWrapper进行条件更新
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("age", 25) // 设置age=25
.set("update_time", LocalDateTime.now()) // 设置更新时间
.eq("status", 0); // 条件:status=0
userMapper.update(null, updateWrapper); // 第一个参数为null表示不传入实体,所有set字段由wrapper指定
// 3. LambdaUpdateWrapper (更安全)
LambdaUpdateWrapper<User> lambdaUpdate = new LambdaUpdateWrapper<>();
lambdaUpdate.set(User::getAge, 25)
.set(User::getUpdateTime, LocalDateTime.now())
.eq(User::getStatus, 0);
userMapper.update(null, lambdaUpdate);
场景三:嵌套查询与OR条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.nested(qw -> qw.like(User::getUsername, "张").or().like(User::getEmail, "zhang")) // (username like '%张%' OR email like '%zhang%')
.and(qw -> qw.gt(User::getAge, 18).lt(User::getAge, 60)) // AND (age > 18 AND age < 60)
.orderByAsc(User::getAge);
List<User> list = userService.list(wrapper);
nested 方法用于构建一个括号内的子条件组,这在复杂逻辑中非常有用。
避坑指南 :
- last() 方法 :用于拼接任意SQL到语句末尾(如 limit , for update )。 极度危险! 因为它可能破坏SQL结构或引发SQL注入。除非绝对必要且参数安全,否则不要使用。分页请用内置的分页插件。
- apply() 方法 :用于拼接SQL片段(如 date_format(create_time, ‘%Y-%m-%d’) = ‘2023-10-01’ )。使用时也要注意参数安全,防止注入。
- 字段名 :在非Lambda版本中,字符串字段名容易拼写错误,且不会在编译期报错。务必使用Lambda版本。
4.4 分页插件配置与使用
Mybatis-Plus的分页插件需要显式配置为一个Spring Bean。
创建一个配置类 MybatisPlusConfig.java :
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 interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDbType(DbType.MYSQL); // 指定数据库类型,优化分页语句
paginationInnerInterceptor.setMaxLimit(1000L); // 设置单页最大记录数限制
paginationInnerInterceptor.setOverflow(true); // 当请求页数大于总页数时,返回首页(false时返回空)
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// 可以继续添加其他插件,如乐观锁插件
// interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
使用分页 :
// 方式一:使用Page对象
Page<User> page = new Page<>(1, 10); // 当前页=1, 每页大小=10
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1);
Page<User> resultPage = userMapper.selectPage(page, wrapper);
System.out.println("总记录数: " + resultPage.getTotal());
System.out.println("总页数: " + resultPage.getPages());
System.out.println("当前页数据: " + resultPage.getRecords());
// 方式二:在Service中调用page方法
IPage<User> pageParam = new Page<>(1, 10);
IPage<User> pageResult = userService.page(pageParam, wrapper);
重要提示 :分页查询会自动在执行的SQL后加上 LIMIT ?, ? 。你需要确保你的查询是单表查询或者简单的连接查询。对于非常复杂的多表关联查询,分页插件可能无法正确优化,会导致性能问题(先查询所有数据到内存再分页)。对于复杂SQL的分页,建议在XML中手写优化后的分页SQL。
4.5 自动填充(MetaObjectHandler)实现
我们之前在实体类中用 @TableField(fill = FieldFill.INSERT) 标记了 createTime 和 updateTime 字段,现在需要实现处理器来填充它们。
创建 MyMetaObjectHandler.java :
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component // 不要忘记加@Component注解,让Spring管理
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());
// 如果需要填充其他字段,如创建人ID,可以在这里添加
// this.strictInsertFill(metaObject, "createBy", Long.class, UserContext.getCurrentUserId());
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时填充
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
strictInsertFill vs setFieldValByName :
- strictInsertFill 是3.3.0之后推荐的方法。它会检查字段是否存在、类型是否匹配,并且只在字段值为空时才填充。更安全。
- setFieldValByName 是旧方法,会强制设置值,可能覆盖已有的值。
配置好后,当你调用 userMapper.insert(user) 时, createTime 和 updateTime 会自动设置为当前时间;调用 updateById 时, updateTime 会自动更新。这保证了数据审计字段的自动维护,无需在业务代码中手动设置。
5. MybatisX插件高效使用技巧
环境配好了,Mybatis-Plus也用熟了,现在让我们把MybatisX这个“导航仪”的功能发挥到极致。它不仅仅是跳转,更能预防错误、统一风格。
5.1 智能跳转与代码提示的实战价值
最基础也最常用的功能就是“跳转”。在Mapper接口的方法名上点击绿色箭头,直接定位到XML中的SQL。反过来,在XML的 <select id=”selectUserById”> 上点击蓝色箭头,跳回接口。这个看似简单的功能,在文件多、方法杂的项目里,能节省大量查找时间。
更强大的是它的 代码提示 。在XML文件中编写SQL时:
- 输入 < ,会自动提示 select , insert , update , delete 等标签。
- 在 parameterType 或 resultType 属性里输入时,能自动提示项目中所有的实体类。
- 在 #{} 或 ${} 中输入时,能提示对应参数对象的属性名。
我常用的一个技巧 :在编写动态SQL(如 <if test=”...”> )时,经常要写 userName != null and userName != ‘’ 。MybatisX能对 test 表达式中的属性名进行提示和校验,如果属性名写错了,会有警告提示,这能有效避免因拼写错误导致的运行时问题。
5.2 一键生成代码与模板定制
除了之前提到的从数据库表生成代码,MybatisX还支持从实体类生成Mapper和XML。
操作 :在编辑器中打开你的实体类文件(如 User.java ),按下 Alt + Enter (Windows/Linux)或 Option + Enter (Mac),选择“Generate MybatisX”,然后可以选择生成Mapper接口、XML文件,或者两者都生成。
模板定制(高级功能) :如果你对默认生成的代码风格不满意(比如想用 @Repository 注解而不是 @Mapper ,或者想统一添加某个注释头),可以修改MybatisX的代码模板。
- 打开 File -> Settings -> Editor -> File and Code Templates 。
- 找到 MybatisX 分组,里面有 Mapper.java , Mapper.xml , Entity.java 等模板。
- 你可以基于原有模板修改,使用预定义的变量如 ${NAME} (类名)、 ${TABLE_NAME} (表名)等。
例如,我习惯在生成的Mapper接口上加上 @Repository 注解,并添加一段作者和时间的注释。定制模板后,以后所有生成的代码都符合团队规范,省去了手动修改的麻烦。
5.3 SQL语句检测与快速修复
MybatisX内置了一个简单的SQL检查器。当你在XML中编写SQL时,它会在后台进行一些基础分析:
- 未使用的参数 :如果在 #{} 中引用了某个参数,但这个参数并没有传入(Mapper接口方法中没有对应的 @Param 或参数名不匹配),它会给出警告。
- 结果映射问题 :如果 resultMap 或 resultType 指定的类找不到,或者结果集中的字段在类中不存在,会有提示。
- SQL语法高亮 :对不同数据库的关键字、函数、字符串等进行高亮,虽然不是完整的语法校验,但能提升可读性。
一个实用场景 :当你重构了实体类,删除了一个字段,但XML的SQL里还在查询这个字段。MybatisX可能会在对应的 resultMap 处给出“无法解析属性”的警告,提醒你及时更新SQL,避免运行时抛出 Unknown column 或属性映射错误。
6. 高级特性与自定义扩展
当基础功能满足不了复杂业务时,就需要用到一些高级特性或进行自定义扩展。
6.1 自定义SQL与XML编写(应对复杂查询)
尽管Mybatis-Plus的条件构造器很强大,但面对多表复杂关联、嵌套查询、窗口函数等场景,还是需要手写SQL。这时,Mybatis-Plus和原生MyBatis是完美兼容的。
- 在Mapper接口中定义方法 :
public interface UserMapper extends BaseMapper<User> { // 自定义一个复杂查询方法 List<UserVO> selectUserWithRole(@Param("userName") String userName); } - 在对应的XML文件(如
UserMapper.xml)中编写SQL :<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.yourpackage.mapper.UserMapper"> <!-- 通用查询映射结果,可被其他语句引用 --> <resultMap id="BaseResultMap" type="com.yourpackage.entity.User"> <id column="id" property="id" /> <result column="username" property="username" /> <!-- ... 其他字段映射 --> </resultMap> <!-- 自定义复杂查询 --> <select id="selectUserWithRole" resultType="com.yourpackage.vo.UserVO"> SELECT u.id, u.username, u.email, r.role_name as roleName FROM sys_user u LEFT JOIN sys_user_role ur ON u.id = ur.user_id LEFT JOIN sys_role r ON ur.role_id = r.id <where> <if test="userName != null and userName != ''"> AND u.username LIKE CONCAT('%', #{userName}, '%') </if> </where> </select> </mapper>
关键点 : namespace 必须对应Mapper接口的全限定名, id 对应方法名。MybatisX的跳转功能在这里依然有效,可以让你在接口和XML间无缝切换。
6.2 多数据源配置简要指南
在微服务或复杂应用中,一个服务连接多个数据库很常见。Mybatis-Plus对多数据源有很好的支持,通常结合 dynamic-datasource-spring-boot-starter 这个组件使用。
核心步骤 :
- 引入依赖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.6.1</version> </dependency> - 修改配置文件,配置多个数据源:
spring: datasource: dynamic: primary: master # 设置默认数据源 strict: false # 是否严格匹配数据源,默认false。true时未匹配到指定数据源会报错,false则使用默认数据源 datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db_master username: root password: 123456 slave1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db_slave username: root password: 123456 - 在Service或Mapper方法上使用
@DS(“slave1”)注解来切换数据源。@Service @DS("slave1") // 整个类默认使用slave1数据源 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Override @DS("master") // 这个方法覆盖类注解,使用master数据源 public boolean save(User user) { return super.save(user); } }
注意事项 :多数据源事务处理比较复杂, @Transactional 注解在默认情况下可能只对默认数据源生效。对于跨库事务,需要引入分布式事务解决方案,如Seata,这超出了本文范围。
6.3 自定义全局注入器(以逻辑删除为例)
虽然Mybatis-Plus提供了逻辑删除全局配置,但有时我们需要更复杂的行为,比如不是简单的0和1,或者删除时需要记录操作人、时间。这时可以自定义 SqlInjector 。
不过,对于大多数逻辑删除需求,使用全局配置 logic-delete-value 和 logic-not-delete-value 配合 @TableLogic 注解已经足够。更常见的自定义需求可能是 自动注入某些通用查询条件 ,比如基于租户ID的多租户数据隔离。
多租户场景示例 :假设每个查询都需要自动加上 tenant_id = #{currentTenantId} 条件。
- 实现一个自定义的 TenantHandler ,实现 TenantLineHandler 接口。
- 在配置类中将此处理器设置到分页插件同级的 TenantLineInnerInterceptor 中。
- 在实体类中,需要隔离的表对应的字段上加上 @TableField(fill = ...) 或通过全局配置指定租户ID字段名。
这个过程相对复杂,需要仔细阅读官方文档。其核心思想是:Mybatis-Plus通过插件机制,在SQL解析阶段动态注入租户条件。这属于框架的高级用法,在引入前要充分测试,确保对性能和新手开发者的理解没有过大影响。
7. 性能调优、常见问题与排查实录
工具用得好,也要用得巧。在高并发、大数据量场景下,一些不当的使用会导致性能问题。这里分享一些实战中积累的调优经验和常见坑位。
7.1 性能调优要点
- 避免在循环中调用单条查询/更新 :这是最常见的性能杀手。比如根据一个ID列表查询用户信息,应该用 userService.listByIds(idList) 批量查询,或者在Mapper中自定义一个 selectBatchIds 的XML方法,使用 WHERE id IN (...) ,而不是在Java循环里调用 userService.getById(id) 。
- 谨慎使用 select * :Mybatis-Plus的默认查询是 SELECT * 。对于宽表(字段很多),而业务只需要其中几个字段的情况,会造成大量的网络IO和内存浪费。务必使用 queryWrapper.select(“col1”, “col2”) 或Lambda版本的 select(Entity::getCol1, Entity::getCol2) 来指定查询列。
- 索引失效警告 :条件构造器生成的SQL,其条件顺序就是你调用wrapper方法的顺序。例如 wrapper.eq(‘status’, 1).like(‘username’, ‘A’) 会生成 WHERE status = 1 AND username LIKE ‘%A%’ 。如果 username 有索引而 status 没有,且 status=1 的数据量很大,这个查询可能很慢。需要根据数据库索引情况,合理安排查询条件的顺序,或者为 status 字段加索引。
- 分页查询优化 :对于超大数据量的分页, LIMIT 1000000, 10 这种深度分页效率极低。优化方案包括:
- 使用覆盖索引 :让查询只扫描索引,不回表。
- 记录上次查询的最大ID : WHERE id > last_max_id LIMIT 10 ,适合有序ID的连续翻页。
- 业务上限制查询范围 :如只允许查询最近3个月的数据。
- XML中大量 <if> 标签 :动态SQL虽然灵活,但过多的 <if> 判断会影响SQL解析性能,且生成的SQL可能无法有效利用数据库的查询缓存。对于查询条件固定的高频接口,考虑拆分成多个明确的查询方法。
7.2 常见问题排查清单
下面这个表格是我和团队在开发中遇到的一些典型问题及解决方案,你可以当作一个速查手册。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 插入/更新时,字段值为null,但数据库有默认值,结果未生效 | Mybatis-Plus的默认策略是,如果实体类字段为null,生成的SQL语句中该字段会被忽略(不插入)。 | 1. 检查实体类字段是否确实为null。 2. 在字段的 @TableField 注解中设置 insertStrategy = FieldStrategy.IGNORED 或 updateStrategy = FieldStrategy.IGNORED ,强制包含null值。但需谨慎,这可能覆盖数据库默认值。 3. 更推荐的做法:业务层设置好合理的初始值,而不是依赖数据库默认值。 |
| 逻辑删除失效,仍然能查到已删除数据 | 1. 全局配置 logic-delete-field 与实体类 @TableLogic 字段名不一致。 2. 实体类字段类型与配置的 logic-delete-value 类型不匹配(如字段是Integer,配置值是”1”字符串)。 3. 自定义的SQL语句中没有自动添加逻辑删除条件。 | 1. 核对配置文件 mybatis-plus.global-config.db-config 下的逻辑删除配置。 2. 核对实体类字段名和类型。 3. 重要 :逻辑删除条件是由Mybatis-Plus的插件自动追加到 BaseMapper 提供的方法生成的SQL中的。 如果你在XML中手写了自定义的 select 语句,逻辑删除条件不会自动追加! 需要在XML的 <where> 标签中手动加上 AND is_deleted = 0 。 |
| 使用LambdaQueryWrapper时报错:找不到getter方法 | 实体类没有正确生成getter/setter方法,或者使用了Lombok但IDE没有启用注解处理器。 | 1. 检查实体类,确认字段有getter/setter(如果用手动生成的话)。 2. 如果使用Lombok,确保IDE安装了Lombok插件,并且在项目的构建配置中启用了注解处理(Annotation Processing)。 3. 清理项目并重新构建( mvn clean compile 或 Gradle对应命令)。 |
| 分页查询结果总条数(total)不对 | 1. 分页插件被其他插件(如某些监控插件)干扰,执行了多次count查询。 2. 自定义的count查询语句有误(如果使用了 @Select 注解自定义count语句)。 3. 查询条件中包含 GROUP BY ,导致count查询结果不是预期的行数。 | 1. 检查 MybatisPlusInterceptor 中插件的添加顺序,分页插件应最后添加。 2. 如果查询非常复杂,建议关闭分页插件的自动优化count查询( paginationInnerInterceptor.setOptimizeJoin(false) ),或者自己手动写一个高效的count查询SQL。 3. 对于分组查询,分页意义本身可能就不大,需要重新考虑业务设计。 |
| 控制台打印的SQL参数是 ? ,看不到具体值 | 日志配置问题。Mybatis-Plus的 log-impl: StdOutImpl 只打印SQL语句框架。 | 1. 这是正常现象, StdOutImpl 就是打印带占位符的SQL。 2. 如果想看到完整的、替换了参数值的SQL,可以开启MyBatis的日志级别为DEBUG,并在logback或log4j2配置中配置 %X 或 %replace 来显示参数。但注意,生产环境不要开启,有安全风险。调试时,更常用的方法是结合IDE的调试功能,查看运行时传入的参数。 |
| 启动报错: Invalid bound statement (not found) | MyBatis找不到Mapper接口对应的XML SQL语句。 | 1. 最常见原因 :XML文件没有放在 mapper-locations 配置的路径下,或者编译后没有被复制到classpath中。检查 target/classes 目录下是否有对应的XML文件。 2. Mapper接口的 namespace 与XML的 namespace 不匹配。 3. 方法名与XML中SQL的 id 不匹配。 4. 使用MybatisX检查 :如果Mapper接口方法左侧没有绿色箭头,或者点击箭头无法跳转,基本就是这个问题。 |
7.3 我踩过的一个“大坑”:字段映射与枚举类型
有一次线上出问题,发现用户状态字段总是保存不正确。排查后发现,数据库 status 字段是 TINYINT ,代码里用的是枚举 UserStatusEnum 。在实体类中,字段类型是 Integer ,我们手动在getter/setter里做了转换。问题出在,Mybatis-Plus的 UpdateWrapper 的 set 方法,传入的是一个明确的对象属性值。
当我们用 updateWrapper.set(“status”, UserStatusEnum.ACTIVE.getCode()) 时,由于 set 方法的参数是 Object ,它接收的是枚举实例 UserStatusEnum.ACTIVE ,而不是其 code 值。MyBatis在将枚举转换为JDBC类型时,默认使用 EnumTypeHandler ,它会保存枚举的 name() (字符串“ACTIVE”)到数据库,导致类型不匹配错误。
解决方案 :
- 为这个枚举类型自定义一个MyBatis的类型处理器( TypeHandler ),并注册到Mybatis-Plus配置中。
- 在实体类中,直接使用 Integer 类型字段,业务层处理枚举转换。这是更简单直接的做法。
- 如果非要使用枚举,在调用 set 方法时,务必传入 getCode() 后的整数值,而不是枚举实例本身。
这个坑让我深刻意识到,在使用框架的便捷功能时,一定要清楚它背后行为的边界条件,对于复杂类型(枚举、集合、自定义对象)的字段操作要格外小心。
到此这篇关于Mybatis-Plus与MybatisX组合实战指南的文章就介绍到这了,更多相关Mybatis-Plus与MybatisX组合内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
