MyBatis-Plus入门指南:提升CRUD效率的实战技巧
2026/9/11 6:37:16 网站建设 项目流程

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()); // 自动回写自增ID

3.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等字段的自动填充:

  1. 实体类注解:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;
  1. 实现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);

常见问题:

  1. 当使用left join时,total计数会不准确,需要自定义count sql
  2. 大数据量分页避免使用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); });

解决方案:

  1. 使用@TableField(exist = false) + 手动join查询
  2. 或者使用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); }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询