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

文章目录
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 传了进去。再看 LambdaQueryChainWrapper 的 list() 方法:
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 提供的
saveBatch、removeByIds等方法。
示例:动态条件查询
// 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 的
IService和BaseMapper通过泛型限定了操作实体,提供了类型安全的单表 CRUD。日常开发中,简单查询我倾向于用 Service 的链式调用,代码简洁;复杂查询则在 Mapper 层手动构建 Wrapper,便于调试。遇到多表关联时,我会在 Mapper 中自定义 SQL,既保持了灵活性,又不破坏框架的整体设计。这种‘约定优于配置’的思想,让团队开发更规范、更高效。”
6.3 动手实践建议
- 对比实验:分别用两种方式实现用户表的增删改查,感受代码量的差异。
- 源码追踪:在 IDE 中点进
lambdaQuery()和list()方法,跟着调用链走一遍,亲眼见证最终调用的是baseMapper.selectList。 - 多表练习:尝试用三种不同的方式实现“查询用户及其最近订单”的功能,体会每种方式的优劣。
希望本文能帮你彻底掌握 MyBatis-Plus 的两种查询方式,在今后的开发中更加游刃有余。如果还有疑问,欢迎在评论区留言讨论!
更多推荐

所有评论(0)