MyBatis Plus 封神玩法:这12个操作让开发效率直接起飞!

简介: 本文以外婆羊肉汤为喻,生动诠释MyBatis-Plus的12个核心优化技巧:避免isNull、精准select、批量操作、善用exists、安全排序、Lambda类型安全、between替代ge/le、索引友好排序、规范分页、空值条件优雅处理,并涵盖性能追踪、枚举映射等高级实践,助你写出高效、安全、可维护的ORM代码。

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

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

MyBatisPlus就像一位精通厨艺的帮厨,它帮你处理了所有繁琐的准备工作。想要一个复杂的查询?不用自己一刀一刀地切肉、一勺一勺地调味,框架已经帮你准备好了。你只需要轻轻地指挥,代码就像汤汁一样顺滑流畅,性能更是鲜美可口。

在接下来的篇幅里,我将与你分享12个MyBatisPlus优化的"秘制配方"。相信看完这些,你写的每一行代码,都会像外婆的羊肉汤一样,让人回味无穷。

耐心看完,你一定有所收获。


1. 避免使用isNull判断

// ❌ 不推荐
LambdaQueryWrapper<User> wrapper1 = new LambdaQueryWrapper<>();
wrapper1.isNull(User::getStatus);

// ✅ 推荐:使用具体的默认值
LambdaQueryWrapper<User> wrapper2 = new LambdaQueryWrapper<>();
wrapper2.eq(User::getStatus, UserStatusEnum.INACTIVE.getCode());

📝 原因:

  1. 使用具体的默认值可以提高代码的可读性和维护性
  2. NULL值会使索引失效,导致MySQL无法使用索引进行查询优化
  3. NULL值的比较需要特殊的处理逻辑,增加了CPU开销
  4. NULL值会占用额外的存储空间,影响数据压缩效率

2. 明确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);

📝 原因:

  1. 避免大量无用字段的网络传输开销
  2. 可以利用索引覆盖,避免回表查询
  3. 减少数据库解析和序列化的负担
  4. 降低内存占用,特别是在大量数据查询时

3. 批量操作方法替代循环

// ❌ 不推荐
for (User user : userList) {
   
    userMapper.insert(user);
}

// ✅ 推荐
userService.saveBatch(userList, 100);  // 每批次处理100条数据

// ✅ 更优写法:自定义批次大小
userService.saveBatch(userList, BatchConstants.BATCH_SIZE);

📝 原因:

  1. 减少数据库连接的创建和销毁开销
  2. 批量操作可以在一个事务中完成,提高数据一致性
  3. 数据库可以优化批量操作的执行计划
  4. 显著减少网络往返次数,提升吞吐量

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

📝 原因:

  1. EXISTS是基于索引的快速查询,可以使用到索引
  2. EXISTS在找到第一个匹配项就会停止扫描
  3. IN子查询需要加载所有数据到内存后再比较
  4. 当外表数据量大时,EXISTS的性能优势更明显

5. 使用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);

📝 原因:

  1. 直接拼接SQL容易导致SQL注入攻击
  2. 动态SQL可能破坏SQL语义完整性
  3. 影响SQL语句的可维护性和可读性
  4. last会绕过MyBatis-Plus的安全检查机制

6. 使用LambdaQuery确保类型安全

// ❌ 不推荐:字段变更后可能遗漏
QueryWrapper<User> wrapper1 = new QueryWrapper<>();
wrapper1.eq("name", "张三").gt("age", 18);

// ✅ 推荐 LambdaQueryWrapper
LambdaQueryWrapper<User> wrapper2 = new LambdaQueryWrapper<>();
wrapper2.eq(User::getName, "张三")
        .gt(User::getAge, 18);

// ✅ 更优写法:使用链式调用
userService.lambdaQuery()
    .eq(User::getName, "张三")
    .gt(User::getAge, 18)
    .list();

📝 原因:

  1. 编译期类型检查,避免字段名拼写错误
  2. IDE可以提供更好的代码补全支持
  3. 重构时能自动更新字段引用
  4. 提高代码的可维护性和可读性

