1. 项目概述为什么我们需要MyBatis-Plus的内置方法如果你用过原生的MyBatis肯定对写不完的XML映射文件和重复的CRUD增删改查SQL感到头疼。每次新建一个实体都要配套写一套几乎一模一样的insert、update、deleteById、selectById方法不仅枯燥还容易出错。MyBatis-Plus简称MP的出现很大程度上就是为了解决这个“体力活”问题。它不是一个全新的框架而是在MyBatis基础上的强力增强其核心价值之一就是提供了一套开箱即用的通用Mapper也就是我们常说的“内置方法”。简单来说MP通过内置方法将开发者从重复的SQL编写中解放出来。你只需要让你的Mapper接口继承MP提供的BaseMapper接口并指定泛型为你的实体类那么针对这个实体的绝大部分单表操作你就已经拥有了。无需编写任何SQL甚至无需任何XML文件就能完成数据的插入、更新、删除和查询。这不仅仅是减少了代码量更重要的是提升了开发效率降低了维护成本并且保证了基础操作的一致性和规范性。这篇文章我就以一个多年Java后端开发者的视角带你彻底搞懂MP这些内置方法。我不会只给你罗列API文档而是结合我实际项目中的使用经验告诉你每个方法怎么用、什么时候用、有哪些“坑”需要避开以及如何利用这些方法应对复杂的业务场景。无论你是刚接触MP的新手还是想深入了解其细节的老手相信都能有所收获。2. 核心设计思路BaseMapper与Active Record模式在深入每个方法之前理解MP的设计哲学至关重要。这能让你明白为什么这些方法这样设计以及如何更优雅地使用它们。2.1 BaseMapper通用操作的基石MP内置方法的载体是com.baomidou.mybatisplus.core.mapper.BaseMapperT接口。这里的泛型T就是你的实体类。当你自己的Mapper接口继承它时就自动拥有了数十个方法。// 你的实体 Data TableName(“user”) // 指定表名如果表名和类名一致可省略 public class User { TableId(type IdType.AUTO) // 指定主键策略为数据库自增 private Long id; private String name; private Integer age; private String email; } // 你的Mapper接口 public interface UserMapper extends BaseMapperUser { // 此时UserMapper已经拥有了BaseMapperUser的所有方法 // 你可以在这里额外定义复杂的、需要自定义SQL的方法 }这种设计的好处是关注点分离。BaseMapper处理所有通用的、模式化的单表操作而你自定义的Mapper接口则专注于处理复杂的、多表关联或特殊业务逻辑的SQL。代码结构非常清晰。2.2 Active Record模式另一种选择除了通过Mapper调用MP还支持Active RecordAR模式。在这种模式下实体类本身继承ModelT类从而拥有操作数据库的能力。Data TableName(“user”) public class User extends ModelUser { private Long id; private String name; // ... 其他字段 // 可以直接在实体对象上调用CRUD方法 public boolean saveUser() { return this.insert(); // 调用的是Model类的方法 } } // 在服务层或控制器中可以直接使用 User user new User(); user.setName(“张三”); boolean success user.insert(); // 无需注入Mapper直接插入AR模式 vs Mapper模式如何选Mapper模式推荐更符合传统的分层架构Controller-Service-Mapper职责清晰易于统一管理事务和复杂逻辑。在绝大多数中大型项目或团队协作中这是首选。AR模式适合快速原型开发、小型项目或简单的独立实体操作。它让代码更简洁但可能会模糊了实体层和数据访问层的边界在复杂事务场景下不如Mapper模式好控制。我个人在项目中更倾向于使用Mapper模式因为它与Spring的依赖注入、事务管理结合得更好架构更清晰。下文也将主要围绕BaseMapper的内置方法展开。3. 插入方法详解insert的学问插入数据是最基础的操作MP提供了最核心的insert方法。3.1 基本使用与主键策略// 1. 准备实体对象 User user new User(); user.setName(“李四”); user.setAge(25); user.setEmail(“lisiexample.com”); // id 字段未设置因为我们希望数据库自增生成 // 2. 执行插入 int rows userMapper.insert(user); // rows 是受影响的行数成功插入时为 1 // 3. 插入后自增主键会自动回填到实体对象中 System.out.println(“插入成功生成的主键ID是” user.getId());这里的关键点是主键回填。如果你的主键策略是IdType.AUTO数据库自增MP会在执行插入后通过JDBC的Statement.getGeneratedKeys()方法获取生成的主键值并自动设置回你传入的实体对象里。这是一个非常贴心且重要的特性省去了你额外查询的麻烦。主键策略 (TableId) 详解MP支持多种主键策略通过TableId注解的type属性指定IdType.AUTO数据库ID自增MySQL、PostgreSQL等。最常用。IdType.NONE无状态该类型为未设置主键类型默认。如果你不指定MP默认认为你已手动设置主键值。IdType.INPUT用户输入ID。插入前必须手动设置主键值。IdType.ASSIGN_ID分配ID默认。使用雪花算法生成一个Long类型的ID。这是MP在IdType.NONE时的默认全局策略如果配置了全局主键策略为ASSIGN_ID。IdType.ASSIGN_UUID分配UUID生成一个String类型的UUID。实操心得在分布式系统中推荐使用ASSIGN_ID雪花算法避免自增ID带来的数据迁移和分库分表难题。对于小项目或传统单库AUTO足够简单高效。务必在项目初期就确定好主键策略。3.2 字段过滤与空值处理insert方法默认会插入实体对象中所有不为null的字段。这是MP的默认策略目的是避免将null值插入数据库覆盖掉字段可能存在的默认值。User user new User(); user.setName(“王五”); // age 和 email 为 null userMapper.insert(user); // 生成的SQL类似于INSERT INTO user (name) VALUES (‘王五’); // age和email字段不会被包含在INSERT语句中如果你想插入null值怎么办这就需要用到MP的字段注解TableField。public class User { // ... TableField(insertStrategy FieldStrategy.IGNORED) // 插入时忽略判断字段一定会被加入SQL private String email; }设置insertStrategy FieldStrategy.IGNORED后即使email为nullMP也会将其包含在INSERT语句中值为NULL。FieldStrategy还有其他策略如NOT_NULL非空才插入默认、NOT_EMPTY非空且非空字符串才插入等。注意事项谨慎使用IGNORED策略。除非业务上明确需要插入NULL否则让数据库使用字段定义的默认值DEFAULT通常是更好的选择。盲目插入NULL可能会影响查询效率因为NULL不能利用普通索引进行等值查询和业务逻辑判断。4. 更新方法解析updateById与动态更新更新操作比插入更复杂因为通常我们只更新部分字段。MP的更新方法很好地处理了这个问题。4.1 根据ID更新updateById这是最常用的更新方法。// 1. 先查询出要更新的实体或至少知道其ID User user new User(); user.setId(1L); // 必须设置主键 user.setName(“赵六”); user.setAge(30); // email 字段为 null // 2. 执行更新 int rows userMapper.updateById(user); // 生成的SQL类似于UPDATE user SET name‘赵六’ age30 WHERE id1; // 注意email字段因为为null默认不会被更新和insert一样updateById默认只更新非null字段。这是一种“动态更新”特性非常实用。你只需要从前端或参数中接收变更的字段构造一个部分字段有值的实体对象调用updateById即可无需担心其他字段被意外覆盖。4.2 如何更新字段为null—— 一个高频问题这是MP使用者最常遇到的困惑之一“我想把某个字段的值清空设为NULL但updateById不生效”正如热搜词提到的“mybatis-plus 的basemapper的updatebyid可以修改字段值为null吗”答案是默认情况下不可以。原因就是上面提到的默认更新策略FieldStrategy.NOT_NULL。当字段值为null时MP认为你不希望更新此字段从而不会将其加入到SET子句中。解决方案有三种使用TableField注解推荐用于特定字段public class User { TableField(updateStrategy FieldStrategy.IGNORED) private String email; }将该字段的更新策略设置为IGNORED这样无论其值是否为null都会参与更新。使用UpdateWrapper推荐用于一次性操作UpdateWrapperUser updateWrapper new UpdateWrapper(); updateWrapper.eq(“id”, 1L).set(“email”, null); // 直接使用set方法设置值为null userMapper.update(null, updateWrapper);通过UpdateWrapper的set()方法可以显式地设置字段为null。这种方式更灵活不影响实体类的全局配置。全局配置谨慎使用 在MP的全局配置中可以设置默认的updateStrategy。但这会影响所有实体所有字段风险较大一般不推荐。mybatis-plus: global-config: db-config: update-strategy: ignored # 全局更新策略设为忽略所有null字段都会更新避坑指南我个人的经验是优先使用UpdateWrapper进行需要设null的更新操作。因为将字段设为null通常是一个特定的业务需求如“清空邮箱”而不是该字段的常态。使用UpdateWrapper可以精准控制避免因实体类注解而影响其他正常的更新逻辑。将字段策略改为IGNORED需要慎重因为它意味着所有更新操作只要该字段在实体对象里无论是否有意都会被纳入SQL。4.3 条件更新update(WrapperT updateWrapper)除了根据ID更新MP还提供了强大的条件更新方法update(T entity, WrapperT updateWrapper)。它允许你结合实体对象提供SET值和条件构造器提供WHERE条件进行复杂更新。// 案例将所有年龄大于25岁的用户的邮箱后缀统一更新 User updateEntity new User(); updateEntity.setEmail(“new-domain.com”); UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.gt(“age”, 25); // age 25 // wrapper.set(“email”, “new-domain.com”) // 也可以在这里用set这样就不需要entity参数了 userMapper.update(updateEntity, wrapper); // 生成的SQLUPDATE user SET email‘new-domain.com’ WHERE age 25;UpdateWrapper功能非常强大支持eq等于、ne不等于、gt大于、lt小于、like模糊查询、in在集合中等丰富的条件方法几乎可以满足所有单表更新条件。5. 插入或更新方法saveOrUpdate的智能抉择这个方法名就揭示了它的作用存在则更新不存在则插入。这在处理“导入数据”、“同步数据”等场景时非常有用可以避免先查询判断是否存在再操作的冗余步骤。5.1 工作原理与使用saveOrUpdate方法并不直接存在于BaseMapper中而是存在于IService接口中。IService是MP提供的服务层通用接口它封装了BaseMapper并提供了更多批量和逻辑删除等方法。通常我们会有一个Service接口继承IService并有一个实现类继承ServiceImpl。// 1. 定义Service public interface UserService extends IServiceUser { // 自定义业务方法 } Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { // 已经拥有了IService的所有方法包括saveOrUpdate } // 2. 使用saveOrUpdate Autowired private UserService userService; User user new User(); user.setId(100L); // 假设这个ID可能已存在 user.setName(“钱七”); boolean result userService.saveOrUpdate(user); // MP会判断如果数据库中存在id100的记录则执行更新否则执行插入。判断依据是什么MP默认根据主键是否存在来判断。它会检查你传入的实体对象的主键字段如果主键有值非null且非空对于数字类型0也可能被视为无值取决于配置则尝试执行更新。如果主键无值则执行插入。5.2 自定义判断逻辑有时判断是否存在的依据可能不是主键而是唯一索引组合如“用户名”。MP也提供了saveOrUpdate(T entity, WrapperT updateWrapper)方法。User user new User(); user.setUsername(“admin”); // 假设username是唯一键 user.setName(“新管理员”); // 构建一个以username为判断条件的Wrapper LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.eq(User::getUsername, user.getUsername()); userService.saveOrUpdate(user, wrapper); // 逻辑根据wrapper查询是否存在username‘admin’的记录存在则用entity更新不存在则插入。实操心得saveOrUpdate虽然方便但在高并发场景下需要特别注意。它的“查询-判断-插入/更新”操作不是原子的可能存在竞态条件即两个线程同时判断为“不存在”然后都执行了插入导致重复数据。对于严格不允许重复的业务场景更可靠的做法是在数据库层面建立唯一约束然后使用try-catch捕获插入时的唯一键冲突异常再转为更新操作或者使用数据库特有的“UPSERT”语句如MySQL的ON DUPLICATE KEY UPDATEMP也支持通过Insert注解编写自定义SQL来实现。6. 删除方法盘点物理删除与逻辑删除删除操作需要格外小心。MP提供了多种删除方式并内置了逻辑删除的支持。6.1 物理删除方法物理删除是指直接从数据库表中移除数据行。deleteById(Serializable id)根据主键删除。deleteByMap(MapString, Object columnMap)根据column-value条件删除。delete(WrapperT wrapper)根据条件构造器删除。这是最灵活的方式。deleteBatchIds(Collection? extends Serializable idList)根据主键ID集合批量删除。// 根据ID删除 userMapper.deleteById(1L); // 根据条件删除删除所有年龄小于18岁的用户 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.lt(User::getAge, 18); userMapper.delete(wrapper); // 批量删除 ListLong idList Arrays.asList(1L, 2L, 3L); userMapper.deleteBatchIds(idList);6.2 逻辑删除现代应用的标配物理删除风险高数据无法恢复。因此逻辑删除已成为企业级应用的标配。MP对逻辑删除提供了近乎零配置的支持。1. 配置逻辑删除首先在实体类的删除标志字段上添加TableLogic注解。Data TableName(“user”) public class User { // ... 其他字段 TableLogic private Integer deleted; // 通常使用 Integer0未删1已删或 Boolean }然后在application.yml中配置逻辑删除的全局值mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名如果实体类注解了优先级低于注解 logic-delete-value: 1 # 逻辑已删除值默认为 1 logic-not-delete-value: 0 # 逻辑未删除值默认为 02. 使用效果配置完成后当你调用deleteById、delete等方法时MP实际执行的是UPDATE语句将deleted字段更新为1。userMapper.deleteById(1L); // 实际执行SQLUPDATE user SET deleted1 WHERE id1 AND deleted0同时所有自动生成的查询语句都会自动加上deleted0的条件确保你查不到已“删除”的数据。userMapper.selectById(1L); // 实际执行SQLSELECT * FROM user WHERE id1 AND deleted03. 查询已删除数据如果你确实需要查询包含已删除的数据可以使用Wrapper手动覆盖条件。LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getId, 1L); wrapper.last(“LIMIT 1”); // 注意直接忽略逻辑删除条件需要自己保证数据安全 // 更规范的做法是使用 SqlParser 注解或配置但MP新版已调整建议使用自定义SQL避坑指南表设计逻辑删除字段建议使用Integer类型并建立普通索引。因为deleted0的数据量最大索引能有效提升查询效率。避免使用is_deleted这种tinyint直接用deleted更简洁。唯一索引冲突这是逻辑删除最大的坑假设username字段有唯一约束用户“张三”deleted0存在。当他被逻辑删除deleted1后就无法再创建一个新的“张三”用户了因为唯一约束会冲突。解决方案有a) 删除原唯一索引建立(username, deleted)的组合唯一索引b) 使用删除时间戳字段并将唯一索引改为包含该字段c) 业务上使用其他唯一标识如UUID。性能随着时间推移表中deleted1的数据会越来越多可能影响查询性能。需要定期归档或迁移这些历史数据。7. 查询方法大全从单条到分页查询是数据库操作中最频繁的部分。MP的BaseMapper提供了丰富的查询方法足以应对90%的单表查询场景。7.1 基础查询方法selectById(Serializable id)根据主键查询。selectBatchIds(Collection? extends Serializable idList)根据主键集合批量查询。selectByMap(MapString, Object columnMap)根据column-value等值条件查询。selectOne(WrapperT queryWrapper)返回一条记录。如果结果多于一条会抛出异常。selectList(WrapperT queryWrapper)返回记录列表。selectCount(WrapperT queryWrapper)返回记录总数。selectMaps(WrapperT queryWrapper)返回ListMapString, Object适用于只查询部分字段或进行统计。selectObjs(WrapperT queryWrapper)返回ListObject通常用于查询单个字段的值列表。7.2 条件构造器QueryWrapper与LambdaQueryWrapper这是MP查询的灵魂。它允许你以Java链式调用的方式构建复杂的SQL WHERE条件。QueryWrapper普通版QueryWrapperUser wrapper new QueryWrapper(); wrapper.select(“id”, “name”, “age”) // 指定查询字段 .eq(“age”, 25) .like(“name”, “张”) .isNotNull(“email”) .orderByDesc(“create_time”) .last(“LIMIT 10”); // 谨慎使用有SQL注入风险 ListUser userList userMapper.selectList(wrapper);LambdaQueryWrapperLambda版推荐使用Lambda表达式避免了魔法值字符串编译时就能检查字段名是否正确重构友好。LambdaQueryWrapperUser lambdaWrapper new LambdaQueryWrapper(); lambdaWrapper.select(User::getId, User::getName, User::getAge) .eq(User::getAge, 25) .like(User::getName, “张”) .isNotNull(User::getEmail) .orderByDesc(User::getCreateTime) .last(“LIMIT 10”); ListUser userList userMapper.selectList(lambdaWrapper);条件构造器常用方法比较eq(),ne(),gt(),ge(),lt(),le()范围between,notBetween,in,notIn模糊like,notLike,likeLeft(%值),likeRight(值%)空值isNull,isNotNull逻辑and,or(注意wrapper.eq(…).or().eq(…)表示两个条件OR)嵌套nested(用于复杂的括号组合)函数apply(用于数据库函数如apply(“date(create_time) {0}”, “2023-10-01”))排序orderByAsc,orderByDesc分组groupByHavinghaving7.3 分页查询selectPageMP的分页功能需要配合分页插件PaginationInnerInterceptor使用。1. 配置分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 根据数据库类型选择 return interceptor; } }2. 使用分页查询// 1. 构建分页对象 PageUser page new Page(1, 10); // 查询第1页每页10条 // 可以设置是否进行count查询优化大数据量场景 // PageUser page new Page(1, 10, false); // 2. 构建查询条件可选 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.ge(User::getAge, 18); // 3. 执行分页查询 PageUser resultPage userMapper.selectPage(page, wrapper); // 4. 获取结果 ListUser records resultPage.getRecords(); // 当前页数据列表 long total resultPage.getTotal(); // 总记录数 long pages resultPage.getPages(); // 总页数分页原理插件会在执行SQL前进行拦截生成带有LIMIT和OFFSET或数据库方言对应的分页语句的查询语句并自动执行一条COUNT(*)查询来获取总数。性能提示对于超大数据量的表COUNT(*)可能会很慢。如果不需要知道精确的总数比如只做“下一页”滚动加载可以在创建Page对象时传入false禁用count查询new Page(1, 10, false)。对于复杂的联表分页查询MP的分页可能会力不从心此时需要考虑优化SQL或使用其他分页方案。7.4 自定义查询与结果映射虽然内置方法强大但复杂查询如多表关联、复杂聚合仍需自定义SQL。这可以通过在Mapper接口中定义方法并配合Select等注解或XML文件来实现。public interface UserMapper extends BaseMapperUser { // 使用注解 Select(“SELECT u.*, d.name as dept_name FROM user u LEFT JOIN department d ON u.dept_id d.id WHERE u.id #{id}”) MapString, Object selectUserWithDept(Param(“id”) Long id); // 使用XML推荐复杂SQL // 在 resources/mapper/UserMapper.xml 中编写SQL ListUserVO selectComplexUserList(Param(“param”) QueryParam param); }MP完全兼容MyBatis的原生特性你可以将内置方法的便捷性和自定义SQL的灵活性结合起来。8. 常见问题排查与实战技巧即使熟悉了所有方法在实际开发中还是会遇到各种问题。这里我总结几个高频问题和处理技巧。8.1 问题排查清单问题现象可能原因解决方案插入/更新时字段值为null不生效字段的插入/更新策略默认为NOT_NULL使用TableField(strategyIGNORED)或UpdateWrapper.set()逻辑删除配置了但不起作用1. 配置未生效如yml格式错误2. 实体类字段名与全局配置不一致3. 使用了自定义SQL未过滤1. 检查配置重启应用2. 确保注解或配置正确3. 在自定义SQL中手动添加deleted0条件调用selectOne返回多条数据报错查询条件不唯一实际匹配到多条数据确保查询条件能唯一确定一条记录或改用selectList取第一条分页查询结果不对或报错1. 未配置分页插件2. 多表联查分页SQL语法错误或性能差1. 检查分页插件配置2. 优化联查SQL或考虑先查ID再查详情使用last(“LIMIT 1”)有SQL注入风险last方法直接拼接SQL片段避免使用用户输入拼接或使用apply方法进行预编译处理实体类字段名与数据库列名不一致未使用TableField注解指定映射在字段上加TableField(value “db_column_name”)查询速度突然变慢1. 未加索引2. 逻辑删除表数据量过大3. 查询条件未走索引1. 分析SQL执行计划添加合适索引2. 归档历史数据3. 优化查询条件避免对索引列进行函数操作8.2 实战性能优化技巧选择性查询默认selectList(wrapper)会查询所有字段SELECT *。使用wrapper.select(…)指定需要的字段能减少网络传输和数据库压力。善用索引确保QueryWrapper中作为查询条件的字段特别是eq、in、range条件已经建立了合适的索引。使用explain命令分析生成的SQL。批量操作对于大量数据插入使用MP的saveBatch方法在IService中它背后通常做了优化如拼接批量插入语句比循环调用insert快得多。避免N1查询在查询列表如用户列表后如果需要关联信息如用户所属部门不要遍历列表在循环中查询。应使用Select注解或XML编写联表查询一次获取所有数据或者使用MP的“查询结果映射”功能相对复杂。监控生成的SQL在开发环境开启MP的SQL日志输出可以清晰看到最终执行的SQL语句是调试和优化的利器。mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印完整SQL带参数8.3 关于“查询方法运行时间”的思考热搜词中提到了“查询方法运行时间”这本质上是SQL性能监控问题。MP本身不直接提供该方法运行时间的统计。但在实际项目中监控SQL耗时至关重要。实现方案使用Druid连接池配置Druid的filters包含stat可以监控到每条SQL的执行时间、执行次数等。使用Spring AOP自定义一个切面拦截Mapper接口的方法在方法执行前后记录时间并打印或发送到监控系统。使用Micrometer Actuator集成Spring Boot Actuator和Micrometer可以暴露jdbc.connections等指标配合Prometheus和Grafana进行可视化监控。应用性能管理APM工具如SkyWalking、Pinpoint等它们可以自动埋点无侵入地监控到数据库调用链和耗时。最直接的方式还是在开发阶段通过MP的SQL日志结合数据库的慢查询日志如MySQL的long_query_time来定位和优化慢SQL。掌握MyBatis-Plus的内置方法就像是掌握了一套强大的单表操作“组合拳”。它能让你在日常开发中游刃有余将精力更多地集中在复杂的业务逻辑上而不是重复的SQL编写上。从配置、使用到避坑、优化每一步都需要结合具体的业务场景来思考。记住工具是为人服务的理解其原理和设计意图才能用得恰到好处真正提升开发效率和系统质量。