1. 核心概念
QueryWrapper 是 MyBatis-Plus 框架提供的一个强大的条件构造器(Wrapper),用于构建复杂的 SQL 查询条件,避免手写 SQL 语句,提高开发效率和代码可读性。
- 核心思想:面向对象的方式构建 SQL WHERE 条件。
- 主要作用:替代 MyBatis 的 XML 中的
<if>
、<where>
等标签进行动态 SQL 拼接。 - 核心类:
QueryWrapper<T>
:用于查询操作(SELECT)。UpdateWrapper<T>
:用于更新操作(UPDATE),除了查询条件,还能设置更新字段。LambdaQueryWrapper<T>
:基于 Lambda 表达式的QueryWrapper
,类型安全,避免硬编码字段名。
- 链式调用:所有条件方法都返回
this
,支持流畅的链式编程。
2. 操作步骤 (非常详细)
步骤 1: 引入依赖
确保项目中已引入 MyBatis-Plus 依赖。以 Maven 为例:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>最新稳定版本</version> <!-- 例如 3.5.3 -->
</dependency>
步骤 2: 创建实体类 (Entity)
创建一个与数据库表对应的 Java 实体类,并使用 MyBatis-Plus 注解(如 @TableName
, @TableId
, @TableField
)。
import com.baomidou.mybatisplus.annotation.*;
@Data // Lombok 注解,生成 getter/setter/toString 等
@TableName("user") // 指定对应表名
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("name")
private String name;
@TableField("age")
private Integer age;
@TableField("email")
private String email;
@TableField("create_time")
private LocalDateTime createTime;
}
步骤 3: 创建 Mapper 接口
创建一个继承 BaseMapper<T>
的 Mapper 接口。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper 后,自动拥有 CRUD 方法
// 如 selectList, selectById, insert, updateById, deleteById 等
}
步骤 4: 在 Service/Controller 中使用 QueryWrapper
这是最核心的步骤。通过 QueryWrapper
构建查询条件。
4.1 创建 QueryWrapper 实例
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
// 方式一:普通 QueryWrapper (需要写字段名字符串)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 方式二:LambdaQueryWrapper (推荐,类型安全)
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
4.2 添加查询条件 (常用方法详解)
注意:以下方法均返回 this
,可链式调用。
等于 (=)
// WHERE name = '张三' queryWrapper.eq("name", "张三"); // Lambda 方式 (推荐) lambdaQueryWrapper.eq(User::getName, "张三");
不等于 (!=)
queryWrapper.ne("name", "李四"); lambdaQueryWrapper.ne(User::getName, "李四");
大于 (>)
queryWrapper.gt("age", 18); lambdaQueryWrapper.gt(User::getAge, 18);
大于等于 (>=)
queryWrapper.ge("age", 18); lambdaQueryWrapper.ge(User::getAge, 18);
小于 (<)
queryWrapper.lt("age", 60); lambdaQueryWrapper.lt(User::getAge, 60);
小于等于 (<=)
queryWrapper.le("age", 60); lambdaQueryWrapper.le(User::getAge, 60);
BETWEEN AND (在...之间)
// WHERE age BETWEEN 18 AND 60 queryWrapper.between("age", 18, 60); lambdaQueryWrapper.between(User::getAge, 18, 60);
NOT BETWEEN AND (不在...之间)
queryWrapper.notBetween("age", 18, 60); lambdaQueryWrapper.notBetween(User::getAge, 18, 60);
IN (在...集合中)
List<String> names = Arrays.asList("张三", "李四", "王五"); // WHERE name IN ('张三', '李四', '王五') queryWrapper.in("name", names); lambdaQueryWrapper.in(User::getName, names);
NOT IN (不在...集合中)
queryWrapper.notIn("name", names); lambdaQueryWrapper.notIn(User::getName, names);
LIKE (模糊匹配)
// WHERE name LIKE '%三%' queryWrapper.like("name", "三"); lambdaQueryWrapper.like(User::getName, "三"); // WHERE name LIKE '张%' queryWrapper.likeRight("name", "张"); // 或 queryWrapper.like("name", "张%") lambdaQueryWrapper.likeRight(User::getName, "张"); // WHERE name LIKE '%三' queryWrapper.likeLeft("name", "三"); // 或 queryWrapper.like("name", "%三") lambdaQueryWrapper.likeLeft(User::getName, "三");
NOT LIKE (不模糊匹配)
queryWrapper.notLike("name", "三"); lambdaQueryWrapper.notLike(User::getName, "三");
NULL 值判断
// WHERE email IS NULL queryWrapper.isNull("email"); lambdaQueryWrapper.isNull(User::getEmail); // WHERE email IS NOT NULL queryWrapper.isNotNull("email"); lambdaQueryWrapper.isNotNull(User::getEmail);
AND / OR 连接条件
// 默认所有条件是 AND 关系 // WHERE name = '张三' AND age > 18 queryWrapper.eq("name", "张三").gt("age", 18); lambdaQueryWrapper.eq(User::getName, "张三").gt(User::getAge, 18); // 显式使用 AND (通常不需要,因为默认就是 AND) queryWrapper.eq("name", "张三").and(wrapper -> wrapper.gt("age", 18)); // 使用 OR (需要括号包裹,避免歧义) // WHERE name = '张三' OR name = '李四' queryWrapper.eq("name", "张三").or().eq("name", "李四"); // 更清晰的写法 (推荐) queryWrapper.and(wrapper -> wrapper.eq("name", "张三").or().eq("name", "李四")); // 或者 queryWrapper.or(wrapper -> wrapper.eq("name", "张三").eq("name", "李四")); // 这里内部是 AND // 复杂条件: (name = '张三' AND age > 18) OR (name = '李四' AND age < 30) queryWrapper .and(wrapper -> wrapper.eq("name", "张三").gt("age", 18)) .or(wrapper -> wrapper.eq("name", "李四").lt("age", 30));
分组 (GROUP BY)
// GROUP BY age queryWrapper.groupBy("age"); lambdaQueryWrapper.groupBy(User::getAge); // GROUP BY age, name queryWrapper.groupBy("age", "name"); lambdaQueryWrapper.groupBy(User::getAge, User::getName);
排序 (ORDER BY)
// ORDER BY age ASC queryWrapper.orderByAsc("age"); lambdaQueryWrapper.orderByAsc(User::getAge); // ORDER BY age DESC queryWrapper.orderByDesc("age"); lambdaQueryWrapper.orderByDesc(User::getAge); // ORDER BY age ASC, create_time DESC queryWrapper.orderByAsc("age").orderByDesc("create_time"); lambdaQueryWrapper.orderByAsc(User::getAge).orderByDesc(User::getCreateTime);
分页 (Pagination)
// 需要配合 MyBatis-Plus 的分页插件使用 import com.baomidou.mybatisplus.extension.plugins.pagination.Page; Page<User> page = new Page<>(1, 10); // 第1页,每页10条 IPage<User> resultPage = userMapper.selectPage(page, queryWrapper); // resultPage.getRecords() 获取当前页数据 // resultPage.getTotal() 获取总记录数 // resultPage.getPages() 获取总页数
SELECT 字段 (指定查询列)
// SELECT id, name, age FROM user queryWrapper.select("id", "name", "age"); // Lambda 方式 (推荐) lambdaQueryWrapper.select(User::getId, User::getName, User::getAge); // 排除某些字段 (不推荐,可能影响性能或逻辑) // queryWrapper.select(User.class, tableFieldInfo -> !tableFieldInfo.getColumn().equals("password"));
HAVING (分组后条件)
// HAVING COUNT(*) > 1 queryWrapper.having("COUNT(*) > {0}", 1); // {0} 是占位符 // 或者 queryWrapper.having("COUNT(*) > 1");
EXISTS / NOT EXISTS
// WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = user.id) queryWrapper.exists("SELECT 1 FROM `order` o WHERE o.user_id = user.id"); queryWrapper.notExists("SELECT 1 FROM `order` o WHERE o.user_id = user.id");
自定义 SQL 片段 (使用 apply)
// 慎用,可能有 SQL 注入风险 queryWrapper.apply("date_format(create_time, '%Y-%m') = {0}", "2023-10"); // 生成: WHERE date_format(create_time, '%Y-%m') = '2023-10'
4.3 执行查询
使用 Mapper 的方法传入 QueryWrapper
执行查询。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getUsersByNameAndAge(String name, Integer minAge) {
// 创建 QueryWrapper
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 添加条件
if (name != null && !name.trim().isEmpty()) {
wrapper.like(User::getName, name);
}
if (minAge != null) {
wrapper.ge(User::getAge, minAge);
}
// 执行查询
return userMapper.selectList(wrapper);
}
public User getUserById(Long id) {
return userMapper.selectById(id);
}
// 使用 UpdateWrapper 进行更新
public boolean updateUserEmail(Long id, String newEmail) {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", id).set("email", newEmail);
// 或者使用 LambdaUpdateWrapper
// LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
// lambdaUpdateWrapper.eq(User::getId, id).set(User::getEmail, newEmail);
return userMapper.update(null, updateWrapper) > 0;
}
}
3. 常见错误
字段名错误:
- 错误:
queryWrapper.eq("userName", "张三");
(实体类字段是name
)。 - 解决:使用
@TableField
注解映射,或使用LambdaQueryWrapper
避免字符串硬编码。
- 错误:
空值处理不当:
- 错误:直接
queryWrapper.eq("name", name);
当name
为null
时,会生成name = null
,通常不是想要的(可能是IS NULL
)。 - 解决:在添加条件前进行判空。
if (name != null && !name.isEmpty()) { wrapper.eq(User::getName, name); } // 或者使用 QueryWrapper 的条件方法 (3.4.0+) wrapper.eq(StringUtils.isNotBlank(name), "name", name);
- 错误:直接
AND/OR 逻辑错误:
- 错误:
wrapper.eq("a", 1).or().eq("b", 2).eq("c", 3);
期望(a=1) OR (b=2 AND c=3)
,实际是(a=1 OR b=2) AND c=3
。 - 解决:使用
and(Consumer<QueryWrapper>)
或or(Consumer<QueryWrapper>)
明确分组。wrapper.eq("a", 1).or(w -> w.eq("b", 2).eq("c", 3));
- 错误:
忽略分页插件:
- 错误:使用
selectPage
但未配置 MyBatis-Plus 分页插件。 - 解决:在 Spring Boot 配置类中配置
PaginationInterceptor
(旧版) 或MybatisPlusInterceptor
(新版)。
- 错误:使用
LambdaQueryWrapper 类型推断失败:
- 错误:在复杂链式调用中,IDE 可能无法正确推断
LambdaQueryWrapper
的泛型类型。 - 解决:显式声明变量类型,或检查链式调用是否中断。
- 错误:在复杂链式调用中,IDE 可能无法正确推断
在 Wrapper 中使用
== null
判断:- 错误:
wrapper.eq("name", null);
会生成name = null
,通常应使用isNull("name")
。 - 解决:使用
isNull
或isNotNull
方法。
- 错误:
4. 注意事项
- SQL 注入风险:避免在
apply()
方法中直接拼接用户输入。优先使用带{}
占位符的方法。 - 性能考虑:
- 避免在
LIKE
左侧使用通配符(如LIKE '%abc'
),这通常无法利用索引。 - 合理使用索引字段进行查询。
- 分页时注意深分页问题(
LIMIT 1000000, 10
)。
- 避免在
- NULL 值处理:数据库中的
NULL
与 Java 的null
处理要一致,注意IS NULL
/IS NOT NULL
的使用场景。 - 字符串拼接:虽然
QueryWrapper
内部会处理字符串拼接,但复杂的动态 SQL 仍需仔细测试。 - 版本兼容性:不同版本的 MyBatis-Plus 可能在 API 上有细微差别,注意查看对应版本的文档。
- 事务管理:更新、删除操作通常需要在事务中执行。
- 日志输出:开启 MyBatis-Plus 的 SQL 日志(如
logging.level.com.baomidou.mybatisplus=DEBUG
)有助于调试生成的 SQL。
5. 使用技巧
- 优先使用
LambdaQueryWrapper
:类型安全,重构安全,避免字段名拼写错误。 - 条件构造器复用:可以将公共的查询条件提取出来复用。
private LambdaQueryWrapper<User> createBaseQuery() { return new LambdaQueryWrapper<User>().eq(User::getDeleted, 0); // 假设软删除 } public List<User> getActiveUsersByName(String name) { return userMapper.selectList(createBaseQuery().like(User::getName, name)); }
- 利用
Consumer<QueryWrapper>
进行条件分组:清晰表达复杂的 AND/OR 逻辑。 select
方法指定字段:只查询需要的字段,减少网络传输和内存消耗。last
方法:在 SQL 末尾追加 SQL 片段(慎用,可能有注入风险,且不支持分页)。queryWrapper.last("LIMIT 1"); // 确保只返回一条 (不推荐用于分页)
func
方法:提供函数式编程接口,链式调用更灵活。queryWrapper.func(i -> { if (someCondition) { i.eq("name", "张三"); } else { i.like("name", "李"); } });
condition
参数:许多方法(如eq
,like
等)有重载版本接受一个boolean
条件参数,只有当条件为true
时才添加该条件。wrapper.like(StringUtils.isNotBlank(name), User::getName, name) .ge(age != null, User::getAge, age);
6. 最佳实践与性能优化
最佳实践:
- 明确需求:在编写查询前,明确业务需求和需要的字段。
- 使用 Lambda:始终优先使用
LambdaQueryWrapper
或LambdaUpdateWrapper
。 - 条件判空:对可能为空的查询参数进行判空处理,避免不必要的
IS NULL
或错误的= null
。 - 合理分页:前端分页或后端分页要明确,后端分页必须配置分页插件。
- 避免 N+1 查询:对于关联查询,考虑使用
@TableField(exist = false)
+@Select
注解或@ResultMap
进行关联查询,或使用 MyBatis-Plus 的@SelectJoin
(如果支持)。 - 代码可读性:将复杂的
QueryWrapper
构建过程封装成方法或使用清晰的变量名。 - 单元测试:对包含复杂查询条件的 Service 方法编写单元测试,验证 SQL 逻辑正确性。
性能优化:
- 索引优化:确保查询条件(特别是
WHERE
,ORDER BY
,GROUP BY
)中的字段有合适的数据库索引。 - **避免
SELECT ***:** 使用
select` 方法只查询必要的字段。 - 优化
LIKE
查询:尽量避免前导通配符%
,考虑使用全文索引(如 MySQL 的FULLTEXT
)或搜索引擎(如 Elasticsearch)处理复杂的文本搜索。 - 分页优化:
- 避免深分页,考虑使用游标分页(Cursor-based Pagination)或基于时间戳/ID 的分页。
- 对于大数据量分页,考虑使用延迟关联(Deferred Join)或覆盖索引(Covering Index)。
- 批量操作:对于大量数据的插入、更新、删除,使用 MyBatis-Plus 的
saveBatch
,updateBatchById
,deleteBatchIds
等批量方法。 - 缓存:对于不经常变化的查询结果,考虑使用 Redis 等缓存。
- 监控慢查询:启用数据库慢查询日志,分析并优化执行时间长的 SQL。
- 连接池配置:合理配置数据库连接池(如 HikariCP)的参数。
- 索引优化:确保查询条件(特别是