跳到主要内容

EXISTS / NOT EXISTS 操作符

使用 EXISTS 和 NOT EXISTS 操作符可以查询关系字段是否存在满足条件的关联实体,无需加载关联数据到内存。

核心概念

什么是 EXISTS?

EXISTS 操作符用于检查关系字段中是否存在满足特定条件的关联实体:

  • 高性能: SQL 层面的子查询,避免加载大量关联数据
  • 类型安全: TypeScript 自动推断子查询类型
  • 增量更新: 支持响应式查询的智能刷新
  • 跨框架: Angular/React/Vue 统一 API

支持的关系类型

关系类型装饰器EXISTS 支持
一对多 (1:N)@OneToMany
多对一 (N:1)@ManyToOne
一对一 (1:1)@OneToOne
多对多 (N:M)@ManyToMany

基础用法

检查关系是否存在

// 查询有订单的用户
const usersWithOrders = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [{ field: 'orders', operator: 'exists' }]
}
})
);

检查关系不存在

// 查询没有订单的用户
const usersWithoutOrders = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [{ field: 'orders', operator: 'notExists' }]
}
})
);

带条件的 EXISTS

基本条件过滤

// 查询有"已完成"订单的用户
const usersWithCompletedOrders = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'status', operator: '=', value: 'completed' }]
}
}
]
}
})
);

复杂条件组合

// 查询有"高价值且未退款"订单的用户
const premiumUsers = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [
{ field: 'totalAmount', operator: '>', value: 1000 },
{ field: 'refunded', operator: '=', value: false },
{ field: 'status', operator: '=', value: 'completed' }
]
}
}
]
}
})
);

日期范围查询

// 查询最近30天有订单的活跃用户
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

const activeUsers = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'createdAt', operator: '>=', value: thirtyDaysAgo.toISOString() }]
}
}
]
}
})
);

各关系类型示例

ONE_TO_MANY (一对多)

@Entity({ name: 'Menu', namespace: 'public' })
class Menu extends EntityBase {
@PrimaryColumn({ type: PropertyType.uuid })
id!: string;

@Column({ type: PropertyType.string })
title!: string;

@Column({ type: PropertyType.uuid, nullable: true })
parentId?: string | null;

@OneToMany(() => Menu, 'parentId', { eager: false })
children?: Menu[];
}

// 查询有子菜单的菜单项
const menusWithChildren = await firstValueFrom(
Menu.find({
where: {
combinator: 'and',
rules: [{ field: 'children', operator: 'exists' }]
}
})
);

// 查询有"激活"子菜单的菜单项
const menusWithActiveChildren = await firstValueFrom(
Menu.find({
where: {
combinator: 'and',
rules: [
{
field: 'children',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'active', operator: '=', value: true }]
}
}
]
}
})
);

MANY_TO_ONE (多对一)

@Entity({ name: 'Order', namespace: 'shop' })
class Order extends EntityBase {
@PrimaryColumn({ type: PropertyType.uuid })
id!: string;

@Column({ type: PropertyType.uuid })
userId!: string;

@ManyToOne(() => User, 'userId', { eager: false })
user?: User;
}

// 查询有关联用户的订单(通常不为 null)
const ordersWithUser = await firstValueFrom(
Order.find({
where: {
combinator: 'and',
rules: [{ field: 'user', operator: 'exists' }]
}
})
);

// 查询关联到 VIP 用户的订单
const vipOrders = await firstValueFrom(
Order.find({
where: {
combinator: 'and',
rules: [
{
field: 'user',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'vipLevel', operator: '>=', value: 3 }]
}
}
]
}
})
);

ONE_TO_ONE (一对一)

@Entity({ name: 'User', namespace: 'shop' })
class User extends EntityBase {
@PrimaryColumn({ type: PropertyType.uuid })
id!: string;

@Column({ type: PropertyType.uuid, nullable: true })
profileId?: string | null;

@OneToOne(() => Profile, 'profileId', { eager: false })
profile?: Profile;
}

// 查询有资料的用户
const usersWithProfile = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [{ field: 'profile', operator: 'exists' }]
}
})
);

// 查询资料已验证的用户
const verifiedUsers = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [
{
field: 'profile',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'verified', operator: '=', value: true }]
}
}
]
}
})
);

MANY_TO_MANY (多对多)

@Entity({ name: 'Project', namespace: 'public' })
class Project extends EntityBase {
@PrimaryColumn({ type: PropertyType.uuid })
id!: string;

@ManyToMany(() => Tag, {
junctionEntity: 'ProjectTag',
junctionNamespace: 'public',
currentKey: 'projectId',
relatedKey: 'tagId',
eager: false
})
tags?: Tag[];
}

// 查询有标签的项目
const projectsWithTags = await firstValueFrom(
Project.find({
where: {
combinator: 'and',
rules: [{ field: 'tags', operator: 'exists' }]
}
})
);

// 查询带有特定标签的项目
const urgentProjects = await firstValueFrom(
Project.find({
where: {
combinator: 'and',
rules: [
{
field: 'tags',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'name', operator: '=', value: 'urgent' }]
}
}
]
}
})
);

高级用法

多个 EXISTS 条件组合

// 查询同时满足多个关系条件的用户
const qualifiedUsers = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [
// 有已完成订单
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'status', operator: '=', value: 'completed' }]
}
},
// 有正面评价
{
field: 'reviews',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'rating', operator: '>=', value: 4 }]
}
},
// 没有退款
{
field: 'refunds',
operator: 'notExists'
}
]
}
})
);

EXISTS 与普通条件混合

