1. MyBatis-Plus入门:为什么选择它而不是原生MyBatis
我第一次接触MyBatis-Plus是在2018年接手一个电商后台项目时。当时团队正在为要不要引入这个"增强工具"争论不休——有人担心增加学习成本,有人认为会带来维护风险。但实际使用后,我们项目组的CRUD开发效率提升了近60%,特别是面对复杂业务表单时,原先需要2-3天完成的DAO层工作,现在半天就能搞定。
MyBatis-Plus(简称MP)本质上是对MyBatis的扩展封装,最新稳定版是3.5.17(截至2024年1月),与Spring Boot 2.7.x版本兼容性最佳。它的核心价值在于:通过内置通用Mapper和Service,让开发者用极简代码实现90%的数据库操作。举个例子,传统MyBatis查询用户列表需要:
// 原生MyBatis方式 @Select("SELECT * FROM user WHERE deleted=0") List<User> selectActiveUsers();而在MP中只需要:
// MyBatis-Plus方式 userService.lambdaQuery() .eq(User::getDeleted, 0) .list();这种改进不仅仅是代码量的减少。MP的lambda表达式写法在编译期就会检查实体类字段是否存在,避免了手写SQL字符串容易出现的字段名拼写错误。我曾在排查一个线上Bug时发现,团队之前用原生MyBatis时,因字段名拼错导致的SQL异常占数据库错误的37%,改用MP后这类错误直接归零。
2. 环境搭建与基础配置
2.1 依赖引入与版本匹配
当前最稳定的组合是:
- Spring Boot 2.7.18
- MyBatis-Plus 3.5.17
- MySQL 8.0.33
Maven配置示例:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.17</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>注意:Spring Boot 3.x用户需要使用MP的5.x版本,但截至2024年初,5.x的生态插件(如代码生成器)还不够完善,生产环境建议仍采用2.7.x+MP3.5.x的组合。
2.2 实体类注解详解
MP通过注解实现ORM映射,这是与JPA相似但更灵活的方式。以用户实体为例:
@Data @TableName("sys_user") // 实际表名 public class User { @TableId(type = IdType.AUTO) // 自增主键 private Long id; @TableField("username") // 字段映射 private String name; @TableField(exist = false) // 非表字段 private String tempToken; @Version // 乐观锁版本字段 private Integer version; }踩坑提醒:当你的字段名是数据库关键字时(如order、desc),必须使用反引号转义:
@TableField("`order`") private Integer order;3. 核心CRUD操作实战
3.1 插入操作的三种姿势
基础插入:
User user = new User(); user.setName("张三"); userMapper.insert(user); // 返回影响行数批量插入优化:MP默认的批量插入其实是循环单条插入,真正的高效批量插入需要配置:
# application.yml mybatis-plus: global-config: db-config: logic-delete-field: deleted # 逻辑删除字段名 id-type: auto # 主键策略 configuration: default-executor-type: batch # 启用批处理模式然后使用:
List<User> users = ...; userService.saveBatch(users, 1000); // 每1000条提交一次带主键回写的高级插入:
User user = new User(); user.setName("李四"); userMapper.insert(user); System.out.println(user.getId()); // 自动回写自增ID3.2 查询的链式调用艺术
MP的lambda查询是最大亮点,这种写法比JPA的Criteria API更直观:
List<User> users = userService.lambdaQuery() .like(User::getName, "张") // 模糊查询 .between(User::getAge, 18, 30) // 范围查询 .orderByDesc(User::getCreateTime) // 排序 .list();复杂查询示例(包含子查询):
List<User> users = userService.lambdaQuery() .inSql(User::getDepartmentId, "SELECT id FROM department WHERE parent_id = 2") .list();性能提示:当查询结果可能很大时,务必使用page()分页而非list()全量查询,否则可能引发OOM。
3.3 更新操作的两种策略
全量更新:
User user = userService.getById(1L); user.setName("王五"); userService.updateById(user); // 更新所有字段动态更新(推荐):
userService.lambdaUpdate() .eq(User::getId, 1L) .set(User::getName, "赵六") .set(User::getAge, 25) .update();特殊场景:乐观锁更新
User user = userService.getById(1L); user.setName("钱七"); user.setVersion(user.getVersion()); // 带上当前版本号 userService.updateById(user); // 内部会检查版本一致性3.4 删除操作的业务考量
物理删除(慎用):
userMapper.deleteById(1L); // 直接从数据库删除逻辑删除(推荐):首先在实体类标记:
@TableLogic private Integer deleted; // 1-已删除 0-未删除然后删除操作变为更新:
userMapper.deleteById(1L); // 实际执行UPDATE SET deleted=1查询时会自动过滤已删除数据:
userService.list(); // 自动附加WHERE deleted=0条件4. 高级特性与性能优化
4.1 自动填充的优雅实现
处理create_time/update_time等字段的自动填充:
- 实体类注解:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;- 实现MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }4.2 分页查询的防坑指南
MP的分页需要先配置拦截器:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }使用示例:
Page<User> page = new Page<>(1, 10); // 当前页,每页条数 Page<User> result = userService.lambdaQuery() .like(User::getName, "张") .page(page);常见问题:
- 当使用left join时,total计数会不准确,需要自定义count sql
- 大数据量分页避免使用
page(pageNum, pageSize),而应该用page(Page page)重用Page对象
4.3 多数据源下的特殊处理
在Spring Boot多数据源场景中,MP需要特殊配置:
@Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.druid.slave") public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean public DynamicDataSource dynamicDataSource() { Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put("master", masterDataSource()); dataSourceMap.put("slave", slaveDataSource()); return new DynamicDataSource(masterDataSource(), dataSourceMap); } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 注意:多数据源时需要为每个数据源单独配置分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL){ @Override public DataSource findDataSource(String sql) { return DynamicDataSourceContextHolder.peek() != null ? dynamicDataSource().getDataSource(DynamicDataSourceContextHolder.peek()) : dynamicDataSource().getDefaultDataSource(); } }); return interceptor; }5. 实战中的血泪教训
5.1 N+1查询问题
即使使用MP,也可能遇到经典的N+1查询问题。例如:
List<Order> orders = orderService.list(); orders.forEach(order -> { User user = userService.getById(order.getUserId()); order.setUser(user); });解决方案:
- 使用@TableField(exist = false) + 手动join查询
- 或者使用MP的@TableField(select = false)延迟加载(需要额外配置)
5.2 大事务下的批量操作
在一次处理10万条数据的场景中,直接使用saveBatch会导致事务过大。正确的做法:
@Transactional(rollbackFor = Exception.class) public void batchProcess(List<User> users) { // 每500条提交一次 int batchSize = 500; for (int i = 0; i < users.size(); i += batchSize) { List<User> subList = users.subList(i, Math.min(i + batchSize, users.size())); userService.saveBatch(subList); // 手动清空会话缓存防止OOM SqlSessionHelper.clearCache(userService.getBaseMapper()); } }5.3 字段类型映射的坑
MySQL的datetime类型映射到Java的LocalDateTime时,如果数据库时区与系统时区不一致,会出现时间偏差。解决方案:
spring: datasource: url: jdbc:mysql://localhost:3306/db?serverTimezone=Asia/Shanghai&useSSL=false对于JSON字段存储,推荐使用MP的TypeHandler:
@TableField(typeHandler = JacksonTypeHandler.class) private Map<String, Object> attributes;5.4 逻辑删除的联表查询
当主表逻辑删除,关联表需要手动处理:
@Select("SELECT u.* FROM user u LEFT JOIN department d ON u.dept_id = d.id " + "WHERE u.deleted = 0 AND d.deleted = 0") List<User> selectActiveUsersWithDepartment();或者使用MP的@SqlParser注解过滤:
@SqlParser(filter = true) public interface UserMapper extends BaseMapper<User> { @Select("SELECT u.* FROM user u LEFT JOIN department d ON u.dept_id = d.id " + "WHERE d.deleted = 0") List<User> selectUsersWithActiveDepartment(); }6. 与前端框架的协作实践
6.1 配合Vue3实现CRUD
前端常见的查询参数结构:
{ "current": 1, "size": 10, "params": { "name": "张", "status": 1 } }后端接收方式:
@PostMapping("/page") public R<Page<User>> page(@RequestBody QueryDTO queryDTO) { LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.like(StringUtils.isNotBlank(queryDTO.getParams().getName()), User::getName, queryDTO.getParams().getName()); wrapper.eq(queryDTO.getParams().getStatus() != null, User::getStatus, queryDTO.getParams().getStatus()); return R.success(userService.page(new Page<>(queryDTO.getCurrent(), queryDTO.getSize()), wrapper)); }6.2 接口返回格式标准化
建议统一返回结构:
@Data public class R<T> implements Serializable { private Integer code; private String msg; private T data; private Long timestamp = System.currentTimeMillis(); public static <T> R<T> success(T data) { R<T> r = new R<>(); r.setCode(200); r.setData(data); return r; } public static <T> R<T> error(String msg) { R<T> r = new R<>(); r.setCode(500); r.setMsg(msg); return r; } }6.3 参数校验的最佳实践
结合Validation注解使用:
@Data public class UserDTO { @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 20, message = "用户名长度2-20位") private String username; @NotNull(message = "部门ID不能为空") private Long deptId; } @PostMapping("/save") public R<String> save(@Valid @RequestBody UserDTO userDTO) { User user = new User(); BeanUtils.copyProperties(userDTO, user); userService.save(user); return R.success("保存成功"); }对于复杂校验(如手机号唯一性),建议在Service层实现:
@Override public boolean save(User user) { if (lambdaQuery().eq(User::getPhone, user.getPhone()).exists()) { throw new BusinessException("手机号已存在"); } return super.save(user); }