1. MybatisPlus LambdaQueryChainWrapper 基础概念
LambdaQueryChainWrapper 是 MybatisPlus 提供的一个链式查询构造器,它通过 Lambda 表达式的方式来构建查询条件,避免了硬编码字段名带来的风险。在实际项目中,我们经常需要处理多表关联查询的场景,而 LambdaQueryChainWrapper 提供了一种优雅的方式来实现这种需求。
1.1 LambdaQueryChainWrapper 核心优势
相比传统的 Mybatis 查询方式,LambdaQueryChainWrapper 具有几个显著优势:
- 类型安全:通过 Lambda 表达式引用实体类属性,编译器可以检查类型是否正确
- 代码可读性:链式调用让查询逻辑更加清晰直观
- 避免魔法值:不再需要手动输入字段名的字符串,减少拼写错误
- IDE 支持:现代 IDE 能够提供代码补全和重构支持
java复制// 传统方式 - 字段名是字符串,容易出错
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "张三").ge("age", 18);
// Lambda方式 - 编译器会检查类型
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(User::getName, "张三").ge(User::getAge, 18);
1.2 基础使用方法
让我们先看一个最简单的 LambdaQueryChainWrapper 使用示例:
java复制List<User> users = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getStatus, 1)
.like(User::getName, "张")
.list();
这段代码会生成类似如下的 SQL:
sql复制SELECT * FROM user WHERE status = 1 AND name LIKE '%张%'
2. 联合查询的实现方式
在实际业务中,单表查询往往不能满足需求,我们需要处理多表关联的场景。MybatisPlus 提供了几种实现联合查询的方式。
2.1 使用 @TableField 注解实现简单关联
对于一对一或一对多的简单关联,可以使用 @TableField 注解:
java复制public class Order {
private Long id;
private String orderNo;
@TableField(exist = false)
private List<OrderItem> items;
}
public class OrderItem {
private Long id;
private Long orderId;
private String productName;
}
查询时可以这样操作:
java复制List<Order> orders = new LambdaQueryChainWrapper<>(orderMapper)
.eq(Order::getStatus, 1)
.list();
// 手动处理关联查询
orders.forEach(order -> {
List<OrderItem> items = new LambdaQueryChainWrapper<>(orderItemMapper)
.eq(OrderItem::getOrderId, order.getId())
.list();
order.setItems(items);
});
2.2 使用自定义SQL实现复杂关联
对于更复杂的关联查询,可以在Mapper中定义自定义SQL:
java复制public interface OrderMapper extends BaseMapper<Order> {
@Select("SELECT o.*, u.name as user_name FROM order o LEFT JOIN user u ON o.user_id = u.id")
List<OrderVO> selectOrderWithUser();
}
然后通过 LambdaQueryChainWrapper 组合其他条件:
java复制List<OrderVO> orders = new LambdaQueryChainWrapper<>(orderMapper)
.apply("EXISTS (SELECT 1 FROM order_item oi WHERE oi.order_id = id AND oi.product_name LIKE '%手机%')")
.list();
3. 高级查询技巧
3.1 动态条件拼接
LambdaQueryChainWrapper 支持灵活的条件拼接:
java复制public List<User> searchUsers(String name, Integer minAge, Integer maxAge) {
return new LambdaQueryChainWrapper<>(userMapper)
.like(StringUtils.isNotBlank(name), User::getName, name)
.gt(minAge != null, User::getAge, minAge)
.lt(maxAge != null, User::getAge, maxAge)
.orderByDesc(User::getCreateTime)
.list();
}
3.2 子查询处理
实现子查询的几种方式:
- 使用 apply 方法:
java复制List<User> users = new LambdaQueryChainWrapper<>(userMapper)
.apply("id IN (SELECT user_id FROM order WHERE amount > 1000)")
.list();
- 使用 inSql/existsSql:
java复制List<User> users = new LambdaQueryChainWrapper<>(userMapper)
.inSql(User::getId, "SELECT user_id FROM order WHERE amount > 1000")
.list();
3.3 分页查询优化
结合 Page 对象实现分页:
java复制Page<User> page = new Page<>(1, 10);
Page<User> result = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getStatus, 1)
.page(page);
对于关联查询的分页,需要注意 count 语句的优化:
java复制@Select("SELECT o.* FROM order o LEFT JOIN user u ON o.user_id = u.id ${ew.customSqlSegment}")
IPage<OrderVO> selectOrderPage(Page<OrderVO> page, @Param(Constants.WRAPPER) Wrapper<OrderVO> wrapper);
4. 性能优化与常见问题
4.1 N+1 查询问题
在使用关联查询时,最容易出现的就是 N+1 查询问题。解决方案包括:
- 批量查询后手动组装:
java复制List<Order> orders = orderMapper.selectList(...);
Set<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
Map<Long, User> userMap = new LambdaQueryChainWrapper<>(userMapper)
.in(User::getId, userIds)
.list()
.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
orders.forEach(order -> order.setUser(userMap.get(order.getUserId())));
- 使用 @TableField 的 select 属性:
java复制public class Order {
@TableField(select = "SELECT * FROM user WHERE id = user_id")
private User user;
}
4.2 索引失效场景
使用 LambdaQueryChainWrapper 时也要注意避免索引失效:
java复制// 不好的写法 - 会导致索引失效
new LambdaQueryChainWrapper<>(userMapper)
.apply("DATE(create_time) = '2023-01-01'")
.list();
// 好的写法 - 能利用索引
new LambdaQueryChainWrapper<>(userMapper)
.ge(User::getCreateTime, "2023-01-01 00:00:00")
.lt(User::getCreateTime, "2023-01-02 00:00:00")
.list();
4.3 复杂查询的调试技巧
当遇到复杂查询时,可以通过以下方式调试:
- 打印生成的 SQL:
java复制LambdaQueryChainWrapper<User> wrapper = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getStatus, 1)
.like(User::getName, "张");
System.out.println(wrapper.getSqlSegment());
- 使用 MybatisPlus 的 SQL 分析插件:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
return interceptor;
}
5. 实际案例:电商订单查询系统
让我们通过一个电商系统中的订单查询功能,展示 LambdaQueryChainWrapper 在复杂业务场景中的应用。
5.1 数据结构设计
java复制// 订单主表
public class Order {
private Long id;
private String orderNo;
private Long userId;
private BigDecimal amount;
private Integer status;
private LocalDateTime createTime;
@TableField(exist = false)
private User user;
@TableField(exist = false)
private List<OrderItem> items;
}
// 订单项
public class OrderItem {
private Long id;
private Long orderId;
private Long productId;
private String productName;
private Integer quantity;
private BigDecimal price;
}
// 用户
public class User {
private Long id;
private String name;
private String phone;
private Integer age;
}
5.2 复杂查询实现
实现一个多条件的订单查询:
java复制public IPage<Order> searchOrders(OrderQuery query) {
LambdaQueryChainWrapper<Order> wrapper = new LambdaQueryChainWrapper<>(orderMapper)
.eq(query.getStatus() != null, Order::getStatus, query.getStatus())
.like(StringUtils.isNotBlank(query.getKeyword()), Order::getOrderNo, query.getKeyword())
.between(query.getStartTime() != null && query.getEndTime() != null,
Order::getCreateTime, query.getStartTime(), query.getEndTime())
.orderByDesc(Order::getCreateTime);
if (StringUtils.isNotBlank(query.getUserName())) {
wrapper.apply("EXISTS (SELECT 1 FROM user u WHERE u.id = user_id AND u.name LIKE {0}",
"%" + query.getUserName() + "%");
}
Page<Order> page = new Page<>(query.getPageNum(), query.getPageSize());
IPage<Order> result = wrapper.page(page);
// 处理关联数据
if (!result.getRecords().isEmpty()) {
loadAssociatedData(result.getRecords());
}
return result;
}
private void loadAssociatedData(List<Order> orders) {
// 批量查询用户
Set<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
Map<Long, User> userMap = new LambdaQueryChainWrapper<>(userMapper)
.in(User::getId, userIds)
.list()
.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
// 批量查询订单项
Set<Long> orderIds = orders.stream().map(Order::getId).collect(Collectors.toSet());
Map<Long, List<OrderItem>> itemMap = new LambdaQueryChainWrapper<>(orderItemMapper)
.in(OrderItem::getOrderId, orderIds)
.list()
.stream()
.collect(Collectors.groupingBy(OrderItem::getOrderId));
// 组装数据
orders.forEach(order -> {
order.setUser(userMap.get(order.getUserId()));
order.setItems(itemMap.get(order.getId()));
});
}
5.3 性能优化实践
对于上述实现,我们可以进一步优化:
- 使用DTO投影:只查询需要的字段
java复制@Select("SELECT o.id, o.order_no, o.amount, u.name as userName FROM order o LEFT JOIN user u ON o.user_id = u.id ${ew.customSqlSegment}")
IPage<OrderDTO> selectOrderDTOPage(Page<OrderDTO> page, @Param(Constants.WRAPPER) Wrapper<Order> wrapper);
- 使用MybatisPlus的join功能(3.5.0+版本):
java复制List<Order> orders = new LambdaQueryChainWrapper<>(orderMapper)
.select(Order.class, info -> !info.getColumn().equals("deleted"))
.leftJoin(User.class, User::getId, Order::getUserId)
.eq(User::getName, "张三")
.list();
- 缓存常用数据:如用户信息等不常变化的数据可以缓存起来
6. 最佳实践与避坑指南
6.1 推荐实践
- 保持链式调用的可读性:适当换行和缩进
java复制List<User> users = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getStatus, 1)
.nested(wq -> wq
.like(User::getName, "张")
.or()
.like(User::getPhone, "138")
)
.orderByAsc(User::getAge)
.last("LIMIT 10")
.list();
- 复用查询条件:
java复制private LambdaQueryWrapper<User> baseQuery() {
return new LambdaQueryWrapper<User>()
.eq(User::getDeleted, 0)
.eq(User::getTenantId, TenantContext.getCurrentTenantId());
}
public List<User> listActiveUsers() {
return new LambdaQueryChainWrapper<>(userMapper, baseQuery())
.eq(User::getStatus, 1)
.list();
}
6.2 常见问题与解决方案
- Boolean类型处理:
java复制// 错误写法 - 当flag为null时会生成错误的SQL
.eq(User::getFlag, flag)
// 正确写法
.eq(flag != null, User::getFlag, flag)
- 日期范围查询:
java复制// 错误写法 - 可能无法使用索引
.apply("DATE(create_time) BETWEEN '2023-01-01' AND '2023-01-31'")
// 正确写法
.ge(createTime != null, User::getCreateTime, createTime.atStartOfDay())
.lt(endTime != null, User::getCreateTime, endTime.plusDays(1).atStartOfDay())
- 动态表名问题:
java复制// 使用动态表名处理器
public class MyTableNameHandler implements TableNameHandler {
@Override
public String dynamicTableName(String sql, String tableName) {
return tableName + "_" + TenantContext.getCurrentTenantId();
}
}
// 配置拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor(new MyTableNameHandler()));
return interceptor;
}
6.3 扩展技巧
- 自定义SQL片段:
java复制default List<User> listAdults() {
return new LambdaQueryChainWrapper<>(this)
.ge(User::getAge, 18)
.list();
}
default List<User> searchByName(String name) {
return new LambdaQueryChainWrapper<>(this)
.like(User::getName, name)
.list();
}
- 与MybatisX插件配合使用:
MybatisX 是 IntelliJ IDEA 的插件,可以自动生成 LambdaQueryChainWrapper 代码:
-
在Mapper接口中右键
-
选择 "MybatisX" -> "Generate" -> "Lambda Query Chain Wrapper"
-
选择要生成的查询方法
-
与Spring Cache集成:
java复制@Cacheable(value = "users", key = "#root.methodName + ':' + #name")
public List<User> findByName(String name) {
return new LambdaQueryChainWrapper<>(userMapper)
.like(User::getName, name)
.list();
}
7. 未来发展与替代方案
7.1 MybatisPlus 新版本特性
MybatisPlus 3.5.0+ 版本引入了一些新特性:
- 真正的Lambda链式join:
java复制List<Order> orders = new LambdaQueryChainWrapper<>(orderMapper)
.leftJoin(User.class, User::getId, Order::getUserId)
.eq(User::getName, "张三")
.list();
- 更加灵活的select方法:
java复制List<User> users = new LambdaQueryChainWrapper<>(userMapper)
.select(User.class, info -> !info.getColumn().equals("password"))
.list();
7.2 其他ORM框架对比
- 与JPA的对比:
- JPA的Criteria API也提供了类型安全的查询
- 但MybatisPlus更贴近SQL,灵活性更高
- MybatisPlus的学习曲线相对较低
- 与JOOQ的对比:
- JOOQ提供了更强大的DSL和类型安全
- 但需要数据库表结构的元信息生成代码
- MybatisPlus配置更简单,适合快速开发
7.3 复杂场景的替代方案
对于极其复杂的查询场景,可以考虑:
- 使用Mybatis的XML映射文件:对于特别复杂的SQL,XML映射文件可能更合适
- 使用视图或存储过程:将复杂逻辑放在数据库层
- 使用QueryDSL:提供更强大的类型安全查询能力
8. 总结与个人实践建议
在实际项目中使用 LambdaQueryChainWrapper 进行联合查询时,我有以下几点建议:
-
合理划分查询复杂度:
- 简单的关联查询可以使用自动映射
- 中等复杂度的查询可以使用 LambdaQueryChainWrapper 组合
- 特别复杂的查询考虑使用XML或原生SQL
-
注意性能问题:
- 警惕N+1查询问题
- 大数据量查询考虑分页
- 合理使用索引
-
保持代码整洁:
- 将复杂查询逻辑封装到Repository层
- 使用DTO投影减少数据传输量
- 适当添加注释说明复杂查询的业务含义
-
测试覆盖:
- 对复杂查询编写单元测试
- 使用不同的参数组合测试边界条件
- 考虑使用内存数据库(H2)进行集成测试
-
监控与优化:
- 使用Druid等工具监控SQL性能
- 定期检查慢查询日志
- 根据业务增长调整查询策略
最后,LambdaQueryChainWrapper 是 MybatisPlus 中非常强大的功能,合理使用可以大幅提高开发效率和代码质量。但它也不是银弹,需要根据实际场景选择最合适的解决方案。
