news 2026/8/17 11:51:52

MyBatisPlus核心特性与实战:从CRUD封装到条件构造器深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatisPlus核心特性与实战:从CRUD封装到条件构造器深度解析

1. 项目概述:为什么MyBatisPlus是后端开发的“瑞士军刀”?

如果你是一名Java后端开发者,尤其是在处理数据库交互时,大概率听说过或者正在使用MyBatis。它通过XML或注解将SQL与Java代码解耦,给了我们极大的灵活性。但灵活的另一面,是大量的重复劳动:为每个实体写基础的增删改查SQL、手动处理分页逻辑、小心翼翼地拼接动态查询条件……这些工作枯燥且容易出错。而MyBatisPlus(简称MP)的出现,就像给MyBatis这把好刀配上了一套精密的自动化磨刀石和多功能刀鞘。

简单说,MyBatisPlus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变。它的核心目标就一个:简化开发、提高效率。你不用再写那些千篇一律的insert into user values(...),也不用在XML里写一大堆<if test="name != null">。MP通过内置的通用Mapper和Service,以及强大的条件构造器,让你能用极少的代码完成绝大部分的单表操作。我从业十多年,从早期的Hibernate、iBATIS到MyBatis,再到拥抱MP,最大的感触就是它真正把开发者从“数据库搬运工”的角色中解放了出来,让我们能更专注于业务逻辑本身。无论是刚入门的新手,还是追求效率的老鸟,MP都是一项值得投入时间掌握的“生产力工具”。

2. 核心特性与设计哲学拆解

2.1 核心特性:不止于代码生成

很多人对MP的第一印象是它的代码生成器(AutoGenerator),这确实是它的一个亮点,能一键生成Entity、Mapper、Service、Controller等层的基础代码。但MP的价值远不止于此。它的核心特性构成了一个完整的效率提升体系:

  1. 无侵入性:这是MP的设计基石。它只是对MyBatis的增强,你原有的MyBatis配置、自定义的XML映射文件、插件等都能正常工作,与MP无缝共存。这意味着你可以渐进式地采用MP,而无需重构现有项目。
  2. 强大的CRUD接口:通过继承BaseMapper<T>,你的Mapper接口瞬间就拥有了数十个通用的单表操作方法,如insertdeleteByIdupdateByIdselectByIdselectList等。这彻底告别了为每个实体编写基础SQL的时代。
  3. 条件构造器(Wrapper):这是MP的灵魂功能之一。它提供了一套Lambda表达式和链式调用的API,让你能以面向对象的方式动态构建查询条件。无论是等值查询、模糊匹配、区间范围,还是复杂的ANDOR组合,都能用流畅的代码清晰表达,避免了SQL注入风险和字符串拼接的混乱。
  4. 分页插件:配置一个分页插件(PaginationInterceptor或MybatisPlusInterceptor),你的分页查询就从复杂的计算和拼接,变成了简单的page(page, queryWrapper)调用。MP会自动处理不同数据库(MySQL, PostgreSQL, Oracle等)的分页方言,兼容性极佳。
  5. 全局元数据填充:对于像create_timeupdate_timecreate_byupdate_by这类每个表都可能有的是审计字段,MP支持通过MetaObjectHandler接口进行自动填充。你只需要在实体字段上加上@TableField(fill = FieldFill.INSERT)等注解,并在插入或更新时,这些字段会自动被赋予预设的值(如当前时间、当前用户ID)。
  6. 乐观锁插件:通过@Version注解和配置乐观锁插件,可以轻松实现乐观锁,解决高并发下的数据更新冲突问题,无需手动在业务代码中处理版本号比对。

2.2 设计哲学:约定优于配置与ActiveRecord模式

MP的设计深受“约定优于配置”(Convention Over Configuration)思想的影响。它预设了一套合理的默认行为,比如默认将实体类的驼峰属性名映射到数据库的下划线字段名(userName->user_name),默认使用id作为主键列名。只有当你的实际情况与约定不符时,才需要通过注解(如@TableName,@TableField)进行显式配置。这大大减少了冗余的XML配置。

此外,MP也支持ActiveRecord模式。在这种模式下,实体类本身就可以直接调用insert()update()delete()selectById()等方法,无需通过Mapper接口。这对于简单的、以模型为中心的快速开发非常友好。你可以根据项目结构和团队习惯,灵活选择使用标准的Mapper模式还是ActiveRecord模式。

