一、核心概念

existsnotExists 是 MyBatis-Plus 条件构造器(如 QueryWrapper, LambdaQueryWrapper)提供的方法,用于生成 SQL 的 EXISTS (subquery)NOT EXISTS (subquery) 子句。

  • EXISTS (subquery): 如果子查询 subquery 返回至少一行记录,则 EXISTS 条件为 TRUE,否则为 FALSEEXISTS 通常用于检查关联表中是否存在满足条件的记录。
  • NOT EXISTS (subquery): 如果子查询 subquery 不返回任何记录,则 NOT EXISTS 条件为 TRUE,否则为 FALSE。常用于查找“没有”关联记录的情况。
  • 子查询 (Subquery): exists/notExists 方法接收一个 Wrapper 对象作为参数,这个 Wrapper 用于构建子查询的 SQL。这个子查询通常是针对另一个表(或同一个表)的 SELECT 查询。
  • 关联性: EXISTS 子查询通常与外部查询相关联(Correlated Subquery),即子查询中会引用外部查询的字段。MyBatis-Plus 允许在子查询的 Wrapper 中通过字段名(字符串)或 Lambda 表达式引用外部表的字段,从而实现关联。
  • 性能: EXISTS 通常比 IN 在处理“存在性”检查时更高效,尤其是在子查询结果集较大时,因为 EXISTS 在找到第一个匹配行后就会停止搜索(短路求值)。NOT EXISTS 同理。
  • 适用构造器: existsnotExists 方法主要在 QueryWrapper<T>LambdaQueryWrapper<T> 中使用,用于构建 SELECT 语句的 WHERE 子句。它们不用于 UpdateWrapperLambdaUpdateWrapperSET 子句。

二、详细操作步骤

以下是使用 existsnotExists 条件的完整、详细步骤:

步骤 1: 引入依赖

确保您的项目中已正确引入 MyBatis-Plus 依赖。

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version> <!-- 使用最新稳定版本 -->
</dependency>

步骤 2: 创建相关实体类

假设我们有两个表:user (用户) 和 order (订单)。一个用户可以有多个订单。

User 实体类 (user 表):

import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;

@TableName("user")
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("name")
    private String name;

    @TableField("email")
    private String email;

    // 省略 Getter、Setter、构造函数
    public User() {}
    // ... (Getter and Setter)
}

Order 实体类 (order 表):

import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.math.BigDecimal;

@TableName("`order`") // 注意:order 是 SQL 关键字,可能需要用反引号包围
public class Order implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("user_id")
    private Long userId; // 关联到 user 表的 id

    @TableField("order_no")
    private String orderNo;

    @TableField("amount")
    private BigDecimal amount;

    @TableField("status")
    private Integer status; // 0:待支付, 1:已支付, 2:已取消

    // 省略 Getter、Setter、构造函数
    public Order() {}
    // ... (Getter and Setter)
}

步骤 3: 创建 Mapper 接口

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 提供基本的 CRUD 操作
}

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    // 可能提供订单相关的操作
}

步骤 4: 在 Service 中使用 exists / notExists

场景 1: 查找拥有至少一个订单的用户 (EXISTS)

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 使用 LambdaQueryWrapper 查找拥有至少一个订单的用户
     * @return 用户列表
     */
    public List<User> getUsersWithOrders() {
        // 1. 创建主查询的 LambdaQueryWrapper (针对 User)
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

        // 2. 构建 EXISTS 子查询
        // exists() 方法接收一个 Wrapper 参数,用于构建子查询
        queryWrapper.exists(new LambdaQueryWrapper<Order>()
            // 子查询:从 order 表中查找
            .eq(Order::getUserId, User::getId) // 关键:关联条件!子查询的 userId 等于外部查询的 user.id
            // 可以添加更多子查询条件,例如只查已支付的订单
            // .eq(Order::getStatus, 1)
        );
        // 生成的 SQL 类似:
        // SELECT * FROM user WHERE EXISTS (SELECT 1 FROM `order` WHERE `order`.user_id = user.id)

        // 3. 执行查询
        List<User> users = userMapper.selectList(queryWrapper);
        return users;
    }

    /**
     * 使用 QueryWrapper (字符串) 查找拥有至少一个订单的用户
     * @return 用户列表
     */
    public List<User> getUsersWithOrdersByString() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();

        // 使用字符串字段名
        // 注意:在子查询的 Wrapper 中,"id" 指的是外部表 (user) 的 id
        // 这是通过 SQL 上下文关联的,MyBatis-Plus 会正确处理
        queryWrapper.exists(new QueryWrapper<Order>()
            .eq("user_id", "id") // "id" 指 user 表的 id
            // .eq("status", 1)
        );

        return userMapper.selectList(queryWrapper);
    }
}

场景 2: 查找没有任何订单的用户 (NOT EXISTS)

