⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7436567167728812044 「一只叫煤球的猫」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

说起数据库ORM,我忽然想起了小时候外婆做的那锅鲜美的羊肉汤。平常人家做的羊肉汤无非是几块肉、几片姜,味道寡淡得很,喝了和喝白开水差不多。但外婆的汤,那是另一回事儿 —— 一锅汤,香气四溢,肉质软烂,汤头浓郁得能让人连碗都想舔干净。

写代码何尝不是如此?以前写Mybatis,就像是在煮一锅没有灵魂的羊肉汤:原料都在,但就是不够鲜美。代码繁琐,每写一个查询都像是在不断调味,却怎么也调不出那种令人惊艳的味道。直到遇见MyBatisPlus,一切都变了 —— 这就像是从普通的羊肉汤,突然升级到了外婆秘制的顶级羊肉汤!

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");

// ✅ 更优写法:使用LambdaQueryWrapper
wrapper.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);
}
}

// 性能追踪工具类
@Slf4j
public 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);
}

@Override
public 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

// ❌ 不推荐:使用 setSql
userService.lambdaUpdate()
.setSql("integral = integral + 10")
.update();

// ✅ 推荐:使用 setIncrBy
userService.lambdaUpdate()
.eq(User::getId, 1L)
.setIncrBy(User::getIntegral, 10)
.update();

// ✅ 推荐:使用 setDecrBy
userService.lambdaUpdate()
.eq(User::getId, 1L)
.setDecrBy(User::getStock, 5)
.update();

原因:

  • 类型安全
  • 避免手动拼接sql,防止sql注入
  • 代码可维护性更强,更清晰

总结

写代码如烹小鲜,讲究的是精细和用心。就像一碗好汤,不仅仅在于锅和火候,更在于厨师对食材的理解和尊重。MyBatisPlus的这12个优化技巧,何尝不是程序员对代码的一种尊重和雕琢?

还记得文章开头说的外婆的羊肉汤吗?优秀的代码,和一碗好汤,都需要用心。每一个细节,每一个调整,都是为了让最终的成果更加完美。MyBatisPlus就像是厨房里的得力助手,它帮你处理繁琐,让你专注于创造。

当你掌握了这些技巧,你的代码将不再是简单的指令堆砌,而是一首优雅的诗,一曲悦耳的交响乐。它们将像外婆的羊肉汤一样,散发着独特的魅力,让人回味无穷。

愿每一位开发者,都能用MyBatisPlus,煮出属于自己的”秘制汤羹”!

代码,就应该是这个样子 —— 简单而不失优雅,高效而不失温度。

文章目录
  1. 1. 前言
  2. 2. 避免使用isNull判断
  3. 3. 明确Select字段
  4. 4. 批量操作方法替代循环
  5. 5. Exists方法子查询
  6. 6. 使用orderBy代替last
  7. 7. 使用LambdaQuery确保类型安全
  8. 8. 用between代替ge和le
  9. 9. 排序字段注意索引
  10. 10. 分页参数设置
  11. 11. 条件构造处理Null值
  12. 12. 查询性能追踪
  13. 13. 枚举类型映射
  14. 14. 自动处理逻辑删除
  15. 15. 乐观锁更新保护
  16. 16. 递增和递减:setIncrBy 和 setDecrBy
  17. 17. 总结