1. MyBatisPlus核心查询方法入门指南第一次接触MyBatisPlus时我被它强大的查询功能惊艳到了。相比原生MyBatis需要手动编写SQL的繁琐MyBatisPlus提供了一套开箱即用的查询方法让日常开发效率提升了至少50%。今天我就来聊聊其中最常用的五个核心查询方法selectById、selectOne、selectBatchIds、selectByMap和selectPage。这五个方法覆盖了日常开发中80%的查询场景。比如用户管理系统中查看用户详情、订单系统中批量查询订单状态、后台管理系统的分页查询等。它们最大的特点是简单易用基本上几行代码就能完成复杂的查询逻辑而且性能表现也很不错。不过要注意的是虽然这些方法用起来方便但如果不了解它们的适用场景和潜在问题很容易踩坑。比如selectOne方法在查询到多条记录时会直接返回第一条这可能会导致业务逻辑出错又比如selectPage分页查询在大数据量时需要特别注意性能优化。2. selectById主键查询的最佳实践2.1 基础用法与性能分析selectById是我用得最多的方法之一它的作用非常简单直接根据主键ID查询单条记录。在实际项目中像用户详情页、商品详情页这种场景selectById就是最佳选择。User user userMapper.selectById(1L);这行代码看起来简单但背后MyBatisPlus做了很多优化。首先它会自动识别实体类的主键字段然后生成对应的SQL语句。我实测过在百万级数据量的表中selectById的查询时间基本能稳定在10ms以内。2.2 实际业务场景应用在电商项目中商品详情页的查询就是个典型例子。当用户点击某个商品时前端会传过来商品ID后端直接用selectById就能快速获取商品信息GetMapping(/product/{id}) public Product getProductDetail(PathVariable Long id) { return productMapper.selectById(id); }这里有个小技巧如果查询结果可能为null建议加上判空处理。我在实际项目中遇到过因为没判空导致的NPE问题排查起来还挺麻烦的。2.3 常见问题与解决方案虽然selectById很简单但还是有几个需要注意的地方主键类型要匹配。如果你的主键是String类型传参时也要用String用Long会查不到数据批量查询不要循环调用selectById应该用selectBatchIds这个后面会详细讲在高并发场景下可以考虑配合缓存使用减轻数据库压力3. selectOne精确查询的利与弊3.1 使用场景解析selectOne方法用于根据条件查询单条记录它的特点是只返回第一条匹配的结果。这个方法在登录验证、唯一性校验等场景特别有用。QueryWrapperUser queryWrapper new QueryWrapper(); queryWrapper.eq(username, admin); User adminUser userMapper.selectOne(queryWrapper);但这里有个大坑如果数据库中有多条记录满足条件selectOne不会报错而是静默返回第一条。我就曾经因为这个问题导致业务逻辑出错后来加了个count查询来确保数据唯一性。3.2 条件构造器的灵活运用QueryWrapper是selectOne的好搭档它提供了丰富的条件构造方法// 多条件查询 QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(status, 1) .gt(create_time, 2023-01-01) .orderByDesc(id); User user userMapper.selectOne(wrapper);对于复杂的查询条件建议把wrapper的构建过程封装成方法这样代码会更清晰。我在项目中的做法是创建一个WrapperHelper类专门处理各种查询条件的组装。3.3 性能优化建议selectOne的性能很大程度上取决于查询条件的索引情况。如果where条件中的字段没有索引在大数据量表上查询会非常慢。我有次在百万级用户表上查一个非索引字段查询耗时达到了2秒多。所以使用selectOne时要注意确保查询条件字段有合适的索引避免在循环中频繁调用selectOne对于热点数据考虑使用缓存4. selectBatchIds批量查询的高效方案4.1 批量操作的优势对比需要根据多个ID查询记录时新手可能会想到用for循环调用selectById但这样效率很低。selectBatchIds就是为解决这个问题而生的ListLong ids Arrays.asList(1L, 2L, 3L, 4L, 5L); ListUser users userMapper.selectBatchIds(ids);我做过测试查询100条记录时selectBatchIds比循环调用selectById快10倍以上。这是因为前者只需要一次数据库交互而后者需要100次。4.2 实际业务场景应用在订单管理系统中经常需要批量查询订单状态public ListOrder getOrderListByIds(ListLong orderIds) { if (CollectionUtils.isEmpty(orderIds)) { return Collections.emptyList(); } return orderMapper.selectBatchIds(orderIds); }这里要注意处理空列表的情况否则会报SQL语法错误。我在早期项目中没有做这个判断导致线上出了个P2故障。4.3 大数据量下的分批次处理当ID列表特别大时比如超过1000个直接使用selectBatchIds可能会导致SQL语句过长甚至超出数据库限制。我的解决方案是分批次查询ListListLong partitions Lists.partition(largeIdList, 200); ListUser result new ArrayList(); for (ListLong batchIds : partitions) { result.addAll(userMapper.selectBatchIds(batchIds)); }使用Guava的Lists.partition方法可以很方便地将大列表拆分成小批次每批200个ID是个比较合理的数值。5. selectByMap灵活的条件查询5.1 Map条件的使用技巧selectByMap提供了一种更灵活的查询方式通过Map来指定查询条件MapString, Object conditionMap new HashMap(); conditionMap.put(age, 18); conditionMap.put(city, 北京); ListUser users userMapper.selectByMap(conditionMap);这种方法特别适合动态条件查询的场景比如后台管理系统的筛选功能。前端传过来的筛选条件可以直接转换成Map使用。5.2 动态查询场景实践在用户管理系统中我们实现了这样的动态查询public ListUser searchUsers(MapString, Object params) { // 过滤掉空值条件 MapString, Object queryMap params.entrySet().stream() .filter(entry - entry.getValue() ! null) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); return queryMap.isEmpty() ? Collections.emptyList() : userMapper.selectByMap(queryMap); }这里加了个空值过滤的逻辑避免无意义的查询条件。实际项目中还可以加入字段白名单校验防止SQL注入。5.3 与Wrapper的对比选择selectByMap和QueryWrapper都能实现条件查询它们的主要区别在于selectByMap更简单直接适合简单的等值查询QueryWrapper功能更强大支持范围查询、排序等复杂操作selectByMap适合动态条件QueryWrapper适合固定条件我的经验是简单查询用selectByMap复杂查询用QueryWrapperselectList。6. selectPage分页查询的完整解决方案6.1 基础分页实现分页查询是后台管理系统中最常见的需求之一selectPage方法让这个功能实现起来非常简单IPageUser page new Page(1, 10); // 第一页每页10条 QueryWrapperUser wrapper new QueryWrapper(); wrapper.gt(age, 20); IPageUser result userMapper.selectPage(page, wrapper); ListUser records result.getRecords(); // 当前页数据 long total result.getTotal(); // 总记录数这个分页方案已经帮我们处理好了总记录数查询和分页逻辑开发效率非常高。我在项目中使用后发现相比手写分页SQL能节省约70%的代码量。6.2 性能优化方案虽然selectPage用起来方便但在大数据量表上性能可能会成为问题。我遇到过在百万级数据表上分页查询很慢的情况特别是翻到后面的页码时。经过实践我总结了几种优化方案使用page.setSearchCount(false)禁用总记录数查询适用于不需要显示总数的场景对排序字段建立索引特别是经常用于分页排序的字段对于深度分页比如第100页考虑使用基于游标的分页方式6.3 复杂分页场景处理有时候我们需要在分页的同时进行多表关联查询。MyBatisPlus也提供了解决方案IPageUser page new Page(1, 10); QueryWrapperUser wrapper new QueryWrapper(); wrapper.select(u.*, d.name as deptName) .from(user u) .leftJoin(department d on u.dept_id d.id) .gt(u.age, 20); IPageUser result userMapper.selectPage(page, wrapper);这种写法既保留了分页功能又能实现复杂的多表查询。不过要注意join查询的性能问题必要时可以考虑冗余字段或者使用缓存。7. 综合应用与避坑指南在实际项目中我们往往需要根据业务场景灵活组合使用这些查询方法。比如在电商系统中订单列表页面可能需要同时用到分页查询和批量查询// 分页查询订单基本信息 IPageOrder page new Page(currentPage, pageSize); QueryWrapperOrder wrapper new QueryWrapper(); wrapper.eq(user_id, userId) .orderByDesc(create_time); IPageOrder orderPage orderMapper.selectPage(page, wrapper); // 批量查询关联的商品信息 ListLong productIds orderPage.getRecords().stream() .map(Order::getProductId) .collect(Collectors.toList()); MapLong, Product productMap productMapper.selectBatchIds(productIds) .stream() .collect(Collectors.toMap(Product::getId, Function.identity()));这种组合使用的方式既能保持代码简洁又能满足复杂业务需求。不过在实际开发中我也踩过不少坑分页查询忘记加排序条件导致分页结果不稳定使用selectOne时没有确保条件唯一性导致返回错误数据在大数据量场景下直接使用selectPage没有考虑性能问题使用selectByMap时没有过滤空值导致查询条件出错针对这些问题我的建议是为常用查询字段建立合适的索引对查询结果进行必要的校验和判空处理在大数据量场景下进行性能测试和优化封装统一的查询工具类避免重复代码最后要提醒的是虽然MyBatisPlus的查询方法很强大但也不是万能的。对于特别复杂的查询场景还是建议使用自定义SQL或者存储过程来实现。