ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

MyBatisPlus核心特性与实战:从CRUD封装到条件构造器深度解析 1. 项目概述为什么MyBatisPlus是后端开发的“瑞士军刀”如果你是一名Java后端开发者尤其是在处理数据库交互时大概率听说过或者正在使用MyBatis。它通过XML或注解将SQL与Java代码解耦给了我们极大的灵活性。但灵活的另一面是大量的重复劳动为每个实体写基础的增删改查SQL、手动处理分页逻辑、小心翼翼地拼接动态查询条件……这些工作枯燥且容易出错。而MyBatisPlus简称MP的出现就像给MyBatis这把好刀配上了一套精密的自动化磨刀石和多功能刀鞘。简单说MyBatisPlus是一个MyBatis的增强工具在MyBatis的基础上只做增强不做改变。它的核心目标就一个简化开发、提高效率。你不用再写那些千篇一律的insert into user values(...)也不用在XML里写一大堆if testname ! null。MP通过内置的通用Mapper和Service以及强大的条件构造器让你能用极少的代码完成绝大部分的单表操作。我从业十多年从早期的Hibernate、iBATIS到MyBatis再到拥抱MP最大的感触就是它真正把开发者从“数据库搬运工”的角色中解放了出来让我们能更专注于业务逻辑本身。无论是刚入门的新手还是追求效率的老鸟MP都是一项值得投入时间掌握的“生产力工具”。2. 核心特性与设计哲学拆解2.1 核心特性不止于代码生成很多人对MP的第一印象是它的代码生成器AutoGenerator这确实是它的一个亮点能一键生成Entity、Mapper、Service、Controller等层的基础代码。但MP的价值远不止于此。它的核心特性构成了一个完整的效率提升体系无侵入性这是MP的设计基石。它只是对MyBatis的增强你原有的MyBatis配置、自定义的XML映射文件、插件等都能正常工作与MP无缝共存。这意味着你可以渐进式地采用MP而无需重构现有项目。强大的CRUD接口通过继承BaseMapperT你的Mapper接口瞬间就拥有了数十个通用的单表操作方法如insert、deleteById、updateById、selectById、selectList等。这彻底告别了为每个实体编写基础SQL的时代。条件构造器Wrapper这是MP的灵魂功能之一。它提供了一套Lambda表达式和链式调用的API让你能以面向对象的方式动态构建查询条件。无论是等值查询、模糊匹配、区间范围还是复杂的AND、OR组合都能用流畅的代码清晰表达避免了SQL注入风险和字符串拼接的混乱。分页插件配置一个分页插件PaginationInterceptor或MybatisPlusInterceptor你的分页查询就从复杂的计算和拼接变成了简单的page(page, queryWrapper)调用。MP会自动处理不同数据库MySQL, PostgreSQL, Oracle等的分页方言兼容性极佳。全局元数据填充对于像create_time、update_time、create_by、update_by这类每个表都可能有的是审计字段MP支持通过MetaObjectHandler接口进行自动填充。你只需要在实体字段上加上TableField(fill FieldFill.INSERT)等注解并在插入或更新时这些字段会自动被赋予预设的值如当前时间、当前用户ID。乐观锁插件通过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 groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version !-- 请使用最新稳定版 -- /dependency !-- 代码生成器依赖如果需要 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.3.1/version scopetest/scope /dependency dependency groupIdorg.apache.velocity/groupId artifactIdvelocity-engine-core/artifactId version2.3/version !-- 代码生成器模板引擎 -- scopetest/scope /dependency接下来在application.yml中进行最基础的配置。MP的配置项继承了MyBatis并增加了一些自己的配置。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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/**/*.xml3.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 deleted1 */ 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 BaseMapperUser { // 你可以在这里定义自己的复杂SQL方法MP的通用方法自动拥有 // 例如User selectUserWithRoleById(Param(id) Long id); }3.3 配置全局元数据处理器与插件为了让createTime和updateTime自动填充生效我们需要实现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(testexample.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(updatedexample.com); int updateCount userMapper.updateById(user); System.out.println(更新记录数 updateCount); } // 4. 根据ID删除如果配置了逻辑删除这里是逻辑删除 int deleteCount userMapper.deleteById(1L); // 5. 批量查询 ListLong ids Arrays.asList(1L, 2L, 3L); ListUser userList userMapper.selectBatchIds(ids); // 6. 简单的条件查询等值查询 MapString, Object conditionMap new HashMap(); conditionMap.put(username, testUser); conditionMap.put(age, 25); ListUser usersByMap userMapper.selectByMap(conditionMap); // WHERE usernametestUser AND age25这些方法已经覆盖了大部分简单的单表操作场景。但更复杂的动态查询就需要请出条件构造器了。4.2 条件构造器Wrapper深度解析条件构造器是MP将动态SQL从XML解放到Java代码中的关键。它主要分为两类QueryWrapper用于查询和UpdateWrapper用于更新。为了更好的类型安全和IDE提示推荐使用它们的Lambda版本LambdaQueryWrapper和LambdaUpdateWrapper。场景一复杂查询条件组合假设我们要查询年龄大于18岁并且用户名包含“张”或者邮箱以“163.com”结尾的用户结果按年龄倒序、ID正序排列。import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; LambdaQueryWrapperUser 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); ListUser userList userMapper.selectList(wrapper);场景二只查询特定字段避免select *LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.select(User::getId, User::getUsername, User::getEmail) // 只查询这三个字段 .eq(User::getDeleted, 0); ListUser userList userMapper.selectList(wrapper); // 此时返回的User对象中只有id, username, email三个字段有值其他为null场景三使用UpdateWrapper进行条件更新将年龄小于20岁或者邮箱为null的用户的用户名改为“YoungOrNull”。import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.lt(User::getAge, 20) .or() .isNull(User::getEmail) .set(User::getUsername, YoungOrNull); int updateCount userMapper.update(null, wrapper); // 第一个参数为null表示不传入实体更新条件全由wrapper指定场景四子查询查询年龄大于平均年龄的用户。LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.gt(User::getAge, new LambdaQueryWrapperUser() .selectFunc(i - AVG(age)) // 使用selectFunc定义聚合函数 ); // 生成的SQL: SELECT * FROM user WHERE age (SELECT AVG(age) FROM user) ListUser userList userMapper.selectList(wrapper);实操心得使用Lambda表达式User::getUsername而不是字符串”username”来引用字段有两个巨大好处一是类型安全编译时就能发现字段名拼写错误二是便于重构IDE可以智能地重命名。这是避免低级错误、提升代码健壮性的最佳实践。4.3 分页查询配置好分页插件后分页查询变得异常简单。import com.baomidou.mybatisplus.extension.plugins.pagination.Page; // 查询第2页每页10条数据 PageUser pageParam new Page(2, 10); LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.ge(User::getAge, 18) .orderByDesc(User::getCreateTime); PageUser 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注解和全局配置使其实现近乎零成本。实体字段加注解如上文User实体中的deleted字段。全局配置可选新版MP通常不需要在application.yml中可以全局配置逻辑删除的字段名和值。mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值默认为1 logic-not-delete-value: 0 # 逻辑未删除值默认为0效果当你调用mapper.deleteById(1)时MP实际执行的是UPDATE user SET deleted1 WHERE id1 AND deleted0。而所有的select*方法MP都会自动在WHERE条件后追加AND deleted0。这保证了已删除数据对普通查询不可见但又真实存在于数据库中。注意事项联表查询时MP无法自动为关联表添加逻辑删除条件。你需要在自定义SQL的ON或WHERE子句中手动处理关联表的逻辑删除状态。例如LEFT JOIN department d ON u.dept_id d.id AND d.deleted0。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。引入依赖dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.2/version /dependency配置数据源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使用注解切换数据源Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { // 默认使用master数据源 DS(slave1) // 标记此方法使用slave1数据源 Override public ListUser getFromSlave() { return this.list(); } }DS注解可以加在类上类中所有方法生效、方法上甚至可以在一个方法内通过DynamicDataSourceContextHolder.push(“slave1”)手动切换。6. 性能调优、常见问题与排查技巧6.1 SQL性能分析与打印开发阶段开启MP的SQL打印功能对调试和性能分析至关重要。除了在配置中设置log-impl为StdOutImpl更推荐使用p6spy这类SQL拦截分析工具它能打印出完整的、带执行时间的SQL语句并支持慢SQL记录。dependency groupIdp6spy/groupId artifactIdp6spy/artifactId version3.9.1/version /dependency将驱动类改为com.p6spy.engine.spy.P6SpyDriverURL前缀改为jdbc:p6spy:即可在控制台看到详细的SQL日志。6.2 常见问题与解决方案实录问题1N1查询问题在使用selectList查询主表然后循环遍历获取关联对象时非常容易发生。场景查询用户列表然后循环查询每个用户的部门信息。错误做法ListUser users userMapper.selectList(null); for (User user : users) { Department dept departmentMapper.selectById(user.getDeptId()); // 循环内查询数据库 user.setDepartment(dept); }解决方案自定义SQL联表查询在XML中编写一个select通过JOIN一次性查出所有用户及其部门信息。这是最高效的方式。使用MP的TableField(exist false)和业务层组装在User实体中定义private Department department;并标记TableField(exist false)。在Service层先批量查询出所有用户再收集所有部门ID用selectBatchIds一次性查出所有部门最后在内存中通过Map进行组装。这减少了数据库连接次数但增加了内存和CPU开销。考虑使用MyBatis的collection或association标签在自定义的XML映射文件中定义ResultMap实现复杂的结果集映射。这需要一定的MyBatis功底。问题2更新时非空字段被意外更新为null。原因MP的updateById(T entity)方法默认会更新实体中所有非null的字段。如果你从数据库查出一个User对象只修改了email字段其他字段保持原样然后调用updateById这是安全的。但如果你是从前端接收了一个DTO并转换成一个新的User对象其中只有id和email有值其他属性为null那么updateById就会用null去覆盖数据库中的原有值。解决方案使用UpdateWrapper只更新明确设置的字段。LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.eq(User::getId, user.getId()) .set(User::getEmail, user.getEmail()); // 只set需要更新的字段 userMapper.update(null, wrapper);使用update(T entity, WrapperT updateWrapper)方法将实体作为部分更新字段的载体结合Wrapper指定更新条件。但实体中为null的字段仍然会被忽略取决于全局配置field-strategy默认为NOT_NULL。更可控的方式还是方案1。问题3分页查询总数COUNT语句慢或不准。原因MP分页插件会先执行一条SELECT COUNT(1) FROM ...语句获取总数。如果表数据量极大或者WHERE条件非常复杂这条COUNT语句可能会很慢。在某些带有GROUP BY的复杂查询中COUNT结果可能不符合预期。解决方案优化COUNT语句为分页查询的WHERE条件字段建立合适的索引。不查询总数如果业务上不需要知道总页数只需要“下一页”功能可以使用Page的setSearchCount(false)方法禁用COUNT查询。PageUser page new Page(current, size); page.setSearchCount(false); // 不执行SELECT COUNT语句 userMapper.selectPage(page, queryWrapper);自定义COUNT语句对于极其复杂的查询可以在Mapper接口中定义一个返回Long的selectCustomCount方法并在XML中编写优化的COUNT SQL然后在分页查询时通过page.setOptimizeCountSql(false)使用自定义的COUNT方法。问题4Lambda表达式在JDK版本下的序列化问题。现象在将LambdaQueryWrapper放入Redis缓存或进行RPC传输时可能会报序列化错误。原因Lambda表达式依赖于运行时的上下文默认的序列化机制如Java原生序列化无法正确处理。解决方案避免序列化Wrapper不要直接缓存或传输Wrapper对象。改为缓存查询结果实体列表或者将查询条件封装成可序列化的普通POJO如UserQueryParam进行传输。使用字符串形式的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或带来有用的新特性保持更新能让你避开很多已知的坑。
返回列表