MyBatisPlus就像一位精通厨艺的帮厨,它帮你处理了所有繁琐的准备工作。想要一个复杂的查询?不用自己一刀一刀地切肉、一勺一勺地调味,框架已经帮你准备好了。你只需要轻轻地指挥,代码就像汤汁一样顺滑流畅,性能更是鲜美可口。
在接下来的篇幅里,我将与你分享12个MyBatisPlus优化的"秘制配方"。相信看完这些,你写的每一行代码,都会像外婆的羊肉汤一样,让人回味无穷。
耐心看完,你一定有所收获。
避免使用isNull判断
// ❌ 不推荐LambdaQueryWrapper<User> wrapper1 = new LambdaQueryWrapper<>();wrapper1.isNull(User::getStatus);// ✅ 推荐:使用具体的默认值LambdaQueryWrapper<User> wrapper2 = new LambdaQueryWrapper<>();wrapper2.eq(User::getStatus, UserStatusEnum.INACTIVE.getCode());
📝 原因: 使用具体的默认值可以提高代码的可读性和维护性 NULL值会使索引失效,导致MySQL无法使用索引进行查询优化 NULL值的比较需要特殊的处理逻辑,增加了CPU开销 NULL值会占用额外的存储空间,影响数据压缩效率
明确Select字段
// ❌ 不推荐// 默认select 所有字段List<User> users1 = userMapper.selectList(null);// ✅ 推荐:指定需要的字段LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.select(User::getId, User::getName, User::getAge);List<User> users2 = userMapper.selectList(wrapper);
📝 原因: 避免大量无用字段的网络传输开销 可以利用索引覆盖,避免回表查询 减少数据库解析和序列化的负担 降低内存占用,特别是在大量数据查询时
批量操作方法替代循环
// ❌ 不推荐for (User user : userList) {userMapper.insert(user);}// ✅ 推荐userService.saveBatch(userList, 100); // 每批次处理100条数据// ✅ 更优写法:自定义批次大小userService.saveBatch(userList, BatchConstants.BATCH_SIZE);
📝 原因: 减少数据库连接的创建和销毁开销 批量操作可以在一个事务中完成,提高数据一致性 数据库可以优化批量操作的执行计划 显著减少网络往返次数,提升吞吐量
Exists方法子查询
// ❌ 不推荐wrapper.inSql("user_id", "select user_id from order where amount > 1000");// ✅ 推荐wrapper.exists("select 1 from order where order.user_id = user.id and amount > 1000");// ✅ 更优写法:使用LambdaQueryWrapperwrapper.exists(orderService.lambdaQuery().gt(Order::getAmount, 1000).apply("order.user_id = user.id"));
📝 原因: EXISTS是基于索引的快速查询,可以使用到索引 EXISTS在找到第一个匹配项就会停止扫描 IN子查询需要加载所有数据到内存后再比较 当外表数据量大时,EXISTS的性能优势更明显
使用orderBy代替last
// ❌ 不推荐:SQL注入风险wrapper.last("ORDER BY " + sortField + " " + sortOrder);// ❌ 不推荐:直接字符串拼接wrapper.last("ORDER BY FIELD(status, 'active', 'pending', 'inactive')");// ✅ 推荐:使用 Lambda 安全排序wrapper.orderBy(true, true, User::getStatus);// ✅ 推荐:多字段排序示例wrapper.orderByAsc(User::getStatus).orderByDesc(User::getCreateTime);
📝 原因: 直接拼接SQL容易导致SQL注入攻击 动态SQL可能破坏SQL语义完整性 影响SQL语句的可维护性和可读性 last会绕过MyBatis-Plus的安全检查机制
使用LambdaQuery确保类型安全
// ❌ 不推荐:字段变更后可能遗漏QueryWrapper<User> wrapper1 = new QueryWrapper<>();wrapper1.eq("name", "张三").gt("age", 18);// ✅ 推荐LambdaQueryWrapper<User> wrapper2 = new LambdaQueryWrapper<>();wrapper2.eq(User::getName, "张三").gt(User::getAge, 18);// ✅ 更优写法:使用链式调用userService.lambdaQuery().eq(User::getName, "张三").gt(User::getAge, 18).list();
📝 原因: 编译期类型检查,避免字段名拼写错误 IDE可以提供更好的代码补全支持 重构时能自动更新字段引用 提高代码的可维护性和可读性
用between代替ge和le
// ❌ 不推荐wrapper.ge(User::getAge, 18).le(User::getAge, 30);// ✅ 推荐wrapper.between(User::getAge, 18, 30);// ✅ 更优写法:条件动态判断wrapper.between(ageStart != null && ageEnd != null,User::getAge, ageStart, ageEnd);
📝 原因: 生成的SQL更简洁,减少解析开销 数据库优化器可以更好地处理范围查询 代码更易读,语义更清晰 减少重复编写字段名的机会
排序字段注意索引
// ❌ 不推荐// 假设lastLoginTime无索引wrapper.orderByDesc(User::getLastLoginTime);// ✅ 推荐// 主键排序wrapper.orderByDesc(User::getId);// ✅ 更优写法:组合索引排序wrapper.orderByDesc(User::getStatus) // status建立了索引.orderByDesc(User::getId); // 主键排序
📝 原因: 索引天然具有排序特性,可以避免额外的排序操作 无索引排序会导致文件排序,极大影响性能 当数据量大时,内存排序可能导致溢出 利用索引排序可以实现流式读取
分页参数设置
// ❌ 不推荐wrapper.last("limit 1000"); // 一次查询过多数据// ✅ 推荐Page<User> page = new Page<>(1, 10);userService.page(page, wrapper);// ✅ 更优写法:带条件的分页查询Page<User> result = userService.lambdaQuery().eq(User::getStatus, "active").page(new Page<>(1, 10));
📝 原因: 控制单次查询的数据量,避免内存溢出 提高首屏加载速度,优化用户体验 减少网络传输压力 数据库资源利用更合理
条件构造处理Null值
// ❌ 不推荐if (StringUtils.isNotBlank(name)) {wrapper.eq("name", name);}if (age != null) {wrapper.eq("age", age);}// ✅ 推荐wrapper.eq(StringUtils.isNotBlank(name), User::getName, name).eq(Objects.nonNull(age), User::getAge, age);// ✅ 更优写法:结合业务场景wrapper.eq(StringUtils.isNotBlank(name), User::getName, name).eq(Objects.nonNull(age), User::getAge, age).eq(User::getDeleted, false) // 默认查询未删除记录.orderByDesc(User::getCreateTime); // 默认按创建时间倒序
📝 原因: 优雅处理空值,避免无效条件 减少代码中的if-else判断 提高代码可读性 防止生成冗余的SQL条件
⚠️ 下面就要来一些高级货了
查询性能追踪
// ❌ 不推荐:简单计时,代码冗余public List<User> listUsers(QueryWrapper<User> wrapper) {long startTime = System.currentTimeMillis();List<User> users = userMapper.selectList(wrapper);long endTime = System.currentTimeMillis();log.info("查询耗时:{}ms", (endTime - startTime));return users;}// ✅ 推荐:使用 Try-with-resources 自动计时public List<User> listUsersWithPerfTrack(QueryWrapper<User> wrapper) {try (PerfTracker.TimerContext ignored = PerfTracker.start()) {return userMapper.selectList(wrapper);}}// 性能追踪工具类@Slf4jpublic class PerfTracker {private final long startTime;private final String methodName;private PerfTracker(String methodName) {this.startTime = System.currentTimeMillis();this.methodName = methodName;}public static TimerContext start() {return new TimerContext(Thread.currentThread().getStackTrace()[2].getMethodName());}public static class TimerContext implements AutoCloseable {private final PerfTracker tracker;private TimerContext(String methodName) {this.tracker = new PerfTracker(methodName);}@Overridepublic void close() {long executeTime = System.currentTimeMillis() - tracker.startTime;if (executeTime > 500) {log.warn("慢查询告警:方法 {} 耗时 {}ms", tracker.methodName, executeTime);}}}}
📝 原因: 业务代码和性能监控代码完全分离 try-with-resources 即使发生异常,close() 方法也会被调用,确保一定会记录耗时 不需要手动管理计时的开始和结束 更优雅
枚举类型映射
// 定义枚举public enum UserStatusEnum {NORMAL(1, "正常"),DISABLED(0, "禁用");@EnumValue // MyBatis-Plus注解private final Integer code;private final String desc;}// ✅ 推荐:自动映射public class User {private UserStatusEnum status;}// 查询示例userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getStatus, UserStatusEnum.NORMAL));
📝 原因: 类型安全 自动处理数据库和枚举转换 避免魔法值 代码可读性更强
自动处理逻辑删除
@TableLogic // 逻辑删除注解private Integer deleted;// ✅ 推荐:自动过滤已删除数据public List<User> getActiveUsers() {return userMapper.selectList(null); // 自动过滤deleted=1的记录}// 手动删除userService.removeById(1L); // 实际是更新deleted状态
📝 原因: 数据不丢失 查询自动过滤已删除数据 支持数据恢复 减少手动编写删除逻辑 📷 注意: XML中需要手动拼接 deleted = 1
乐观锁更新保护
public class Product {@Version // 乐观锁版本号private Integer version;}// ✅ 推荐:更新时自动处理版本public boolean reduceStock(Long productId, Integer count) {LambdaUpdateWrapper<Product> wrapper = new LambdaUpdateWrapper<>();wrapper.eq(Product::getId, productId).ge(Product::getStock, count);Product product = new Product();product.setStock(product.getStock() - count);return productService.update(product, wrapper);}
📝 原因: 防止并发冲突 自动处理版本控制 简化并发更新逻辑 提高数据一致性
递增和递减:setIncrBy 和 setDecrBy
// ❌ 不推荐:使用 setSqluserService.lambdaUpdate().setSql("integral = integral + 10").update();// ✅ 推荐:使用 setIncrByuserService.lambdaUpdate().eq(User::getId, 1L).setIncrBy(User::getIntegral, 10).update();// ✅ 推荐:使用 setDecrByuserService.lambdaUpdate().eq(User::getId, 1L).setDecrBy(User::getStock, 5).update();
📝 原因: 类型安全 避免手动拼接sql,防止sql注入 代码可维护性更强,更清晰
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