/**
 * 使用 LambdaQueryWrapper 查找没有任何订单的用户
 * @return 用户列表
 */
public List<User> getUsersWithoutOrders() {
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

    // 使用 notExists
    queryWrapper.notExists(new LambdaQueryWrapper<Order>()
        .eq(Order::getUserId, User::getId) // 关联条件
    );
    // 生成的 SQL 类似:
    // SELECT * FROM user WHERE NOT EXISTS (SELECT 1 FROM `order` WHERE `order`.user_id = user.id)

    return userMapper.selectList(queryWrapper);
}

/**
 * 使用 QueryWrapper (字符串) 查找没有任何订单的用户
 * @return 用户列表
 */
public List<User> getUsersWithoutOrdersByString() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.notExists(new QueryWrapper<Order>()
        .eq("user_id", "id") // "id" 指 user 表的 id
    );

    return userMapper.selectList(queryWrapper);
}

圔景 3: 复杂的 EXISTS 条件 (查找有未支付订单的用户)

/**
 * 查找拥有至少一个状态为“待支付”(status=0) 的订单的用户
 * @return 用户列表
 */
public List<User> getUsersWithPendingOrders() {
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

    queryWrapper.exists(new LambdaQueryWrapper<Order>()
        .eq(Order::getUserId, User::getId) // 关联
        .eq(Order::getStatus, 0)          // 子查询额外条件:订单状态为待支付
    );
    // SQL: SELECT * FROM user WHERE EXISTS (SELECT 1 FROM `order` WHERE `order`.user_id = user.id AND `order`.status = 0)

    return userMapper.selectList(queryWrapper);
}

场景 4: 在 UPDATE 操作中使用 EXISTS (通过 UpdateWrapper)

虽然 exists/notExists 主要在 QueryWrapper 中用于 SELECT,但也可以在 UpdateWrapperWHERE 子句中使用,用于条件更新。

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;

/**
 * 将拥有至少一个“已支付”订单的用户的邮箱后缀统一更新为 @premium.com
 * (仅作演示,实际业务需谨慎)
 */
@Transactional
public int updateEmailForPremiumUsers() {
    LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();

    // SET 子句
    updateWrapper.setSql("email = CONCAT(LEFT(email, LOCATE('@', email) - 1), '@premium.com')");

    // WHERE 子句使用 EXISTS
    updateWrapper.exists(new LambdaQueryWrapper<Order>()
        .eq(Order::getUserId, User::getId) // 关联
        .eq(Order::getStatus, 1)           // 订单已支付
    );

    // 执行更新 (第一个参数 entity 传 null,因为 SET 由 wrapper 定义)
    return userMapper.update(null, updateWrapper);
    // SQL: UPDATE user SET email = CONCAT(...) WHERE EXISTS (SELECT 1 FROM `order` WHERE `order`.user_id = user.id AND `order`.status = 1)
}

三、常见错误

  1. 忘记添加关联条件:

    • 错误: queryWrapper.exists(new LambdaQueryWrapper<Order>().eq(Order::getStatus, 1));
    • 后果: 子查询 SELECT 1 FROM order WHERE status = 1 会独立执行。如果 order 表中任何一条记录的 status=1EXISTS 就为 TRUE,导致外部查询返回所有用户!逻辑完全错误。
    • 解决: 必须在子查询中添加关联条件(如 .eq(Order::getUserId, User::getId)),将子查询与外部查询关联起来。
  2. UpdateWrapperSET 子句中误用:

    • 错误: 试图在 set() 方法中使用 exists
    • 后果: UpdateWrapperset() 方法用于定义 SET column = valueexistsWHERE 子句的条件,不能在这里使用。
    • 解决: exists/notExists 只能用于构造 WHERE 子句,应放在 QueryWrapperUpdateWrapperWHERE 链中。
  3. 字段名混淆 (在 QueryWrapper 字符串模式下):

    • 错误: 在 QueryWrapper 的子查询中,eq("user_id", "user_id")
    • 后果: 这通常会被解释为 order.user_id = order.user_id (恒真),而不是 order.user_id = user.id
    • 解决: 在子查询中引用外部表字段时,要清楚地知道上下文。通常直接写外部表的字段名(如 "id")即可,MyBatis-Plus 会根据 SQL 结构推断。使用 LambdaQueryWrapper 可以完全避免此问题。
  4. 性能不佳的子查询:

    • 错误: 子查询没有使用索引,或者条件过于宽泛。
    • 后果: 每次检查外部表的一行,都需要执行一次可能很慢的子查询,导致整体查询性能极差(N+1 问题的一种表现)。
    • 解决: 确保子查询中用于关联和过滤的字段(如 order.user_id, order.status)有合适的数据库索引。

