在这里插入图片描述

MyBatis-Plus 查询双雄:Service 链式调用 vs Mapper 传统写法,你真的懂吗?

1. 引言

假设你经营一家仓库,里面堆满了各种货物。你需要随时知道某个货架上有什么、数量多少。这时候,有两种方式:一种是直接找仓库管理员(Mapper),他手里有详细的台账,但需要你明确告诉他查哪个货架、用什么条件;另一种是找业务接待员(Service),他不仅会转告仓库管理员,还能帮你处理一些额外的业务逻辑(比如记录查询日志、统计查询次数)。接待员虽然不能直接接触货物,但他的服务更贴心,让你少操心。

在 MyBatis-Plus 的世界里,BaseMapper 就是那个仓库管理员,提供最基础的数据库操作;IService 则是业务接待员,它在 Mapper 的基础上封装了更便捷的 API,让我们能用更少的代码完成相同的 CRUD 任务。本文将通过对比两种典型的查询写法,带你彻底搞懂它们的区别、联系,并解答“泛型限定是否导致不灵活”的核心疑问。


2. 前置知识:MyBatis-Plus 的 Lambda 机制

在深入两种写法之前,我们需要先理解 MyBatis-Plus 引入 Lambda 表达式的用意。传统 MyBatis 中,我们写查询条件时通常使用字符串指定字段名:

// 传统写法:硬编码字段名,容易写错且重构时不会报错
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三");

这种方式有两个痛点:

  • 字段名以字符串形式硬编码,拼写错误只能在运行时发现。
  • 如果实体类的字段名重构(例如 name 改为 userName),所有字符串引用不会自动更新,容易遗漏。

MyBatis-Plus 的 Lambda 机制通过 Java 8 的方法引用解决了这个问题:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三");

User::getName 是一个方法引用,编译器会检查 User 类中是否存在 getName 方法。这带来了两个核心好处:

  • 类型安全:字段名错误会在编译阶段报错。
  • 重构友好:如果实体类字段名变更,IDE 会同步更新所有方法引用。

理解了 Lambda 机制后,我们来看两种具体的查询写法。


3. 两种查询写法的巅峰对决

3.1 写法 A:Mapper 流派(传统手工派)

代码示例(来自 LearnTaskExerciseSubmitQuizMapper):

@Override
public List<LearnTaskExerciseSubmitQuiz> listLearnTaskExerciseSubmitQuiz(List<Long> submitIds, Integer appealFlag) {
    // 1. 手动创建 LambdaQueryWrapper 对象
    LambdaQueryWrapper<LearnTaskExerciseSubmitQuiz> queryWrapper = new LambdaQueryWrapper<>();
    // 2. 逐个添加条件
    queryWrapper.eq(LearnTaskExerciseSubmitQuiz::getAppealFlag, appealFlag);
    queryWrapper.in(LearnTaskExerciseSubmitQuiz::getSubmitId, submitIds);
    // 3. 调用 Mapper 的方法执行查询
    return learnTaskExerciseSubmitQuizMapper.selectList(queryWrapper);
}

特点与拆解

  • 步骤明确:先创建 Wrapper,再添加条件,最后调用 Mapper 方法。
  • Wrapper 可复用:同一个 Wrapper 对象可以传递给多个 Mapper 方法,适合构建复杂动态条件。
  • 直接操作 Mapper:需要注入 LearnTaskExerciseSubmitQuizMapper 才能调用。

这种写法源于 MyBatis 的传统编程模型,理解门槛低,但代码略显冗长(每次都要 new 一个 Wrapper)。

3.2 写法 B:Service 流派(链式优雅派)

代码示例(来自 BotConversation 查询):

// 直接通过 Service 对象调用 lambdaQuery() 并链式添加条件
botConversations = botConversationService.lambdaQuery()
        .eq(BotConversation::getBotId, botId)
        .eq(BotConversation::getInstanceBotId, instanceBotId)
        .eq(BotConversation::getDeleteFlag, Constants.DELETE_FLAG_UNDELETED)
        .eq(BotConversation::getCreatedBy, user.getUserId())
        .orderByAsc(BotConversation::getCreatedTime)
        .list();

特点与拆解

  • 链式调用lambdaQuery() 返回一个 LambdaQueryChainWrapper 对象,该对象提供了链式条件方法。
  • 无需手动创建 Wrapper:框架内部已经帮你创建好。
  • 直接返回结果list() 执行查询并返回列表,一气呵成。
  • 依赖 Service:需要注入 IBotConversationService,而不是直接操作 Mapper。

这种写法让代码变得极其简洁,尤其适合单表查询,开发效率极高。

3.3 底层真相:Service 只是 Mapper 的“包装纸”

为了看清两者本质,我们不妨追踪一下 Service 的 lambdaQuery().list() 到底做了什么。

查看 MyBatis-Plus 源码(以 ServiceImpl 为例):

public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
    @Override
    public LambdaQueryChainWrapper<T> lambdaQuery() {
        return new LambdaQueryChainWrapper<>(getBaseMapper());
    }
}

lambdaQuery() 方法创建了一个 LambdaQueryChainWrapper,并将当前 Service 关联的 Mapper 传了进去。再看 LambdaQueryChainWrapperlist() 方法:

public class LambdaQueryChainWrapper<T> extends AbstractLambdaWrapper<T, LambdaQueryChainWrapper<T>> {
    private final BaseMapper<T> baseMapper;

    public List<T> list() {
        return baseMapper.selectList(getWrapper()); // 最终还是调用 Mapper 的 selectList
    }
}

结论:Service 的链式查询本质上是对 Mapper 查询的封装——它帮你创建 Wrapper,并在最后把控制权交还给 Mapper。两者底层执行的是完全相同的 SQL,没有任何性能差异。


