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. 常见错误

  1. 字段名错误

    • 错误queryWrapper.eq("userName", "张三"); (实体类字段是 name)。
    • 解决:使用 @TableField 注解映射,或使用 LambdaQueryWrapper 避免字符串硬编码。
  2. 空值处理不当

    • 错误:直接 queryWrapper.eq("name", name);namenull 时,会生成 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);
      
  3. 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));
      
  4. 忽略分页插件

    • 错误:使用 selectPage 但未配置 MyBatis-Plus 分页插件。
    • 解决:在 Spring Boot 配置类中配置 PaginationInterceptor (旧版) 或 MybatisPlusInterceptor (新版)。
  5. LambdaQueryWrapper 类型推断失败

    • 错误:在复杂链式调用中,IDE 可能无法正确推断 LambdaQueryWrapper 的泛型类型。
    • 解决:显式声明变量类型,或检查链式调用是否中断。
  6. 在 Wrapper 中使用 == null 判断

    • 错误wrapper.eq("name", null); 会生成 name = null,通常应使用 isNull("name")
    • 解决:使用 isNullisNotNull 方法。

4. 注意事项

  1. SQL 注入风险:避免在 apply() 方法中直接拼接用户输入。优先使用带 {} 占位符的方法。
  2. 性能考虑
    • 避免在 LIKE 左侧使用通配符(如 LIKE '%abc'),这通常无法利用索引。
    • 合理使用索引字段进行查询。
    • 分页时注意深分页问题(LIMIT 1000000, 10)。
  3. NULL 值处理:数据库中的 NULL 与 Java 的 null 处理要一致,注意 IS NULL/IS NOT NULL 的使用场景。
  4. 字符串拼接:虽然 QueryWrapper 内部会处理字符串拼接,但复杂的动态 SQL 仍需仔细测试。
  5. 版本兼容性:不同版本的 MyBatis-Plus 可能在 API 上有细微差别,注意查看对应版本的文档。
  6. 事务管理:更新、删除操作通常需要在事务中执行。
  7. 日志输出:开启 MyBatis-Plus 的 SQL 日志(如 logging.level.com.baomidou.mybatisplus=DEBUG)有助于调试生成的 SQL。

5. 使用技巧

  1. 优先使用 LambdaQueryWrapper:类型安全,重构安全,避免字段名拼写错误。
  2. 条件构造器复用:可以将公共的查询条件提取出来复用。
    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));
    }
    
  3. 利用 Consumer<QueryWrapper> 进行条件分组:清晰表达复杂的 AND/OR 逻辑。
  4. select 方法指定字段:只查询需要的字段,减少网络传输和内存消耗。
  5. last 方法:在 SQL 末尾追加 SQL 片段(慎用,可能有注入风险,且不支持分页)。
    queryWrapper.last("LIMIT 1"); // 确保只返回一条 (不推荐用于分页)
    
  6. func 方法:提供函数式编程接口,链式调用更灵活。
    queryWrapper.func(i -> {
        if (someCondition) {
            i.eq("name", "张三");
        } else {
            i.like("name", "李");
        }
    });
    
  7. condition 参数:许多方法(如 eq, like 等)有重载版本接受一个 boolean 条件参数,只有当条件为 true 时才添加该条件。
    wrapper.like(StringUtils.isNotBlank(name), User::getName, name)
           .ge(age != null, User::getAge, age);
    

6. 最佳实践与性能优化

  1. 最佳实践

    • 明确需求:在编写查询前,明确业务需求和需要的字段。
    • 使用 Lambda:始终优先使用 LambdaQueryWrapperLambdaUpdateWrapper
    • 条件判空:对可能为空的查询参数进行判空处理,避免不必要的 IS NULL 或错误的 = null
    • 合理分页:前端分页或后端分页要明确,后端分页必须配置分页插件。
    • 避免 N+1 查询:对于关联查询,考虑使用 @TableField(exist = false) + @Select 注解或 @ResultMap 进行关联查询,或使用 MyBatis-Plus 的 @SelectJoin (如果支持)。
    • 代码可读性:将复杂的 QueryWrapper 构建过程封装成方法或使用清晰的变量名。
    • 单元测试:对包含复杂查询条件的 Service 方法编写单元测试,验证 SQL 逻辑正确性。
  2. 性能优化

    • 索引优化:确保查询条件(特别是 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)的参数。