注意:ActiveRecord模式虽然方便,但将数据操作与实体类强耦合,可能会破坏领域模型的纯粹性。在大型、复杂且强调领域驱动设计(DDD)的项目中,建议谨慎使用或仍以Mapper模式为主。

3. 从零开始:环境搭建与基础配置

3.1 依赖引入与基础配置

假设我们使用Spring Boot项目。首先,在pom.xml中引入依赖。这里需要注意版本兼容性,通常选择与你的Spring Boot版本相匹配的MP版本。

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> <!-- 请使用最新稳定版 --> </dependency> <!-- 代码生成器依赖(如果需要) --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> <!-- 代码生成器模板引擎 --> <scope>test</scope> </dependency>

接下来,在application.yml中进行最基础的配置。MP的配置项继承了MyBatis,并增加了一些自己的配置。

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password mybatis-plus: configuration: # 控制台打印完整带参数SQL(开发环境非常有用) log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启驼峰命名自动转换 map-underscore-to-camel-case: true global-config: db-config: # 全局主键类型。AUTO为数据库自增,INPUT为手动输入,ASSIGN_ID为MP雪花算法,ASSIGN_UUID为UUID id-type: ASSIGN_ID # 全局表前缀 # table-prefix: t_ # 指定Mapper XML文件位置,如果和Mapper接口在同一目录且同名,可省略 mapper-locations: classpath*:/mapper/**/*.xml

3.2 实体类与Mapper接口定义

实体类通过注解与数据库表关联。这是MP操作的基石。

import com.baomidou.mybatisplus.annotation.*; import lombok.Data; // 推荐使用Lombok简化代码 @Data @TableName("sys_user") // 指定表名,如果类名是User,表名也是user,可省略 public class User { /** * 主键。 * IdType.ASSIGN_ID: 使用MP的雪花算法生成19位Long型ID(推荐分布式环境) * IdType.AUTO: 数据库自增 */ @TableId(type = IdType.ASSIGN_ID) private Long id; private String username; // 解决字段名与属性名不一致的问题 @TableField("nick_name") private String nickName; private Integer age; private String email; /** * 逻辑删除标记。0-未删除,1-已删除。 * 配置后,调用deleteById方法实际是执行UPDATE SET deleted=1 */ @TableLogic(value = "0", delval = "1") private Integer deleted; /** * 插入时自动填充字段 */ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; /** * 插入和更新时自动填充字段 */ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

Mapper接口只需要简单地继承BaseMapper,并指定泛型为对应的实体类即可。无需任何方法体,MP会在运行时动态代理实现所有通用方法。

import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper // Spring Boot中也可在启动类加@MapperScan批量扫描 public interface UserMapper extends BaseMapper<User> { // 你可以在这里定义自己的复杂SQL方法,MP的通用方法自动拥有 // 例如:User selectUserWithRoleById(@Param("id") Long id); }

3.3 配置全局元数据处理器与插件

为了让createTimeupdateTime自动填充生效,我们需要实现MetaObjectHandler接口。

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 插入操作时,为有@TableField(fill = FieldFill.INSERT)的字段设值 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 实际项目中,可以从SecurityContext或ThreadLocal中获取当前用户ID // this.strictInsertFill(metaObject, "createBy", Long.class, UserContext.getCurrentUserId()); } @Override public void updateFill(MetaObject metaObject) { // 更新操作时,为有@TableField(fill = FieldFill.INSERT_UPDATE)的字段设值 this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // this.strictUpdateFill(metaObject, "updateBy", Long.class, UserContext.getCurrentUserId()); } }

接下来,配置核心插件。在Spring Boot中,通常通过一个配置类来统一管理。

import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; 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); // 设置单页最大记录数 interceptor.addInnerInterceptor(paginationInnerInterceptor); // 乐观锁插件(如果实体类中有@Version字段) // interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 动态表名插件、防止全表更新与删除插件等也可在此添加 // interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } }

4. 核心操作:CRUD与条件构造器实战

4.1 基本CRUD操作

配置完成后,你就可以在Service或Controller中注入UserMapper,开始使用强大的CRUD功能了。