// 结合实体自身条件和关系条件
const targetUsers = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [
// 用户自身条件
{ field: 'age', operator: '>=', value: 18 },
{ field: 'active', operator: '=', value: true },
// 关系条件
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'totalAmount', operator: '>', value: 500 }]
}
}
]
}
})
);

自引用关系

// 查询有子节点的菜单项(自引用)
const parentMenus = await firstValueFrom(
Menu.find({
where: {
combinator: 'and',
rules: [{ field: 'children', operator: 'exists' }]
}
})
);

// 查询有激活子节点的菜单项
const menusWithActiveChildren = await firstValueFrom(
Menu.find({
where: {
combinator: 'and',
rules: [
{
field: 'children',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'active', operator: '=', value: true }]
}
}
]
}
})
);

响应式查询支持

EXISTS 操作符完全支持响应式查询的增量更新。

自动刷新场景

场景 1: 创建关联实体

// 查询有订单的用户
const users$ = User.find({
where: {
combinator: 'and',
rules: [{ field: 'orders', operator: 'exists' }]
}
});

// 创建新订单时,查询自动更新
const order = new Order();
order.userId = 'user-123';
await order.save(); // ✅ 用户 'user-123' 自动加入结果集

场景 2: 更新关联实体

// 查询有已完成订单的用户
const users$ = User.find({
where: {
combinator: 'and',
rules: [
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'status', operator: '=', value: 'completed' }]
}
}
]
}
});

// 订单状态变更时,查询自动更新
order.status = 'completed';
await order.save(); // ✅ 订单所属用户自动加入结果集

场景 3: 删除关联实体

// 查询有订单的用户
const users$ = User.find({
where: {
combinator: 'and',
rules: [{ field: 'orders', operator: 'exists' }]
}
});

// 删除用户的最后一个订单
await order.remove(); // ✅ 用户自动从结果集移除(如果没有其他订单)

性能优化

SQL 层面优化

EXISTS 操作符在 SQL 层面使用子查询实现,性能优于传统 JOIN:

-- EXISTS 查询(推荐)
SELECT * FROM "public$User" _
WHERE EXISTS (
SELECT 1 FROM "shop$Order" child
WHERE child."userId" = _."id"
AND child."status" = 'completed'
);

-- 传统 JOIN(可能产生重复行)
SELECT DISTINCT _ .* FROM "public$User" _
LEFT JOIN "shop$Order" child ON child."userId" = _."id"
WHERE child."status" = 'completed';

性能对比:

场景EXISTSJOIN
存在性检查⚡ 快 (找到第一个即返回)慢 (需扫描所有)
内存占用低 (不加载关联数据)高 (加载并去重)
索引利用

索引建议

为 EXISTS 查询添加合适的索引:

@Entity({ name: 'Order', namespace: 'shop' })
@Index({ name: 'idx_order_user_status', columns: ['userId', 'status'] })
class Order extends EntityBase {
@Column({ type: PropertyType.uuid })
userId!: string;

@Column({ type: PropertyType.string })
status!: string;
}

避免的反模式

不要: 先加载关联数据再过滤

// 低效:加载所有订单到内存
const users = await User.findAll({ relations: ['orders'] });
const filtered = users.filter(u => u.orders?.some(o => o.status === 'completed'));

推荐: 使用 EXISTS 在数据库层面过滤

// 高效:数据库层面过滤
const users = await firstValueFrom(
User.find({
where: {
combinator: 'and',
rules: [
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [{ field: 'status', operator: '=', value: 'completed' }]
}
}
]
}
})
);

类型安全

TypeScript 自动推断子查询的类型:

// ✅ 类型安全:TypeScript 知道 where 子句应该是 Order 的条件
User.find({
where: {
combinator: 'and',
rules: [
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [
{ field: 'status', operator: '=', value: 'completed' }, // ✅ Order.status
{ field: 'totalAmount', operator: '>', value: 100 } // ✅ Order.totalAmount
]
}
}
]
}
});

// ❌ 编译错误:unknownField 不存在于 Order 类型
User.find({
where: {
combinator: 'and',
rules: [
{
field: 'orders',
operator: 'exists',
where: {
combinator: 'and',
rules: [
{ field: 'unknownField', operator: '=', value: 'x' } // ❌ TypeScript 报错
]
}
}
]
}
});

常见问题

Q: EXISTS 和 JOIN 有什么区别?

A:

  • EXISTS: 只检查存在性,SQL 找到第一个匹配就返回,不加载关联数据
  • JOIN: 加载所有匹配的关联数据,可能产生重复行,需要 DISTINCT

Q: EXISTS 支持嵌套条件吗?

A: 支持!可以在 where 子句中使用任意复杂的条件组合(and/or)。

Q: 性能如何?

A: EXISTS 通常比 JOIN 更快,特别是:

  • ✅ 只需检查存在性(不需要关联数据)
  • ✅ 关联表有大量记录
  • ✅ 有合适的索引

Q: 如何调试 EXISTS 查询?

A:

  1. 检查 SQL 日志(SQLite 控制台)
  2. 使用 EXPLAIN QUERY PLAN 分析执行计划
  3. 确认关联字段有索引

Q: 响应式查询何时刷新?

A: 当以下情况发生时自动刷新:

  • 关联实体被创建/更新/删除
  • 关联实体的条件字段变更
  • 中间表记录变更(MANY_TO_MANY)

最佳实践

  1. 优先使用 EXISTS 进行存在性检查,避免加载不必要的关联数据
  2. 添加索引 到 WHERE 子句中使用的字段
  3. 简化条件 避免过于复杂的嵌套子查询
  4. 利用类型推断 让 TypeScript 帮助你编写正确的查询
  5. 测试性能 在大数据集上验证查询效率

参考