7. 用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);

📝 原因:

  1. 生成的SQL更简洁,减少解析开销
  2. 数据库优化器可以更好地处理范围查询
  3. 代码更易读,语义更清晰
  4. 减少重复编写字段名的机会

8. 排序字段注意索引

// ❌ 不推荐(lastLoginTime无索引)
wrapper.orderByDesc(User::getLastLoginTime);

// ✅ 推荐:主键排序
wrapper.orderByDesc(User::getId);

// ✅ 更优写法:组合索引排序
wrapper.orderByDesc(User::getStatus)  // status建立了索引
       .orderByDesc(User::getId);     // 主键排序

📝 原因:

  1. 索引天然具有排序特性,可以避免额外的排序操作
  2. 无索引排序会导致文件排序,极大影响性能
  3. 当数据量大时,内存排序可能导致溢出
  4. 利用索引排序可以实现流式读取

9. 分页参数规范设置

// ❌ 不推荐:last直接限制,一次性拉取大量数据
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));

📝 原因:

  1. 控制单次查询的数据量,避免内存溢出
  2. 提高首屏加载速度,优化用户体验
  3. 减少网络传输压力
  4. 数据库资源利用更合理

10. 条件构造优雅处理Null值

// ❌ 不推荐:大量if判断
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);  // 默认按创建时间倒序

📝 原因:

  1. 优雅处理空值,避免无效条件
  2. 减少代码中的if-else判断
  3. 提高代码可读性
  4. 防止生成冗余的SQL条件

⚠️ 高级技巧篇

11. 查询性能追踪

// ❌ 不推荐:手动计时,代码冗余
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);
            }
        }
    }
}

📝 原因:

  1. 业务代码和性能监控代码完全分离
  2. try-with-resources 即使发生异常,close() 方法也会被调用,确保一定会记录耗时
  3. 不需要手动管理计时的开始和结束
  4. 代码更加优雅

12. 枚举类型映射

// 定义枚举
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)
);

📝 原因:

  1. 类型安全
  2. 自动处理数据库和枚举转换
  3. 避免魔法值
  4. 代码可读性更强

补充拓展实用功能

1. 自动逻辑删除

// 实体字段标记
@TableLogic
private Integer deleted;

// 查询自动过滤已删除数据
public List<User> getActiveUsers() {
   
    return userMapper.selectList(null);  // 自动过滤deleted=1的记录
}

// 删除操作实际执行更新
userService.removeById(1L);

📝 原因:

  1. 数据不丢失,支持数据恢复
  2. 查询自动过滤已删除数据
  3. 减少手动编写删除逻辑

    注意:XML自定义SQL需要手动拼接 deleted = 0

2. 乐观锁并发更新保护

// 实体添加版本字段
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);
}

📝 原因:

  1. 防止并发冲突
  2. 自动处理版本控制
  3. 简化并发更新逻辑
  4. 提高数据一致性

3. 字段自增/自减 setIncrBy / setDecrBy

// ❌ 不推荐:手写setSql,存在注入风险
userService.lambdaUpdate()
    .setSql("integral = integral + 10")
    .update();

// ✅ 推荐:积分自增
userService.lambdaUpdate()
    .eq(User::getId, 1L)
    .setIncrBy(User::getIntegral, 10)
    .update();

// ✅ 推荐:库存自减
userService.lambdaUpdate()
    .eq(User::getId, 1L)
    .setDecrBy(User::getStock, 5)
    .update();

📝 原因:

  1. 类型安全,编译校验
  2. 避免手动拼接sql,防止sql注入
  3. 代码可维护性更强,逻辑清晰

总结

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

还记得文章开头说的外婆的羊肉汤吗?优秀的代码,和一碗好汤,都需要用心。MyBatisPlus就像是厨房里的得力助手,它帮你处理繁琐,让你专注于创造。

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

愿每一位开发者,都能用MyBatisPlus,煮出属于自己的"秘制汤羹"!
代码,就应该是这个样子 —— 简单而不失优雅,高效而不失温度。