@Autowired private UserMapper userMapper; // 1. 插入 User newUser = new User(); newUser.setUsername("testUser"); newUser.setAge(25); newUser.setEmail("test@example.com"); int insertCount = userMapper.insert(newUser); // 插入后,newUser的id会被自动回填 System.out.println("插入记录数:" + insertCount + ",生成ID:" + newUser.getId()); // 2. 根据ID查询 User user = userMapper.selectById(1L); // 3. 根据ID更新 if (user != null) { user.setEmail("updated@example.com"); int updateCount = userMapper.updateById(user); System.out.println("更新记录数:" + updateCount); } // 4. 根据ID删除(如果配置了逻辑删除,这里是逻辑删除) int deleteCount = userMapper.deleteById(1L); // 5. 批量查询 List<Long> ids = Arrays.asList(1L, 2L, 3L); List<User> userList = userMapper.selectBatchIds(ids); // 6. 简单的条件查询(等值查询) Map<String, Object> conditionMap = new HashMap<>(); conditionMap.put("username", "testUser"); conditionMap.put("age", 25); List<User> usersByMap = userMapper.selectByMap(conditionMap); // WHERE username='testUser' AND age=25

这些方法已经覆盖了大部分简单的单表操作场景。但更复杂的动态查询,就需要请出条件构造器了。

4.2 条件构造器(Wrapper)深度解析

条件构造器是MP将动态SQL从XML解放到Java代码中的关键。它主要分为两类:QueryWrapper(用于查询)和UpdateWrapper(用于更新)。为了更好的类型安全和IDE提示,推荐使用它们的Lambda版本:LambdaQueryWrapperLambdaUpdateWrapper

场景一:复杂查询条件组合

假设我们要查询:年龄大于18岁,并且(用户名包含“张”或者邮箱以“163.com”结尾)的用户,结果按年龄倒序、ID正序排列。

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.gt(User::getAge, 18) // age > 18 .and(wq -> wq.like(User::getUsername, "张") // AND (username LIKE '%张%' .or() // OR .likeRight(User::getEmail, "163.com")) // email LIKE '163.com%' (注意likeRight) .orderByDesc(User::getAge) .orderByAsc(User::getId); List<User> userList = userMapper.selectList(wrapper);