4. 核心疑问解答:泛型限定到底“灵活不灵活”?

不少初学者会困惑:IService<BotConversation> 限定了只能操作 BotConversation 表,BaseMapper<LearnTaskExerciseSubmitQuiz> 也只能操作对应表。那如果我想在一个 Service 里查询多张表,岂不是被框架限制死了?

4.1 为什么要有泛型限定?

  • 类型安全:明确告诉框架你操作的是哪个实体,避免误操作其他表。
  • 代码生成器基础:MyBatis-Plus 的代码生成器根据表生成实体后,自动生成对应的 Mapper 和 Service,泛型保证了它们的一一对应。
  • 框架内部实现ServiceImpl<M, T> 需要知道实体类型 T 和 Mapper 类型 M,才能提供通用的 CRUD 实现。

4.2 多表查询的解决方案

如果你真的需要在一个 Service 方法里查询多个表(例如关联查询),框架提供了多种灵活的扩展方式,从未限制你的发挥。

方案一:注入多个 Mapper

在 Service 实现类中,可以注入其他表的 Mapper,分别查询,然后在内存中组装数据。

@Service
public class UserOrderServiceImpl extends ServiceImpl<UserMapper, User> implements UserOrderService {
    
    @Resource
    private OrderMapper orderMapper; // 注入另一个 Mapper

    public UserOrderVO getUserWithOrders(Long userId) {
        User user = getById(userId); // 使用本 Service 的方法查询用户
        List<Order> orders = orderMapper.selectList(
            new LambdaQueryWrapper<Order>().eq(Order::getUserId, userId)
        ); // 使用 OrderMapper 查询订单
        return assemble(user, orders);
    }
}

这种方式保持了代码清晰,且符合单一职责原则。

方案二:自定义 Mapper 方法 + XML

在 Mapper 接口中定义方法,编写 XML SQL 进行多表关联查询。

public interface UserMapper extends BaseMapper<User> {
    // 自定义方法
    List<UserOrderVO> selectUserOrders(@Param("userId") Long userId);
}
<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectUserOrders" resultType="com.example.vo.UserOrderVO">
        SELECT u.*, o.* 
        FROM user u
        LEFT JOIN order o ON u.id = o.user_id
        WHERE u.id = #{userId}
    </select>
</mapper>
方案三:使用 @Select 注解直接写 SQL

对于简单关联,可以直接在 Mapper 方法上加注解:

@Select("SELECT * FROM user WHERE id IN (SELECT user_id FROM order WHERE amount > 100)")
List<User> getRichUsers();
方案四:利用 Wrapper 的 apply 方法拼接原生 SQL

如果只是临时需要关联子查询,可以用 apply 拼接 SQL 片段:

lambdaQuery().apply("user_id in (select user_id from order where status=1)").list();

4.3 结论:约束即契约

MyBatis-Plus 通过泛型限定,在 90% 的单表操作场景下为我们提供了“开箱即用”的便捷,同时又保留了应对复杂场景的扩展能力。这种“约定优于配置”的设计,恰恰是优秀框架的体现——它不限制你的想象力,但帮你规范了常规操作


5. 进阶技巧:如何优雅地混合使用两种风格?

在实际项目中,我们经常需要根据场景灵活选择:

  • 单表简单查询:用 Service 链式调用,代码简洁到极致。
  • 单表复杂动态查询:在 Mapper 层手动构建 Wrapper,便于调试和复用。
  • 多表关联查询:自定义 Mapper + XML,将复杂 SQL 交给最擅长的人(SQL 本身)。
  • 批量操作:优先使用 Service 提供的 saveBatchremoveByIds 等方法。

示例:动态条件查询

// Mapper 层:手动构建 Wrapper,条件动态组装
public List<User> getUsersByCondition(String name, Integer age) {
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.like(StringUtils.isNotBlank(name), User::getName, name)
           .eq(age != null, User::getAge, age)
           .orderByDesc(User::getCreateTime);
    return userMapper.selectList(wrapper);
}

6. 总结与学习建议

6.1 核心要点回顾

  • BaseMapper 是数据访问层的基础,提供最底层的 CRUD 方法。
  • IService 在 Mapper 之上封装了更便捷的 API,包括链式查询、批量操作等。
  • 两种查询写法本质上一致,Service 的 lambdaQuery() 最终仍调用 Mapper 的 selectList
  • 泛型限定是为了类型安全和框架约定,并不限制多表查询——MyBatis-Plus 提供了多种扩展方式。

6.2 面试话术参考

“MyBatis-Plus 的 IServiceBaseMapper 通过泛型限定了操作实体,提供了类型安全的单表 CRUD。日常开发中,简单查询我倾向于用 Service 的链式调用,代码简洁;复杂查询则在 Mapper 层手动构建 Wrapper,便于调试。遇到多表关联时,我会在 Mapper 中自定义 SQL,既保持了灵活性,又不破坏框架的整体设计。这种‘约定优于配置’的思想,让团队开发更规范、更高效。”

6.3 动手实践建议

  1. 对比实验:分别用两种方式实现用户表的增删改查,感受代码量的差异。
  2. 源码追踪:在 IDE 中点进 lambdaQuery()list() 方法,跟着调用链走一遍,亲眼见证最终调用的是 baseMapper.selectList
  3. 多表练习:尝试用三种不同的方式实现“查询用户及其最近订单”的功能,体会每种方式的优劣。

希望本文能帮你彻底掌握 MyBatis-Plus 的两种查询方式,在今后的开发中更加游刃有余。如果还有疑问,欢迎在评论区留言讨论!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