四、注意事项

  1. 关联条件是关键: EXISTS/NOT EXISTS 子查询的威力在于关联。没有正确的关联条件,结果通常是错误的(全真或全假)。
  2. 子查询性能: EXISTS 的性能很大程度上取决于子查询的效率。确保子查询能快速执行,主要依靠索引。
  3. SELECT 1: MyBatis-Plus 生成的 EXISTS 子查询通常是 SELECT 1 ...,因为 EXISTS 只关心是否存在行,不关心具体列值,这是标准的优化做法。
  4. IN 的区别:
    • EXISTS 是关联子查询,通常性能更好,尤其是当子查询结果集大时。
    • IN 可以是关联或非关联的,但当 IN 后面是子查询且结果集很大时,性能可能不如 EXISTSINNULL 值敏感(expr IN (..., NULL, ...) 永远为 UNKNOWN),而 EXISTS 不受此影响。
  5. 空值 (NULL) 处理: EXISTS 子查询中 NULL 值的处理遵循 SQL 标准。例如,eq("status", null) 会生成 status IS NULL,这是正确的。
  6. NOT EXISTSLEFT JOIN ... IS NULL: NOT EXISTS 通常可以被优化器转换为 LEFT JOIN ... WHERE right_table.key IS NULL。两者语义等价,性能可能相近,选择更清晰的写法即可。
  7. 谨慎用于 UPDATE/DELETE: 在 UpdateWrapperDeleteWrapper 中使用 EXISTS/NOT EXISTS 进行条件更新或删除时,务必仔细测试,确保 WHERE 条件的逻辑正确,避免误操作大量数据。

五、使用技巧

  1. 优先使用 LambdaQueryWrapper: 在构建 EXISTS/NOT EXISTS 子查询时,强烈推荐使用 LambdaQueryWrapper,因为它能提供字段引用的编译时检查,避免字符串拼写错误,代码更清晰。
  2. nested() 在子查询中: 子查询内部也可以使用 nested() 来构建复杂的括号逻辑。
    queryWrapper.exists(new LambdaQueryWrapper<Order>()
        .eq(Order::getUserId, User::getId)
        .nested(iw -> iw.eq(Order::getStatus, 0).or().eq(Order::getStatus, 1)) // (status=0 OR status=1)
    );
    
  3. 结合其他条件: exists/notExists 可以与其他条件(eq, like 等)组合使用。
    // 查找姓名包含 "John" 且有订单的用户
    queryWrapper.like(User::getName, "John")
                .exists(subQueryWrapper);
    
  4. apply() 在子查询中: 可以在子查询的 Wrapper 中使用 apply() 拼接复杂的 SQL 片段。
    queryWrapper.exists(new LambdaQueryWrapper<Order>()
        .eq(Order::getUserId, User::getId)
        .apply("amount > {0}", BigDecimal.valueOf(1000)) // 订单金额大于1000
    );
    
  5. 调试生成的 SQL: 开启 MyBatis 日志,查看最终生成的 SQL,确认 EXISTS 子查询的 WHERE 条件是否正确包含了关联。

六、最佳实践与性能优化

  1. 明确使用场景: EXISTS/NOT EXISTS 最适合“存在性”检查。对于简单的等值关联,INNER JOINLEFT JOIN 可能更直观。
  2. 强制使用关联: 在编写 exists/notExists 时,养成习惯,首先写出关联条件(.eq(子表外键, 主表主键)),再添加其他过滤条件。
  3. 索引优化:
    • 确保子查询中用于关联的字段(如 order.user_id)有索引。
    • 确保子查询中用于过滤的字段(如 order.status)有索引,特别是当这些条件能显著减少子查询结果集时。
    • 考虑创建复合索引(如 (user_id, status))以同时优化关联和过滤。
  4. 避免在子查询中 SELECT *: MyBatis-Plus 默认生成 SELECT 1,这是最佳实践,无需更改。
  5. 考虑 JOIN 替代方案: 对于某些查询,使用 INNER JOINLEFT JOIN ... WHERE ... IS NULL 可能产生相同的执行计划,甚至更优。可以通过执行计划分析(EXPLAIN)来比较。
  6. 分页与 EXISTS: EXISTS 查询可以正常进行分页 (Page<User> page = ...; userMapper.selectPage(page, queryWrapper);)。
  7. 单元测试: 为包含 exists/notExists 的查询编写单元测试,使用内存数据库(如 H2)或测试数据,验证其逻辑正确性。
  8. 监控慢查询: 在生产环境中监控执行时间较长的包含 EXISTS/NOT EXISTS 的 SQL,及时进行优化。

通过本教程,您应该已经掌握了 MyBatis-Plus 中 existsnotExists 条件的精髓。记住,正确的关联条件子查询的性能是使用这两个条件成功的关键。优先使用 LambdaQueryWrapper 以获得类型安全和代码清晰度。