MyBatis-Plus 复杂查询Lambda+Wrapper 多条件功能实现
作者:Jinkxs
Java 数据 01:MyBatis-Plus 复杂查询(Lambda+Wrapper 多条件)
在现代 Java Web 开发中,持久层框架的选择对于提升开发效率和系统性能至关重要。MyBatis-Plus 作为 MyBatis 的增强工具,极大地简化了 MyBatis 的使用,提供了丰富的 CRUD 操作和强大的条件构造器。特别是其 Lambda 和 Wrapper 结合使用的方式,使得构建复杂查询语句变得简洁而优雅。本文将深入探讨如何利用 MyBatis-Plus 的 Lambda + Wrapper 功能进行复杂的多条件查询。
一、MyBatis-Plus 简介与核心概念
1.1 什么是 MyBatis-Plus?
MyBatis-Plus 是一个 MyBatis 的增强工具,它在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。它提供了许多便捷的功能,如:
- 代码生成器:一键生成 Mapper、Service、Controller 等代码。
- 通用 CRUD 操作:无需编写 SQL,即可完成基本的增删改查。
- 条件构造器 (Wrapper):提供强大且灵活的条件拼接功能。
- 分页插件:轻松实现分页查询。
- 性能分析插件:帮助分析 SQL 性能。
- 乐观锁支持:简化并发控制。
- 多租户支持:方便实现数据隔离。
1.2 核心组件
- Mapper 接口:继承
BaseMapper<T>,获得通用 CRUD 方法。 - Entity 实体类:映射数据库表结构。
- Wrapper 条件构造器:用于构建查询、更新、删除的条件。
- LambdaQueryWrapper / LambdaUpdateWrapper:基于 Lambda 表达式的 Wrapper,避免硬编码字段名,提高安全性。
二、Lambda + Wrapper 基础入门
2.1 准备工作
首先,确保你的项目已引入 MyBatis-Plus 依赖。例如,在 Maven 项目中:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version> <!-- 请根据实际情况选择版本 -->
</dependency>假设我们有一个用户表 user,对应的实体类 User 如下:
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("user") // 指定数据库表名
public class User {
@TableId(value = "id", type = IdType.AUTO) // 主键自增
private Long id;
private String name; // 用户名
private Integer age; // 年龄
private String email; // 邮箱
private Integer status; // 状态 (例如: 0 - 无效, 1 - 有效)
@TableField(fill = FieldFill.INSERT) // 插入时自动填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充
private LocalDateTime updateTime;
// 构造函数、getter、setter 等省略...
}对应的 Mapper 接口:
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper 后,通常不需要额外定义方法
}2.2 基本查询示例
让我们从最基础的查询开始。假设我们要查询所有状态为有效的用户(status = 1)。
// UserService.java
package com.example.demo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getUsersByStatus(Integer status) {
// 使用 LambdaQueryWrapper 构建查询条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getStatus, status); // WHERE status = ?
return userMapper.selectList(queryWrapper);
}
// 或者更简洁的写法,使用 Wrappers 工具类
public List<User> getUsersByStatusWithWrappers(Integer status) {
return userMapper.selectList(Wrappers.<User>lambdaQuery().eq(User::getStatus, status));
}
}关键点解析:
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();创建了一个 Lambda 查询包装器。queryWrapper.eq(User::getStatus, status);构建了一个等于条件。User::getStatus是一个方法引用,它指向User类的getStatus()方法,MyBatis-Plus 会通过反射获取该方法对应的字段名(status)。userMapper.selectList(queryWrapper);执行查询,返回符合条件的对象列表。
这种方式相比传统 MyBatis 直接写 SQL,具有以下优势:
- 类型安全:使用方法引用来指定字段,避免了字符串写错导致的运行时错误。
- 可维护性高:当实体类属性名修改时,IDE 会提示相关代码需要修改,降低维护成本。
- 语法简洁:代码更加清晰易读。
三、复杂查询条件详解
3.1 基础比较操作符
MyBatis-Plus 提供了丰富的条件构造方法,用于构建各种比较操作。
public List<User> getComplexUsers() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 等于 (eq)
wrapper.eq(User::getStatus, 1);
// 不等于 (ne)
wrapper.ne(User::getAge, 18);
// 大于 (gt)
wrapper.gt(User::getAge, 18);
// 大于等于 (ge)
wrapper.ge(User::getAge, 18);
// 小于 (lt)
wrapper.lt(User::getAge, 60);
// 小于等于 (le)
wrapper.le(User::getAge, 60);
// 模糊匹配 (like)
wrapper.like(User::getName, "张"); // 名字包含 '张'
// 左模糊匹配 (likeLeft)
wrapper.likeLeft(User::getName, "张"); // 名字以 '张' 开头
// 右模糊匹配 (likeRight)
wrapper.likeRight(User::getName, "三"); // 名字以 '三' 结尾
// 不为空 (isNotNull)
wrapper.isNotNull(User::getEmail);
// 为空 (isNull)
wrapper.isNull(User::getPhone); // 假设存在 phone 字段
// 在某个范围内 (between)
wrapper.between(User::getAge, 18, 60); // 年龄在 18 到 60 之间
// 在某个集合内 (in)
List<Integer> statuses = Arrays.asList(0, 1);
wrapper.in(User::getStatus, statuses); // 状态在 [0, 1] 中
// 不在某个集合内 (notIn)
List<Long> ids = Arrays.asList(1L, 2L, 3L);
wrapper.notIn(User::getId, ids); // ID 不在 [1, 2, 3] 中
// 空值处理 (isNotNull / isNull) 的组合
// 例如:查询有邮箱且状态为有效的用户
wrapper.isNotNull(User::getEmail).eq(User::getStatus, 1);
return userMapper.selectList(wrapper);
}3.2 逻辑操作符
在实际应用中,往往需要组合多个条件。MyBatis-Plus 提供了 and() 和 or() 方法来处理逻辑关系。
3.2.1 AND 连接
默认情况下,所有条件都是 AND 关系。例如,查询年龄大于等于 18 且小于等于 60 且状态为有效的用户:
public List<User> getUsersByAgeAndStatus() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 默认就是 AND 关系
wrapper.ge(User::getAge, 18)
.le(User::getAge, 60)
.eq(User::getStatus, 1);
return userMapper.selectList(wrapper);
}3.2.2 OR 连接
使用 or() 方法可以将条件转换为 OR 关系。例如,查询用户名包含“张”或邮箱包含“@qq.com”的用户:
public List<User> getUsersByNameOrEmail() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "张")
.or()
.like(User::getEmail, "@qq.com");
return userMapper.selectList(wrapper);
}3.2.3 复杂嵌套逻辑
可以通过 apply() 方法或者嵌套 LambdaQueryWrapper 来处理更复杂的逻辑。
使用 apply() 方法
apply() 方法允许直接传入 SQL 片段,适用于一些无法通过标准方法表达的复杂条件。
public List<User> getUsersWithApply() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 示例:查询邮箱后缀为 '.com' 且年龄在某个范围内的用户
wrapper.apply("email LIKE '%.com'")
.between(User::getAge, 20, 50);
return userMapper.selectList(wrapper);
}嵌套 LambdaQueryWrapper
对于复杂的 OR 和 AND 组合,可以创建子 LambdaQueryWrapper 并将其作为参数传递给 and() 或 or() 方法。
public List<User> getUsersComplexLogic() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 示例:查询满足以下任一条件的用户:
// 1. 年龄大于等于 18 且状态为有效 (age >= 18 AND status = 1)
// 2. 年龄小于 18 且邮箱包含 '@school.edu' (age < 18 AND email LIKE '%@school.edu')
wrapper.and(subWrapper -> subWrapper.ge(User::getAge, 18).eq(User::getStatus, 1))
.or()
.and(subWrapper -> subWrapper.lt(User::getAge, 18).like(User::getEmail, "@school.edu"));
return userMapper.selectList(wrapper);
}3.3 聚合函数与分组查询
MyBatis-Plus 也支持聚合函数和分组查询,常用于统计分析。
public void groupByAgeAndCount() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getAge, "COUNT(*) as count") // 选择年龄和计数
.groupBy(User::getAge); // 按年龄分组
// 注意:selectList 返回的是 List<User>,这里需要特别注意
// 如果你想得到分组后的结果,通常会用到 Map 或自定义 DTO
// 但 MyBatis-Plus 的 selectList 一般不直接返回聚合结果
// 更常见的是使用原生 SQL 或者自定义方法
// 这里展示一种方式,可能需要调整
}
// 更推荐的做法是使用自定义 SQL 或者结合分页插件
// 例如,使用自定义 Mapper 方法对于更复杂的聚合查询,建议使用自定义 SQL 或者结合 MyBatis 的 <select> 标签。
3.4 排序与分页
3.4.1 排序 (orderBy)
public List<User> getUsersSortedByAgeDesc() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(User::getAge); // 按年龄降序排列
return userMapper.selectList(wrapper);
}
public List<User> getUsersSortedMultiple() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 先按年龄降序,再按创建时间升序
wrapper.orderByDesc(User::getAge).orderByAsc(User::getCreateTime);
return userMapper.selectList(wrapper);
}3.4.2 分页 (Page)
MyBatis-Plus 提供了分页插件,配合 Page 对象使用。
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
public IPage<User> getUsersPage(int current, int size) {
Page<User> page = new Page<>(current, size); // 当前页码,每页大小
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1) // 状态为有效
.orderByDesc(User::getCreateTime); // 按创建时间倒序
// 执行分页查询
return userMapper.selectPage(page, wrapper);
}四、高级特性与最佳实践
4.1 动态查询构建
在实际业务中,查询条件往往是动态的。我们可以根据传入的参数,动态地构建 LambdaQueryWrapper。
public List<User> searchUsers(String name, Integer minAge, Integer maxAge, Integer status, Boolean includeInactive) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 动态添加条件
if (name != null && !name.isEmpty()) {
wrapper.like(User::getName, name);
}
if (minAge != null) {
wrapper.ge(User::getAge, minAge);
}
if (maxAge != null) {
wrapper.le(User::getAge, maxAge);
}
if (status != null) {
wrapper.eq(User::getStatus, status);
}
// 特殊处理:如果 includeInactive 为 false,则排除状态为 0 的用户
if (includeInactive != null && !includeInactive) {
wrapper.ne(User::getStatus, 0); // 不等于 0
} else if (includeInactive == null || includeInactive) {
// 如果 includeInactive 为 null 或 true,可以不加任何条件,或者加上其他逻辑
// 这里简单示例,不加额外条件
}
// 添加排序
wrapper.orderByDesc(User::getCreateTime);
return userMapper.selectList(wrapper);
}4.2 使用select()指定字段
为了提高查询效率,通常只查询需要的字段,而不是使用 *。
public List<User> getUsersOnlyNameAndEmail() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getName, User::getEmail); // 只查询 name 和 email 字段
return userMapper.selectList(wrapper);
}4.3 使用selectObjs()获取单个字段值
如果你只需要查询某个字段的所有值,可以使用 selectObjs()。
public List<String> getUserNames() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getName); // 查询所有用户的姓名
return userMapper.selectObjs(wrapper, row -> (String) row); // 转换为 String 列表
}4.4 使用exists()和notExists()
用于检查子查询是否存在记录。
public List<User> getUsersWithOrders() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 查询有订单的用户
wrapper.exists(
"SELECT 1 FROM order o WHERE o.user_id = user.id"
);
return userMapper.selectList(wrapper);
}4.5 性能优化建议
- 索引优化:确保查询条件中涉及的字段有合适的数据库索引。
- **避免 SELECT ***:明确指定需要的字段。
- 合理使用分页:避免一次性查询大量数据。
- 缓存查询结果:对于不经常变动的数据,可以考虑使用缓存。
- 避免 N+1 查询:在关联查询时注意性能问题。
五、实战案例:用户管理后台查询接口
让我们结合一个完整的场景来演示如何使用 Lambda + Wrapper 构建复杂的查询。假设我们需要实现一个用户管理后台的查询接口,支持以下功能:
- 按用户名模糊搜索
- 按年龄区间筛选
- 按状态筛选
- 按邮箱后缀筛选
- 支持排序(按创建时间倒序)
- 支持分页
5.1 控制器层 (Controller)
package com.example.demo.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.dto.UserQueryDTO;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 查询用户列表
* @param queryDTO 查询条件
* @return 分页结果
*/
@GetMapping("/list")
public Page<User> listUsers(UserQueryDTO queryDTO) {
return userService.getUsersPage(queryDTO);
}
/**
* 获取所有用户(不分页)
* @param queryDTO 查询条件
* @return 用户列表
*/
@GetMapping("/all")
public List<User> getAllUsers(UserQueryDTO queryDTO) {
return userService.getAllUsers(queryDTO);
}
}5.2 DTO 层 (Data Transfer Object)
package com.example.demo.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserQueryDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String name; // 用户名
private Integer minAge; // 最小年龄
private Integer maxAge; // 最大年龄
private Integer status; // 状态
private String emailSuffix; // 邮箱后缀 (例如: @gmail.com)
private Integer current = 1; // 当前页码
private Integer size = 10; // 每页大小
}5.3 Service 层 (Service)
package com.example.demo.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.dto.UserQueryDTO;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 分页查询用户
* @param queryDTO 查询条件
* @return 分页结果
*/
public Page<User> getUsersPage(UserQueryDTO queryDTO) {
Page<User> page = new Page<>(queryDTO.getCurrent(), queryDTO.getSize());
// 构建查询条件
buildQueryCondition(queryDTO, page);
return userMapper.selectPage(page, page.getOptimizeJoinFlag());
}
/**
* 获取所有符合条件的用户(不分页)
* @param queryDTO 查询条件
* @return 用户列表
*/
public List<User> getAllUsers(UserQueryDTO queryDTO) {
// 构建查询条件
buildQueryCondition(queryDTO, null); // 注意:这里传入 null 或者构建一个没有分页的 wrapper
// 由于 selectPage 需要 wrapper 参数,我们直接调用 selectList
// 但是为了复用逻辑,我们先构建一个 wrapper
return userMapper.selectList(buildQueryCondition(queryDTO, null));
}
/**
* 构建查询条件并返回 LambdaQueryWrapper
* @param queryDTO 查询条件
* @param page 分页对象(可选,如果不需要分页则传 null)
* @return LambdaQueryWrapper
*/
private com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User> buildQueryCondition(UserQueryDTO queryDTO, Page<User> page) {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User> wrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
// 动态添加查询条件
if (queryDTO.getName() != null && !queryDTO.getName().isEmpty()) {
wrapper.like(User::getName, queryDTO.getName());
}
if (queryDTO.getMinAge() != null) {
wrapper.ge(User::getAge, queryDTO.getMinAge());
}
if (queryDTO.getMaxAge() != null) {
wrapper.le(User::getAge, queryDTO.getMaxAge());
}
if (queryDTO.getStatus() != null) {
wrapper.eq(User::getStatus, queryDTO.getStatus());
}
if (queryDTO.getEmailSuffix() != null && !queryDTO.getEmailSuffix().isEmpty()) {
// 使用 likeRight 或者 apply 都可以
// 这里使用 likeRight 示例
wrapper.likeRight(User::getEmail, queryDTO.getEmailSuffix());
// 或者使用 apply: wrapper.apply("email LIKE '%" + queryDTO.getEmailSuffix() + "'");
}
// 添加排序
wrapper.orderByDesc(User::getCreateTime);
// 如果传入了分页对象,则设置分页信息
if (page != null) {
// 这里可以设置分页信息,但实际上 selectPage 会自动处理
// 但为了清晰,我们可以将 wrapper 传回给 selectPage
// 注意:selectPage 方法本身会接收 wrapper 和 page 对象
}
return wrapper;
}
// 修正后的 getAllUsers 方法,使用单独的方法构建 wrapper
public List<User> getAllUsersCorrect(UserQueryDTO queryDTO) {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User> wrapper = buildQueryCondition(queryDTO, null);
return userMapper.selectList(wrapper);
}
}5.4 Mapper 层 (Mapper)
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper 后,通常不需要额外定义方法
// 如果需要自定义 SQL,可以在 XML 文件中定义
}5.5 请求示例
启动应用后,可以通过如下请求来测试接口:
- 获取所有有效用户(分页):
GET /users/list?status=1¤t=1&size=10 - 获取用户名包含“李”且年龄在 20 到 40 之间的有效用户:
GET /users/list?name=李&minAge=20&maxAge=40&status=1¤t=1&size=10 - 获取邮箱以 “@gmail.com” 结尾的所有用户:
GET /users/list?emailSuffix=@gmail.com¤t=1&size=10
六、常见问题与解决方案
6.1 字段名拼写错误
使用 Lambda 表达式可以有效避免字段名拼写错误的问题,因为编译期就会检查方法引用是否正确。
6.2 性能问题
- 原因:未建立合适的索引、查询字段过多、未使用分页等。
- 解决:确保查询字段上有索引;只查询需要的字段;使用分页。
6.3 空指针异常
- 原因:在构建条件时,没有对传入的参数进行非空判断。
- 解决:在构建条件前,对参数进行判空处理。
6.4 SQL 注入风险
- 原因:使用
apply()方法时,如果直接拼接用户输入,可能导致 SQL 注入。 - 解决:使用参数化查询,避免直接拼接用户输入。对于
apply(),应谨慎使用,并确保输入经过验证。
七、总结与展望
通过本文的介绍,我们深入了解了 MyBatis-Plus 中 Lambda + Wrapper 的强大功能。它不仅极大地简化了 SQL 的编写,还提高了代码的安全性和可维护性。从简单的条件查询到复杂的嵌套逻辑,从动态构建到性能优化,Lambda + Wrapper 都能胜任。
掌握这些技巧,可以让我们的 Java Web 开发更加高效和优雅。随着 MyBatis-Plus 的持续发展,相信未来会有更多便捷的功能加入,进一步提升开发体验。
📚 参考资料
希望这篇文章能帮助你更好地理解和使用 MyBatis-Plus 的 Lambda + Wrapper 功能!🚀
到此这篇关于MyBatis-Plus 复杂查询Lambda+Wrapper 多条件功能实现的文章就介绍到这了,更多相关mybits-plus复杂查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