场景二:只查询特定字段(避免select *

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.select(User::getId, User::getUsername, User::getEmail) // 只查询这三个字段 .eq(User::getDeleted, 0); List<User> userList = userMapper.selectList(wrapper); // 此时返回的User对象中,只有id, username, email三个字段有值,其他为null

场景三:使用UpdateWrapper进行条件更新

将年龄小于20岁,或者邮箱为null的用户的用户名改为“YoungOrNull”。

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(); wrapper.lt(User::getAge, 20) .or() .isNull(User::getEmail) .set(User::getUsername, "YoungOrNull"); int updateCount = userMapper.update(null, wrapper); // 第一个参数为null表示不传入实体,更新条件全由wrapper指定

场景四:子查询

查询年龄大于平均年龄的用户。

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.gt(User::getAge, new LambdaQueryWrapper<User>() .selectFunc(i -> "AVG(age)") // 使用selectFunc定义聚合函数 ); // 生成的SQL: SELECT * FROM user WHERE age > (SELECT AVG(age) FROM user) List<User> userList = userMapper.selectList(wrapper);

实操心得:使用Lambda表达式(User::getUsername)而不是字符串(”username”)来引用字段,有两个巨大好处:一是类型安全,编译时就能发现字段名拼写错误;二是便于重构,IDE可以智能地重命名。这是避免低级错误、提升代码健壮性的最佳实践。

4.3 分页查询

配置好分页插件后,分页查询变得异常简单。

import com.baomidou.mybatisplus.extension.plugins.pagination.Page; // 查询第2页,每页10条数据 Page<User> pageParam = new Page<>(2, 10); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.ge(User::getAge, 18) .orderByDesc(User::getCreateTime); Page<User> pageResult = userMapper.selectPage(pageParam, wrapper); System.out.println("总记录数:" + pageResult.getTotal()); System.out.println("总页数:" + pageResult.getPages()); System.out.println("当前页数据:" + pageResult.getRecords());

Page对象既是一个分页参数载体,也是一个分页结果容器。查询后,分页信息(总条数、每页大小等)和数据列表都包含在其中。

5. 进阶功能与最佳实践

5.1 逻辑删除的优雅实现

逻辑删除几乎是现代应用的标配。MP通过@TableLogic注解和全局配置,使其实现近乎零成本。

  1. 实体字段加注解:如上文User实体中的deleted字段。
  2. 全局配置(可选,新版MP通常不需要):在application.yml中,可以全局配置逻辑删除的字段名和值。
    mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值(默认为1) logic-not-delete-value: 0 # 逻辑未删除值(默认为0)
  3. 效果:当你调用mapper.deleteById(1)时,MP实际执行的是UPDATE user SET deleted=1 WHERE id=1 AND deleted=0。而所有的select*方法,MP都会自动在WHERE条件后追加AND deleted=0。这保证了已删除数据对普通查询不可见,但又真实存在于数据库中。

注意事项:联表查询时,MP无法自动为关联表添加逻辑删除条件。你需要在自定义SQL的ONWHERE子句中手动处理关联表的逻辑删除状态。例如:LEFT JOIN department d ON u.dept_id = d.id AND d.deleted=0

5.2 自动填充(审计字段)的深入应用

除了基础的创建/更新时间,自动填充可以用于更广泛的场景,如操作用户ID、租户ID等。关键在于如何获取到这些需要填充的值。通常的做法是结合ThreadLocal或Spring Security的SecurityContextHolder。

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { Long currentUserId = getCurrentUserId(); // 从ThreadLocal或SecurityContext中获取 this.strictInsertFill(metaObject, "createBy", Long.class, currentUserId); this.strictInsertFill(metaObject, "updateBy", Long.class, currentUserId); this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { Long currentUserId = getCurrentUserId(); this.strictUpdateFill(metaObject, "updateBy", Long.class, currentUserId); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } private Long getCurrentUserId() { // 示例:从SecurityContext获取 // Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // if (authentication != null && authentication.isAuthenticated()) { // return ((LoginUser) authentication.getPrincipal()).getId(); // } return 0L; // 返回系统用户或默认值 } }

5.3 代码生成器的定制化使用

虽然代码生成器能快速搭建框架,但生成出来的代码往往需要根据项目规范进行二次调整。因此,定制化生成模板是关键。

import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; // 或用Velocity public class CodeGenerator { public static void main(String[] args) { String projectPath = System.getProperty("user.dir"); String outputDir = projectPath + "/src/main/java"; FastAutoGenerator.create("jdbc:mysql://localhost:3306/your_db", "root", "password") .globalConfig(builder -> { builder.author("YourName") // 作者 .outputDir(outputDir) // 输出目录 .disableOpenDir() // 生成后不打开目录 .commentDate("yyyy-MM-dd"); // 注释日期格式 }) .packageConfig(builder -> { builder.parent("com.yourcompany.project") // 父包名 .moduleName("system") // 模块名 .entity("entity") // 实体包名 .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller") .pathInfo(Collections.singletonMap(OutputFile.xml, projectPath + "/src/main/resources/mapper")); // XML位置 }) .strategyConfig(builder -> { builder.addInclude("sys_user", "sys_role") // 要生成的表 .addTablePrefix("sys_") // 过滤表前缀 .entityBuilder() .enableLombok() // 启用Lombok .enableTableFieldAnnotation() // 字段加上@TableField注解 .logicDeleteColumnName("deleted") // 逻辑删除字段 .versionColumnName("version") // 乐观锁字段 .addTableFills(new Column("create_time", FieldFill.INSERT), new Column("update_time", FieldFill.INSERT_UPDATE)) // 自动填充字段 .controllerBuilder() .enableRestStyle() // 生成@RestController .mapperBuilder() .enableMapperAnnotation(); // 生成@Mapper注解 }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎,模板更易定制 .execute(); } }

你可以复制MP源码中的模板文件(位于mybatis-plus-generatorjar包的templates目录下)到项目的resources/templates目录下,然后自由修改。例如,修改controller.java.ftl,让生成的Controller统一继承某个基类,或者添加特定的注解。

5.4 多数据源与动态数据源

在微服务或复杂应用中,一个服务连接多个数据库是常见需求。MP本身不直接提供多数据源支持,但它与Spring Boot的生态完美兼容,可以轻松结合第三方多数据源组件,如dynamic-datasource-spring-boot-starter

  1. 引入依赖
    <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.2</version> </dependency>
  2. 配置数据源
    spring: datasource: dynamic: primary: master # 设置默认数据源 strict: false # 是否严格匹配数据源,默认false datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/master_db username: root password: 123456 slave1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/slave_db username: root password: 123456
  3. 使用注解切换数据源
    @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { // 默认使用master数据源 @DS("slave1") // 标记此方法使用slave1数据源 @Override public List<User> getFromSlave() { return this.list(); } }
    @DS注解可以加在类上(类中所有方法生效)、方法上,甚至可以在一个方法内通过DynamicDataSourceContextHolder.push(“slave1”)手动切换。

6. 性能调优、常见问题与排查技巧

6.1 SQL性能分析与打印

开发阶段,开启MP的SQL打印功能对调试和性能分析至关重要。除了在配置中设置log-implStdOutImpl,更推荐使用p6spy这类SQL拦截分析工具,它能打印出完整的、带执行时间的SQL语句,并支持慢SQL记录。

<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>

将驱动类改为com.p6spy.engine.spy.P6SpyDriver,URL前缀改为jdbc:p6spy:,即可在控制台看到详细的SQL日志。

6.2 常见问题与解决方案实录

问题1:N+1查询问题在使用selectList查询主表,然后循环遍历获取关联对象时非常容易发生。

  • 场景:查询用户列表,然后循环查询每个用户的部门信息。
  • 错误做法
    List<User> users = userMapper.selectList(null); for (User user : users) { Department dept = departmentMapper.selectById(user.getDeptId()); // 循环内查询数据库! user.setDepartment(dept); }
  • 解决方案
    1. 自定义SQL联表查询:在XML中编写一个<select>,通过JOIN一次性查出所有用户及其部门信息。这是最高效的方式。
    2. 使用MP的@TableField(exist = false)和业务层组装:在User实体中定义private Department department;并标记@TableField(exist = false)。在Service层,先批量查询出所有用户,再收集所有部门ID,用selectBatchIds一次性查出所有部门,最后在内存中通过Map进行组装。这减少了数据库连接次数,但增加了内存和CPU开销。
    3. 考虑使用MyBatis的<collection><association>标签:在自定义的XML映射文件中定义ResultMap,实现复杂的结果集映射。这需要一定的MyBatis功底。

问题2:更新时,非空字段被意外更新为null。

  • 原因:MP的updateById(T entity)方法,默认会更新实体中所有非null的字段。如果你从数据库查出一个User对象,只修改了email字段,其他字段保持原样,然后调用updateById,这是安全的。但如果你是从前端接收了一个DTO,并转换成一个新的User对象,其中只有idemail有值,其他属性为null,那么updateById就会用null去覆盖数据库中的原有值。
  • 解决方案
    1. 使用UpdateWrapper:只更新明确设置的字段。
      LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(User::getId, user.getId()) .set(User::getEmail, user.getEmail()); // 只set需要更新的字段 userMapper.update(null, wrapper);
    2. 使用update(T entity, Wrapper<T> updateWrapper)方法:将实体作为部分更新字段的载体,结合Wrapper指定更新条件。但实体中为null的字段仍然会被忽略(取决于全局配置field-strategy,默认为NOT_NULL)。更可控的方式还是方案1。

问题3:分页查询总数COUNT语句慢或不准。

  • 原因:MP分页插件会先执行一条SELECT COUNT(1) FROM ...语句获取总数。如果表数据量极大,或者WHERE条件非常复杂,这条COUNT语句可能会很慢。在某些带有GROUP BY的复杂查询中,COUNT结果可能不符合预期。
  • 解决方案
    1. 优化COUNT语句:为分页查询的WHERE条件字段建立合适的索引。
    2. 不查询总数:如果业务上不需要知道总页数,只需要“下一页”功能,可以使用PagesetSearchCount(false)方法,禁用COUNT查询。
      Page<User> page = new Page<>(current, size); page.setSearchCount(false); // 不执行SELECT COUNT语句 userMapper.selectPage(page, queryWrapper);
    3. 自定义COUNT语句:对于极其复杂的查询,可以在Mapper接口中定义一个返回LongselectCustomCount方法,并在XML中编写优化的COUNT SQL,然后在分页查询时,通过page.setOptimizeCountSql(false)使用自定义的COUNT方法。

问题4:Lambda表达式在JDK版本下的序列化问题。

  • 现象:在将LambdaQueryWrapper放入Redis缓存或进行RPC传输时,可能会报序列化错误。
  • 原因:Lambda表达式依赖于运行时的上下文,默认的序列化机制(如Java原生序列化)无法正确处理。
  • 解决方案
    1. 避免序列化Wrapper:不要直接缓存或传输Wrapper对象。改为缓存查询结果(实体列表),或者将查询条件封装成可序列化的普通POJO(如UserQueryParam)进行传输。
    2. 使用字符串形式的QueryWrapper:如果必须传输查询条件,可以使用非Lambda版本的QueryWrapper,它基于字符串属性名,是可序列化的。但这就牺牲了类型安全和IDE提示的优势。

问题5:全局配置的id-type与数据库设计冲突。

  • 现象:数据库主键是自增(AUTO_INCREMENT),但实体类@TableId未指定或全局配置为ASSIGN_ID(雪花算法),导致插入时主键冲突或为null。
  • 解决方案:明确指定主键策略。优先在实体类的@TableId注解上指定,它比全局配置优先级高。
    @TableId(value = “id”, type = IdType.AUTO) // 明确使用数据库自增 private Long id;
    确保实体类、数据库、全局配置三者的主键生成策略一致。

掌握MyBatisPlus,本质上是在掌握一种“如何更聪明地偷懒”的艺术。它通过封装和约定,将开发者从重复的体力劳动中解放出来。但越是强大的工具,越需要理解其原理和边界。清晰地区分哪些工作应该交给MP自动化,哪些复杂的业务逻辑需要自己精细控制(比如手写复杂联表SQL),是用好MP的关键。从我个人的经验来看,在单表操作占比超过80%的中后台管理类项目中,MP能带来至少30%的开发效率提升。而对于那些核心的、复杂的业务查询,回归到手写XML或注解SQL,保持灵活性和可控性,才是明智之举。最后一个小技巧:定期关注MP的GitHub Releases页面,这个项目迭代很快,新版本经常会修复一些隐蔽的Bug或带来有用的新特性,保持更新能让你避开很多已知的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/17 11:51:34

MySQL EXPLAIN执行计划详解:从原理到实战优化慢查询

1. 从一次线上慢查询说起&#xff1a;为什么我们需要Explain 那天下午&#xff0c;监控系统突然告警&#xff0c;一个核心业务接口的响应时间从平时的几十毫秒飙升到了十几秒。整个团队立刻紧张起来&#xff0c;数据库的CPU使用率也冲到了90%以上。登录到数据库服务器&#xff…

作者头像 李华
网站建设 2026/8/17 11:46:02

Windows系统Redis 5.0.14.1安装配置与实战指南

1. 项目缘起&#xff1a;为什么选择在Windows上安装Redis 5.0.14.1&#xff1f; 如果你是一名后端开发者&#xff0c;或者正在学习缓存、消息队列相关的技术&#xff0c;Redis这个名字你一定不陌生。它是一个开源的、基于内存的键值存储数据库&#xff0c;常被用作缓存、消息代…

作者头像 李华
网站建设 2026/8/17 11:43:23

CSS背景图片自适应全解析:从background-size到object-fit的实战方案

1. 项目概述&#xff1a;为什么我们需要让背景图片“活”起来&#xff1f; 在网页开发中&#xff0c;处理图片展示是一个高频且基础的需求。我们最熟悉的莫过于 <img> 标签&#xff0c;它有一个非常直观的特性&#xff1a;图片会默认填充其容器的宽度&#xff0c;高度按…

作者头像 李华
网站建设 2026/8/17 11:41:32

Figma文件整理四步法:从评估到复用的设计资产管理实践

最近在整理设计资产时&#xff0c;发现团队里积压了不少陈旧的 Figma 文件。这些“老物”就像数字仓库里的旧箱子&#xff0c;看似无用&#xff0c;却可能藏着可复用的组件、过时的设计规范&#xff0c;甚至是记录产品演进的历史线索。直接删除怕误伤&#xff0c;放任不管又占地…

作者头像 李华
网站建设 2026/8/17 11:40:53

离线语音识别怎么部署?——灵声智库离线 ASR、批量录音转写、CPU/GPU 与私有化部署实践

北京宜天信达技术委员会 灵声智库&#xff5c;离线语音识别、离线部署、批量转写与本地私有化ASR技术长文 图 1 离线语音识别、历史录音批处理与本地化部署场景 摘要&#xff1a;离线语音识别、离线部署和批量录音转写仍然是企业 ASR 的核心需求。本文围绕任务队列、GPU Bat…

作者头像 李华
网站建设 2026/8/17 11:40:44

CapFrameX:专业帧时间分析工具,精准定位游戏卡顿与性能瓶颈

1. 从“小飞机”到CapFrameX&#xff1a;为什么我们需要更专业的帧数监测工具 如果你是一名PC游戏玩家&#xff0c;或者经常需要测试显卡、CPU性能&#xff0c;那么对“小飞机”这个昵称一定不陌生。MSI Afterburner&#xff0c;这款集超频、监控、截图录像于一体的瑞士军刀&am…

作者头像 李华