尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MyBatis-Plus 核心 CRUD 方法深度解析与实战避坑指南

MyBatis-Plus 核心 CRUD 方法深度解析与实战避坑指南 1. 项目概述为什么我们需要 MyBatis-Plus 的内置方法如果你用过原生的 MyBatis肯定对写不完的 XML 映射文件和那些重复的增删改查 SQL 感到头疼。每次新加一个实体insert、updateById、selectById、deleteById这些方法几乎都要重写一遍虽然简单但极其枯燥且容易出错。MyBatis-Plus简称 MP的出现就是为了把开发者从这种“重复造轮子”的泥潭里拉出来。它的核心价值之一就是提供了一套开箱即用的通用 Mapper 接口里面封装了绝大多数单表操作的方法。今天要聊的就是这套内置方法里的“硬核干货”——插入、更新、删除、查询四大核心操作。这不仅仅是知道方法名怎么调用那么简单更重要的是理解每个方法背后的行为逻辑、使用场景以及那些官方文档里可能不会明说的“坑”。比如updateById能不能把字段更新为nullsaveOrUpdate到底是怎么判断该插入还是更新的批量操作时怎么选才能兼顾性能和安全性这些才是日常开发中真正决定效率和质量的关键点。无论你是刚接触 MP 想快速上手还是已经用过一段时间想深入优化理清这些内置方法的门道都能让你的代码更健壮开发更顺畅。2. 核心设计思想与接口体系解析在深入每个方法之前我们必须先理解 MyBatis-Plus 的顶层设计。它并非简单地提供一堆静态工具方法而是构建了一个基于接口和 Service 层的完整体系。2.1 BaseMapper 与 IService 的分工MP 提供了两套核心接口BaseMapperT和IServiceT。很多新手会困惑我该用哪个BaseMapperT是数据访问层DAO的基石。它直接继承自 MyBatis 的Mapper接口所有方法最终都会转换为对应的 SQL 语句。它更“底层”更贴近数据库操作。当你需要一个非常纯粹、直接的数据库交互入口时就使用它。例如在你的UserMapper.java中你会这样定义public interface UserMapper extends BaseMapperUser { // 这里可以定义自定义的复杂SQL方法 }IServiceT则是业务逻辑层的抽象。它内部封装了一个BaseMapper对象并在其基础上提供了更多面向业务、功能更丰富的方法比如链式查询、批量操作等。它更“高级”更适合在 Service 层使用。通常我们会有一个对应的实现类public interface UserService extends IServiceUser { // 自定义业务方法 } Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { // 实现自定义业务方法 }选择策略对于简单的单表 CRUD在 Service 层注入IService来调用会更方便因为它提供了更多语法糖和批量操作支持。而对于需要高度定制化 SQL 或复杂联查的场景你依然可以在Mapper接口中定义自己的方法并通过ServiceImpl中的getBaseMapper()方法获取到底层的BaseMapper来执行。理解这种分工能让你在架构上更清晰地组织代码。2.2 实体类与数据库映射的约定MP 内置方法能智能工作的前提是你的实体类遵循一定的约定。最重要的两个注解是TableId和TableField。TableId用于标识主键字段。value属性指定数据库主键列名如果字段名遵循驼峰转下划线规则可省略。type属性决定主键生成策略这是影响插入行为的关键IdType.AUTO数据库自增最常用。IdType.NONE无状态需要手动设置。IdType.INPUT用户输入。IdType.ASSIGN_IDMP 分配一个长整型 ID默认基于雪花算法。IdType.ASSIGN_UUIDMP 分配一个 UUID 字符串。TableField用于映射非主键字段。有几个关键属性value数据库列名。exist是否为数据库表字段默认为true。设置为false表示该字段不对应数据库列。fill字段自动填充策略这是实现“创建时间”、“更新时间”等字段自动化的神器我们会在更新方法部分详细讲。insertStrategy和updateStrategy字段的插入/更新策略这是控制null值是否参与 SQL 生成的核心也是解决“updateById不能更新为null”问题的关键。注意实体类中的字段名到数据库列名的默认转换策略是驼峰命名转下划线命名。例如userName字段默认对应user_name列。如果数据库设计风格不一致务必使用TableField(value “...”)显式指定。3. 插入方法详解与实战插入操作是将新数据持久化到数据库的第一步。MP 提供了最基础的insert方法但其中有许多细节值得推敲。3.1 insert 方法的核心行为BaseMapper中的插入方法签名很简单int insert(T entity);。它的行为可以概括为尝试将实体对象entity的所有属性除了标记为existfalse的插入到对应的数据库表中。执行流程与返回值MP 会检查实体类的主键策略TableId(type ...)。如果策略是ASSIGN_ID或ASSIGN_UUIDMP 会先为主键字段生成一个值并设置到实体对象中。根据TableField的insertStrategy等规则动态生成INSERT INTO table (column1, column2, ...) VALUES (?, ?, ...)语句。执行 SQL返回受影响的行数通常为 1。一个容易被忽略的细节即使插入失败例如主键冲突这个insert方法也不会抛出异常除非是 SQL 语法错误等运行时异常。它只会返回0。因此绝对不能仅凭返回值大于 0 就认为业务意义上的“插入成功”。对于需要确保数据唯一性的场景如用户注册插入后必须通过其他方式如查询进行验证。3.2 主键生成策略的抉择与陷阱主键策略的选择直接影响插入行为和后续的数据一致性。AUTO(数据库自增)最通用。插入后数据库生成的主键值会自动回填到实体对象的id字段中前提是你的数据库驱动支持如 MySQL。这是获取自增 ID 最优雅的方式。User user new User(); user.setName(“张三”); user.setAge(20); userMapper.insert(user); // 执行插入 System.out.println(“插入成功用户ID” user.getId()); // 此时 id 已被回填ASSIGN_ID(雪花算法)分布式场景下的首选。ID 在调用insert方法前就已生成并设置到对象里。优势全局唯一、趋势递增、无需数据库交互。注意生成的 ID 是长整型在 JavaScript 前端处理时可能因精度丢失需要转为字符串。ASSIGN_UUID生成字符串类型的 UUID。优势绝对唯一。劣势作为主键时如果表数据量极大由于 UUID 的无序性可能导致索引碎片化影响写入性能。通常建议在特定场景如作为业务编号下使用而非主键。实操心得对于全新的项目如果确认是单数据源且无需分库分表用AUTO最简单。但凡有一点分布式或数据迁移的考虑ASSIGN_ID都是更稳妥的选择。一旦表数据量上来再想从AUTO切换到分布式 ID会非常痛苦。3.3 批量插入的性能考量BaseMapper本身没有提供批量插入方法。批量插入通常通过IService接口的saveBatch方法实现。// 在 Service 层使用 ListUser userList ... // 待插入的用户列表 boolean success userService.saveBatch(userList);saveBatch方法默认会将列表拆分成每 1000 条一条 SQL 进行批量插入INSERT INTO ... VALUES (), (), ...。这个批处理大小可以通过重载方法saveBatch(CollectionT entityList, int batchSize)自定义。性能陷阱SQL 长度限制一条 SQL 语句有长度限制如 MySQL 的max_allowed_packet。如果单次批量插入的数据行数过多或字段内容过大可能导致 SQL 过长而执行失败。因此合理设置batchSize比如 500 或 1000很重要。事务边界saveBatch方法默认不开启事务。如果中途某条数据插入失败之前已经成功的数据不会被回滚。这可能导致数据不一致。正确的做法是在调用批量插入的 Service 方法上添加Transactional注解确保整个批量操作原子性。Transactional(rollbackFor Exception.class) public boolean batchInsertUsers(ListUser userList) { return saveBatch(userList); }字段过滤批量插入时MP 会以列表中的第一个实体对象为模板生成插入字段列表。这意味着如果列表中后续对象的某个字段为null而第一个对象该字段不为null生成的 SQL 依然会包含该字段并为其插入null值。这符合预期但需要注意。4. 更新方法深度剖析更新操作是业务逻辑中最频繁的操作之一MP 的更新方法看似简单实则暗藏玄机。4.1 updateById最常用的更新及其“null值”难题int updateById(T entity);这是根据主键进行更新的方法。你传入一个实体对象MP 会生成UPDATE table SET column1?, column2? WHERE id?的 SQL。核心问题它默认不会更新字段为null的值这是 MP 的默认字段更新策略FieldStrategy在起作用。默认策略是NOT_NULL即当字段值为null时该字段不会加入到 SET 语句中。这样设计是为了避免在部分更新时意外地用null覆盖掉数据库里原有的值。场景还原假设你想把某个用户的email清空设为null。User user new User(); user.setId(1L); user.setEmail(null); // 希望将邮箱更新为 null userMapper.updateById(user);默认情况下生成的 SQL 是UPDATE user WHERE id1根本没有SET emailnull部分更新不会生效。解决方案有三种各有适用场景全局配置谨慎使用在application.yml中设置全局的字段策略。这会影响所有实体所有字段风险较高一般不推荐。mybatis-plus: global-config: db-config: update-strategy: ignored # 设置为忽略判断null值也会参与更新实体类字段注解推荐在需要支持更新为null的字段上使用TableField注解单独设置策略。public class User { TableField(updateStrategy FieldStrategy.IGNORED) private String email; }FieldStrategy.IGNORED表示忽略判断无论字段值是否为null都会加入 SQL。这是最精准的控制方式。使用 UpdateWrapper灵活通过UpdateWrapper来构建更新条件它可以显式地设置SET column NULL。UpdateWrapperUser updateWrapper new UpdateWrapper(); updateWrapper.eq(“id”, 1L).set(“email”, null); userMapper.update(null, updateWrapper); // 第一个参数为 null 的实体这种方式最灵活适合在特定业务逻辑中动态处理。避坑指南对于像“更新时间”updateTime这样的字段我们通常希望每次更新都自动设置为当前时间无论其他字段变不变。这时应该使用TableField(fill FieldFill.UPDATE)配合**元对象处理器MetaObjectHandler**来实现自动填充而不是依赖更新策略。4.2 条件更新update(T entity, Wrapper updateWrapper)这是功能更强大的更新方式。entity对象用于设置新的值Wrapper用于构造复杂的WHERE条件。经典场景给所有年龄小于 18 岁的用户账户增加 100 积分。User updateEntity new User(); updateEntity.setPoints(100); // 注意这里是设置值不是增量 UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.lt(“age”, 18); // WHERE age 18 // 错误写法userMapper.update(updateEntity, wrapper); 这会将积分 SET 为 100而不是增加100。 // 正确写法使用 setSql 进行字段表达式更新 wrapper.setSql(“points points 100”); // SET points points 100 userMapper.update(null, wrapper); // entity 参数传 null 即可这个例子清晰地展示了entity参数和Wrapper的setSql方法的区别entity用于设置静态值setSql用于执行字段表达式运算。4.3 字段自动填充FieldFill最佳实践自动填充是提升开发体验、保证数据一致性的利器。常用于create_time,update_time,create_by,update_by等字段。实现步骤在实体字段上标记填充策略Data public class User { TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableField(fill FieldFill.INSERT) private Long createUserId; TableField(fill FieldFill.INSERT_UPDATE) private Long updateUserId; }实现MetaObjectHandler接口创建一个配置类实现插入和更新时的填充逻辑。Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, “createTime”, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, “updateTime”, LocalDateTime.class, LocalDateTime.now()); // 如何获取当前用户ID通常从线程上下文如SecurityContextHolder获取 Long currentUserId getCurrentUserId(); this.strictInsertFill(metaObject, “createUserId”, Long.class, currentUserId); this.strictInsertFill(metaObject, “updateUserId”, Long.class, currentUserId); } Override public void updateFill(MetaObject metaObject) { // 注意更新填充时要避免覆盖已有的值。strictUpdateFill方法会判断字段是否已有值。 this.strictUpdateFill(metaObject, “updateTime”, LocalDateTime.class, LocalDateTime.now()); Long currentUserId getCurrentUserId(); this.strictUpdateFill(metaObject, “updateUserId”, Long.class, currentUserId); } private Long getCurrentUserId() { // 实现你的用户信息获取逻辑例如从Spring Security上下文 // 返回一个默认值或抛出异常具体看业务 return 1L; } }关键点使用strictInsertFill和strictUpdateFill可以避免属性类型不匹配的运行时错误并且strictUpdateFill会检查字段是否已被赋值即update时传入的 entity 对象里该字段是否不为null如果已赋值则不再填充这符合部分更新的预期。5. 插入或更新方法saveOrUpdate 的智慧IService接口提供了saveOrUpdate(T entity)和saveOrUpdateBatch(CollectionT entityList)方法。这个方法名听起来很智能——“有则更新无则插入”。但它到底如何判断判断逻辑MP 的判断依据非常直接——检查实体对象的主键字段是否有值。如果主键有值不为null且不为空对于数字类型不为 0MP 会尝试执行更新。更新成功受影响行数0则结束如果更新失败例如该主键在数据库中不存在则会转而执行插入。如果主键无值则直接执行插入。这个逻辑存在一个潜在风险假设你的主键是数据库自增IdType.AUTO但你手动设置了一个数据库中不存在的id比如从别处拷贝来的一个随机ID。调用saveOrUpdate时因为主键有值它会先执行UPDATE。由于记录不存在更新行数为 0然后它会再执行INSERT。由于你手动设置了id且数据库是自增主键这个INSERT很可能会失败主键冲突或者即使成功也可能打乱数据库的自增序列。重要建议对于使用数据库自增主键的表谨慎使用saveOrUpdate。更安全的做法是在业务逻辑中明确判断先根据业务唯一键如用户名、手机号查询如果存在则获取其主键进行更新不存在则执行插入。saveOrUpdate更适用于主键是ASSIGN_ID雪花ID或INPUT手动输入的场景因为主键的生成和存在性由程序控制。批量saveOrUpdateBatch其内部逻辑是对集合中的每个实体串行或并行取决于配置地执行上述saveOrUpdate逻辑。同样需要注意主键策略带来的问题。6. 删除方法物理删除与逻辑删除删除操作关乎数据安全。MP 提供了物理删除和逻辑删除两种方式。6.1 物理删除deleteById, deleteByMap, delete(Wrapper)这些都是直接从数据库删除记录的“硬删除”。int deleteById(Serializable id);// 按主键删int deleteByMap(Param(“cm”) MapString, Object columnMap);// 按 columnvalue 条件删int delete(Param(“ew”) WrapperT wrapper);// 按条件构造器删风险物理删除不可逆一旦执行数据恢复困难。在生产环境中对于核心业务数据应尽量避免使用。6.2 逻辑删除优雅的数据“隐身术”逻辑删除是行业最佳实践。它并不真正删除数据而是通过一个标记字段如is_deleted来标识记录是否被删除。配置逻辑删除数据库表添加一个标记字段例如deletedtinyint默认值为 0。实体类在对应字段上添加TableLogic注解。TableLogic private Integer deleted; // 0-未删除1-已删除全局配置可选在application.yml中配置逻辑删除的默认值。mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值启用逻辑删除后的效果删除操作调用deleteById等方法实际执行的是UPDATE table SET deleted1 WHERE id? AND deleted0。查询操作MP 会自动在所有SELECT语句的WHERE条件中加上AND deleted0。这意味着你通过 MP 内置查询方法查到的数据永远都是“未删除”的。需要查询已删除数据怎么办你需要自己编写 SQL或者在Wrapper中手动过滤掉deleted条件这很麻烦。因此更常见的做法是为已删除数据建立单独的归档表或查询视图。逻辑删除的注意事项唯一索引冲突如果表上对“用户名”等字段设置了唯一索引用户删除deleted1后新用户再用同样的用户名注册就会冲突。解决方案是将唯一索引改为包含deleted字段的复合唯一索引如UNIQUE KEY uk_username (username, deleted)但需要确保deleted为非null通常deleted0代表有效数据。性能影响所有查询都附带deleted0条件对性能有轻微影响但可接受。对于极度追求性能的场景需权衡。7. 查询方法全解与性能优化查询是数据库操作中最复杂的部分。MP 内置的查询方法提供了极大的便利但也需要正确使用以避免性能问题。7.1 基础查询方法速览selectById(Serializable id)按主键查返回单个实体。selectBatchIds(Collection? extends Serializable idList)按主键集合批量查。注意如果集合很大生成的 SQLIN (...)语句会很长可能超出数据库限制或导致性能下降。对于大批量ID查询建议分页或改用其他方式。selectByMap(MapString, Object columnMap)根据等值条件查。key是字段名value是字段值。只能做AND连接的多字段等值查询。selectOne(WrapperT queryWrapper)返回单个实体。重要如果查询结果多于一条会抛出TooManyResultsException。务必确保Wrapper能唯一确定一条记录如通过唯一键查询。selectList(WrapperT queryWrapper)返回实体列表。selectMaps(WrapperT queryWrapper)返回ListMapString, Object。当只需要少数几个字段或者查询结果无法映射到实体类时使用可以避免不必要的字段映射开销。selectObjs(WrapperT queryWrapper)返回ListObject通常用于查询单个字段的值列表如SELECT id FROM user。selectCount(WrapperT queryWrapper)返回记录数用于分页或统计。7.2 QueryWrapper构建动态查询条件的利器QueryWrapper是 MP 查询的灵魂它允许你以链式编程的方式构建复杂的WHERE条件。常用条件构造方法eq(“column”, value)等于ne(“column”, value)不等于gt(“column”, value)大于ge(“column”, value)大于等于lt(“column”, value)小于le(“column”, value)小于等于like(“column”, value)模糊查询LIKE ‘%value%’leftLike(“column”, value)LIKE ‘value%’rightLike(“column”, value)LIKE ‘%value’in(“column”, Collection)IN (value1, value2, ...)between(“column”, value1, value2)BETWEEN value1 AND value2isNull(“column”)/isNotNull(“column”)groupBy(“column1”, “column2”)分组orderByAsc(“column”)/orderByDesc(“column”)排序last(“LIMIT 1”)拼接 SQL 语句的最后一部分谨慎使用有 SQL 注入风险。链式调用示例查询年龄在18到30之间姓名包含“张”并且状态为激活的用户按创建时间倒序排列。QueryWrapperUser queryWrapper new QueryWrapper(); queryWrapper.between(“age”, 18, 30) .like(“name”, “张”) .eq(“status”, 1) .orderByDesc(“create_time”); ListUser userList userMapper.selectList(queryWrapper);7.3 LambdaQueryWrapper类型安全与编译时检查LambdaQueryWrapper使用函数式引用来引用实体字段避免了硬编码字符串带来的拼写错误风险重构也更友好。LambdaQueryWrapperUser lambdaQueryWrapper new LambdaQueryWrapper(); lambdaQueryWrapper.between(User::getAge, 18, 30) .like(User::getName, “张”) .eq(User::getStatus, 1) .orderByDesc(User::getCreateTime); ListUser userList userMapper.selectList(lambdaQueryWrapper);强烈推荐在项目中使用LambdaQueryWrapper它能极大提升代码的健壮性和可维护性。7.4 分页查询Page 对象与性能陷阱MP 提供了强大的分页插件PaginationInnerInterceptor需要先配置。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 根据数据库类型选择 return interceptor; } }使用分页// 构造分页参数查第2页每页10条 PageUser page new Page(2, 10); // 构造查询条件 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getStatus, 1); // 执行分页查询 PageUser resultPage userMapper.selectPage(page, wrapper); // 获取分页数据 ListUser records resultPage.getRecords(); // 当前页数据列表 long total resultPage.getTotal(); // 总记录数 long pages resultPage.getPages(); // 总页数性能陷阱selectPage会执行两条 SQLSELECT COUNT(*) FROM table WHERE ...查询总数SELECT * FROM table WHERE ... LIMIT ?, ?查询当前页数据问题在复杂查询特别是多表关联或条件复杂时COUNT查询可能非常慢拖累整个分页性能。优化方案方案一不查询总数。如果业务不需要知道总页数只关心“是否有下一页”可以使用Page的setSearchCount(false)方法。PageUser page new Page(2, 10); page.setSearchCount(false); // 关闭 count 查询 userMapper.selectPage(page, wrapper); // 此时 page.getTotal() 为 0但 records 是当前页数据。 // 判断是否有下一页如果返回的记录数等于 pageSize则可能还有下一页否则就是最后一页。方案二手动优化 COUNT 语句。对于极端复杂的查询可以自定义一个selectPage方法在 XML 中编写优化后的 COUNT 查询 SQL或者使用缓存等手段。7.5 查询性能优化核心避免 SELECT *善用 select(...)MP 的selectList()等方法默认行为是SELECT *。这是最需要警惕的性能杀手之一尤其是在表字段多、有大字段如TEXT,BLOB的情况下。优化方法使用select(...)方法显式指定查询字段。LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getStatus, 1) .select(User::getId, User::getName, User::getAge); // 只查询 id, name, age 三个字段 ListUser userList userMapper.selectList(wrapper);或者使用字符串形式不推荐易出错QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(“status”, 1) .select(“id”, “name”, “age”);selectMaps的用武之地当你只需要几个字段且不想创建专门的 DTO/VO 对象时selectMaps是绝佳选择。它返回ListMapString, Object每个Map的key是字段名或别名value是字段值。QueryWrapperUser wrapper new QueryWrapper(); wrapper.select(“id”, “name as username”, “age”).eq(“status”, 1); ListMapString, Object list userMapper.selectMaps(wrapper); // 遍历 list通过 map.get(“username”) 获取数据这种方式完全避免了实体类的映射开销在简单的报表、下拉列表等场景下非常高效。8. 实战避坑与高级技巧掌握了基本方法再来看看那些容易踩坑和能体现功力的地方。8.1 Wrapper 的使用误区与正确姿势条件值为null的处理wrapper.eq(“name”, null)生成的 SQL 是name null这在 SQL 中是无法匹配到任何结果的需要用IS NULL。MP 的eq等方法在传入值为null时默认会忽略该条件。如果你确实需要查询IS NULL应该使用wrapper.isNull(“name”)。链式调用的顺序Wrapper 的条件是顺序叠加的都是AND关系。如果需要OR条件要使用or()方法。wrapper.eq(“type”, 1) .or() // 注意 or() 的位置它将其前后的条件用 OR 连接 .eq(“type”, 2) .like(“name”, “A”); // 生成的 SQL: WHERE (type 1) OR (type 2 AND name LIKE ‘%A%’) // 如果想生成 (type1 OR type2) AND name LIKE ‘%A%’需要嵌套 wrapper.nested(wq - wq.eq(“type”, 1).or().eq(“type”, 2)) .like(“name”, “A”);apply方法的使用与 SQL 注入风险apply用于拼接 SQL 片段要绝对警惕 SQL 注入。// 危险直接拼接用户输入 String userInput “1; DROP TABLE user;”; wrapper.apply(“id “ userInput); // 正确使用预编译占位符 {0} wrapper.apply(“date(create_time) {0}”, “2023-10-01”);8.2 多租户数据隔离的实现思路“多租户”是一个常见需求即一套系统为多个客户租户服务数据需要严格隔离。MP 提供了TenantLineInnerInterceptor拦截器来实现行级数据隔离。核心思想在每个需要隔离的表上增加一个租户ID字段如tenant_id。MP 拦截所有增删改查的 SQL自动在WHERE条件中加上AND tenant_id ?在INSERT时自动填充tenant_id。配置示例实现TenantLineHandler接口告诉 MP 当前租户ID是什么以及哪些表需要过滤。Component public class MyTenantLineHandler implements TenantLineHandler { Override public Expression getTenantId() { // 从当前请求上下文如ThreadLocal获取租户ID String tenantId TenantContext.getCurrentTenantId(); return new StringValue(tenantId); } Override public String getTenantIdColumn() { return “tenant_id”; // 租户ID字段名 } Override public boolean ignoreTable(String tableName) { // 返回 true 表示此表不需要多租户过滤如系统字典表 return “sys_dict”.equalsIgnoreCase(tableName); } }将多租户拦截器添加到MybatisPlusInterceptor中。interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MyTenantLineHandler()));注意事项多租户过滤是全局性的对于某些需要跨租户查询的后台管理功能需要使用 MP 的disableTenantFilter()方法在特定Mapper方法上临时关闭过滤但这需要非常谨慎的权限控制。8.3 监控查询方法运行时间这是一个非常实用的调试和优化技巧。虽然 MP 本身不直接提供但我们可以利用 Spring 的 AOP 或 MyBatis 的插件机制轻松实现。使用 MyBatis 的Interceptor推荐Intercepts({Signature(type StatementHandler.class, method “query”, args {Statement.class, ResultHandler.class})}) Component public class SqlCostInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { long startTime System.currentTimeMillis(); try { return invocation.proceed(); } finally { long endTime System.currentTimeMillis(); long cost endTime - startTime; StatementHandler statementHandler (StatementHandler) invocation.getTarget(); BoundSql boundSql statementHandler.getBoundSql(); String sql boundSql.getSql(); if (cost 200) { // 超过200毫秒的慢查询 log.warn(“慢SQL执行耗时{} ms SQL: {}”, cost, sql); } else { log.debug(“SQL执行耗时{} ms”, cost); } } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { } }将这个拦截器配置到 Spring 容器它就会自动打印所有 SQL 的执行时间帮助你快速定位性能瓶颈。从最基本的insert、updateById到复杂的条件构造器QueryWrapper和分页查询每一个方法背后都有其设计哲学和最佳实践。真正用好 MyBatis-Plus关键在于理解这些默认行为背后的“为什么”并知道在什么场景下该用什么方法以及如何规避其中的陷阱。比如用LambdaQueryWrapper代替字符串QueryWrapper来保证类型安全在分页时考虑关闭count查询来优化性能对于核心数据坚决使用逻辑删除而非物理删除。把这些细节做到位你的数据层代码就会既简洁又健壮。最后多关注 SQL 日志那是检验 ORM 使用是否得当的最直观窗口。
返回列表