一、核心概念
exists
和 notExists
是 MyBatis-Plus 条件构造器(如 QueryWrapper
, LambdaQueryWrapper
)提供的方法,用于生成 SQL 的 EXISTS (subquery)
和 NOT EXISTS (subquery)
子句。
EXISTS (subquery)
: 如果子查询subquery
返回至少一行记录,则EXISTS
条件为TRUE
,否则为FALSE
。EXISTS
通常用于检查关联表中是否存在满足条件的记录。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
同理。 - 适用构造器:
exists
和notExists
方法主要在QueryWrapper<T>
和LambdaQueryWrapper<T>
中使用,用于构建SELECT
语句的WHERE
子句。它们不用于UpdateWrapper
或LambdaUpdateWrapper
的SET
子句。
二、详细操作步骤
以下是使用 exists
和 notExists
条件的完整、详细步骤:
步骤 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
,但也可以在 UpdateWrapper
的 WHERE
子句中使用,用于条件更新。
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)
}
三、常见错误
忘记添加关联条件:
- 错误:
queryWrapper.exists(new LambdaQueryWrapper<Order>().eq(Order::getStatus, 1));
- 后果: 子查询
SELECT 1 FROM order WHERE status = 1
会独立执行。如果order
表中任何一条记录的status=1
,EXISTS
就为TRUE
,导致外部查询返回所有用户!逻辑完全错误。 - 解决: 必须在子查询中添加关联条件(如
.eq(Order::getUserId, User::getId)
),将子查询与外部查询关联起来。
- 错误:
在
UpdateWrapper
的SET
子句中误用:- 错误: 试图在
set()
方法中使用exists
。 - 后果:
UpdateWrapper
的set()
方法用于定义SET column = value
,exists
是WHERE
子句的条件,不能在这里使用。 - 解决:
exists
/notExists
只能用于构造WHERE
子句,应放在QueryWrapper
或UpdateWrapper
的WHERE
链中。
- 错误: 试图在
字段名混淆 (在
QueryWrapper
字符串模式下):- 错误: 在
QueryWrapper
的子查询中,eq("user_id", "user_id")
。 - 后果: 这通常会被解释为
order.user_id = order.user_id
(恒真),而不是order.user_id = user.id
。 - 解决: 在子查询中引用外部表字段时,要清楚地知道上下文。通常直接写外部表的字段名(如
"id"
)即可,MyBatis-Plus 会根据 SQL 结构推断。使用LambdaQueryWrapper
可以完全避免此问题。
- 错误: 在
性能不佳的子查询:
- 错误: 子查询没有使用索引,或者条件过于宽泛。
- 后果: 每次检查外部表的一行,都需要执行一次可能很慢的子查询,导致整体查询性能极差(N+1 问题的一种表现)。
- 解决: 确保子查询中用于关联和过滤的字段(如
order.user_id
,order.status
)有合适的数据库索引。
四、注意事项
- 关联条件是关键:
EXISTS
/NOT EXISTS
子查询的威力在于关联。没有正确的关联条件,结果通常是错误的(全真或全假)。 - 子查询性能:
EXISTS
的性能很大程度上取决于子查询的效率。确保子查询能快速执行,主要依靠索引。 SELECT 1
: MyBatis-Plus 生成的EXISTS
子查询通常是SELECT 1 ...
,因为EXISTS
只关心是否存在行,不关心具体列值,这是标准的优化做法。- 与
IN
的区别:EXISTS
是关联子查询,通常性能更好,尤其是当子查询结果集大时。IN
可以是关联或非关联的,但当IN
后面是子查询且结果集很大时,性能可能不如EXISTS
。IN
对NULL
值敏感(expr IN (..., NULL, ...)
永远为UNKNOWN
),而EXISTS
不受此影响。
- 空值 (
NULL
) 处理:EXISTS
子查询中NULL
值的处理遵循 SQL 标准。例如,eq("status", null)
会生成status IS NULL
,这是正确的。 NOT EXISTS
与LEFT JOIN ... IS NULL
:NOT EXISTS
通常可以被优化器转换为LEFT JOIN ... WHERE right_table.key IS NULL
。两者语义等价,性能可能相近,选择更清晰的写法即可。- 谨慎用于
UPDATE
/DELETE
: 在UpdateWrapper
或DeleteWrapper
中使用EXISTS
/NOT EXISTS
进行条件更新或删除时,务必仔细测试,确保WHERE
条件的逻辑正确,避免误操作大量数据。
五、使用技巧
- 优先使用
LambdaQueryWrapper
: 在构建EXISTS
/NOT EXISTS
子查询时,强烈推荐使用LambdaQueryWrapper
,因为它能提供字段引用的编译时检查,避免字符串拼写错误,代码更清晰。 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) );
- 结合其他条件:
exists
/notExists
可以与其他条件(eq
,like
等)组合使用。// 查找姓名包含 "John" 且有订单的用户 queryWrapper.like(User::getName, "John") .exists(subQueryWrapper);
apply()
在子查询中: 可以在子查询的Wrapper
中使用apply()
拼接复杂的 SQL 片段。queryWrapper.exists(new LambdaQueryWrapper<Order>() .eq(Order::getUserId, User::getId) .apply("amount > {0}", BigDecimal.valueOf(1000)) // 订单金额大于1000 );
- 调试生成的 SQL: 开启 MyBatis 日志,查看最终生成的 SQL,确认
EXISTS
子查询的WHERE
条件是否正确包含了关联。
六、最佳实践与性能优化
- 明确使用场景:
EXISTS
/NOT EXISTS
最适合“存在性”检查。对于简单的等值关联,INNER JOIN
或LEFT JOIN
可能更直观。 - 强制使用关联: 在编写
exists
/notExists
时,养成习惯,首先写出关联条件(.eq(子表外键, 主表主键)
),再添加其他过滤条件。 - 索引优化:
- 确保子查询中用于关联的字段(如
order.user_id
)有索引。 - 确保子查询中用于过滤的字段(如
order.status
)有索引,特别是当这些条件能显著减少子查询结果集时。 - 考虑创建复合索引(如
(user_id, status)
)以同时优化关联和过滤。
- 确保子查询中用于关联的字段(如
- 避免在子查询中
SELECT *
: MyBatis-Plus 默认生成SELECT 1
,这是最佳实践,无需更改。 - 考虑
JOIN
替代方案: 对于某些查询,使用INNER JOIN
或LEFT JOIN ... WHERE ... IS NULL
可能产生相同的执行计划,甚至更优。可以通过执行计划分析(EXPLAIN
)来比较。 - 分页与
EXISTS
:EXISTS
查询可以正常进行分页 (Page<User> page = ...; userMapper.selectPage(page, queryWrapper);
)。 - 单元测试: 为包含
exists
/notExists
的查询编写单元测试,使用内存数据库(如 H2)或测试数据,验证其逻辑正确性。 - 监控慢查询: 在生产环境中监控执行时间较长的包含
EXISTS
/NOT EXISTS
的 SQL,及时进行优化。
通过本教程,您应该已经掌握了 MyBatis-Plus 中 exists
和 notExists
条件的精髓。记住,正确的关联条件和子查询的性能是使用这两个条件成功的关键。优先使用 LambdaQueryWrapper
以获得类型安全和代码清晰度。