相关文章
|
人工智能 搜索推荐 安全
GPT Prompt编写的艺术:如何提高AI模型的表现力
GPT Prompt编写的艺术:如何提高AI模型的表现力
919 0
|
算法 计算机视觉 开发者
OpenCV中使用Eigenfaces人脸识别器识别人脸实战(附Python源码)
OpenCV中使用Eigenfaces人脸识别器识别人脸实战(附Python源码)
908 0
|
1月前
|
存储 人工智能 安全
阿里云618AI加速季域名及建站产品优惠:万小智送.CN域名,买域名送邮箱,域名批量低至0.9折
2026年阿里云AI加速季618活动期间,万网权益中心推出域名、建站、备案一站式优惠矩阵。新人15元AI建站赠.CN域名,10分钟生成官网,SSL证书+备案一步到位;新客域名首年低至1元,.xyz/.top等主流后缀可选;备案域名6折转入,周三转入更优惠;组合购(域名+ECS+OSS)低至0.3折,109元/月起。活动覆盖个人建站到企业上云全场景,产品组合灵活、优惠力度大,用户可按需选购。
|
4月前
|
存储 消息中间件 数据挖掘
数据仓库是什么?离线数仓和实时数仓有什么区别?
本文深入解析离线数仓与实时数仓的本质区别:离线数仓以T+1批量处理为主,依托Hive/Spark和分层建模,保障稳定与准确;实时数仓聚焦秒级延迟,基于Flink/Kafka流式架构,满足大屏、风控等强时效场景。二者非替代而是互补,选型需兼顾业务需求、团队能力与成本。附免费数仓建设全案指南。
|
4月前
|
SQL 关系型数据库 Java
吃透 Seata 分布式事务:原理拆解 + 生产级落地 + 全场景避坑实战
本文深度解析阿里开源分布式事务框架Seata:剖析TC/TM/RM三大角色与全局事务流程,详解AT(零侵入)、TCC(强控制)、SAGA(长事务)、XA(强一致)四大模式原理、适用场景及核心对比,并通过电商下单实战演示AT模式落地,最后系统梳理生产环境高可用、SQL限制、幂等处理、XID传播等全链路避坑指南。
1134 4
|
并行计算 Python 容器
uv找不到Python头文件的解决方案
最近在微调LLM的时候,我发现使用uv构建的环境,有时候会找不到Python.h,导致一些库报错,如`fatal error: Python.h: No such file or directory`。通过设置`python-preference`可以解决。
1098 35
|
存储 人工智能 安全
《探索鸿蒙Next上开发人工智能游戏应用的技术难点》
在科技飞速发展的今天,鸿蒙Next系统为人工智能游戏应用开发带来新机遇与挑战。开发者需解决多设备协同、自适应布局、AI模型训练、实时反应、游戏引擎选择、图形渲染、数据管理和安全保护等技术难点。同时,还需进行兼容性测试和性能优化,确保游戏流畅运行并满足用户需求。通过不断学习创新,开发者有望打造出高质量的鸿蒙Next游戏应用。
563 23
|
存储 供应链 数据建模
供应链场景使用ClickHouse最佳实践
在供应链管理中,ClickHouse凭借其高性能查询、高压缩比和实时数据处理能力,能够显著提升数据处理和分析的效率。通过合理的数据建模、优化实践和性能调优,可以充分发挥ClickHouse的优势,为供应链管理提供强有力的支持。
425 12
|
人工智能 运维 负载均衡
智能运维新时代:AI在云资源管理中的应用与实践
智能运维新时代:AI在云资源管理中的应用与实践
1464 23
|
前端开发
西门子S7-1200模块如何安装和拆卸
本文我们来学习西门子S7-1200模块如何安装和拆卸。西门子S7-1200模块的设计易于安装,尺寸较小,可以有效的利用空间。S7-1200可以安装在面板或标准导轨上,既可以水平安装,也可以垂直安装。
西门子S7-1200模块如何安装和拆卸