跳到主要内容

响应式查询优化

背景

如果我们的查询是可观察的,那么每当数据发生变化时,查询就需要重新计算一次;随着观察的数据越来越多计算也会随来越多,我们需要对查询进行优化。

核心问题

在响应式数据库中,每次数据变更都可能触发多个查询的重新计算,这会带来以下问题:

  • 性能开销大:SQL 查询需要遍历整个表,计算成本高
  • 响应延迟:大量查询同时执行会阻塞主线程
  • 资源浪费:重复查询相同或相似的数据集

基础场景示例

场景 1:我们正在查询 Todo 表的数据

// 查询全部的 todo 数据
Todo.findAll({
where: { combinator: 'and', rules: [] }
}).subscribe(todos => {
console.log('todos', todos);
});

接着我们又创建了一条类型 Todo 数据

const todo = new Todo({ title: '新任务' });
await todo.save();

显而易见结果里需要加入这条新数据,那么我们如何实现呢?

  • 方案一:重新执行 SQL 遍历整个表更新数据
  • 方案二:我们直接在结果里加入新数据增量更新

不难看出方案二是更高效的,在 Local-First 场景中也很安全,所以我们需要找到一个可以增量合并数据的机制来优化计算效率,通过实时的 JS 计算来减少更昂贵的 SQL 请求。

已知背景

  • 数据变化是可观察的,RxDB 会记录所有的数据变化并发送通知
  • 每个在使用的查询,都会记录完整的信息,原始查询语句,查询结果,查询状态,观察者数量等等信息
  • 查询管理器维护了实体类型依赖映射表,用于快速判断哪些变更会影响查询

实体事件类型

响应式查询优化主要监听以下实体变更事件:

事件类型常量说明触发时机
EntityNewEventENTITY_NEW_EVENT实体初始化通过 new 创建实例时
EntityCreateEventENTITY_CREATE_EVENT实体创建成功首次 save() 持久化到数据库
EntityUpdateEventENTITY_UPDATE_EVENT实体更新成功修改后 save() 更新到数据库
EntityRemoveEventENTITY_REMOVE_EVENT实体删除成功调用 remove() 从数据库删除

注意:EntityNewEvent 不会触发查询更新,因为此时实体尚未持久化到数据库。只有 CREATE/UPDATE/REMOVE 事件会触发查询的增量更新。

查询方法

方法参数说明适用场景
getid根据 id 获取实体精确查找单个实体
findOnewhere, orderBy查找一个实体条件查询,允许不存在
findOneOrFailwhere, orderBy查找一个实体,查不到就抛出错误条件查询,必须存在
findwhere, orderBy, limit, skip查找多个实体(带分页)分页列表查询
findAllwhere, orderBy查找所有实体(不分页)获取全量数据
findByCursorwhere, orderBy, cursor, take使用游标分页查找实体高效分页,支持无限滚动
countwhere统计实体数量获取符合条件的数据总数
countAncestorsentityId, where统计祖先节点数量树形结构,统计父级层级
countDescendantsentityId, where统计后代节点数量树形结构,统计子级层级
findAncestorsentityId, where, orderBy查找祖先节点树形结构,获取所有父级节点
findDescendantsentityId, where, orderBy查找后代节点树形结构,获取所有子级节点

增量更新算法

为了判断如何处理实体变更,系统会根据以下规则判断是执行 SQL 刷新还是 JS 重新计算:

刷新规则类型

系统定义了以下 6 种匹配规则:

规则名称说明适用场景
match_where实体变更后满足查询的 where 条件判断新实体或更新后的实体是否符合条件
not_match_where实体变更后不再满足查询的 where 条件判断更新后的实体是否需要从结果移除
match_where_before实体变更前满足查询的 where 条件(UPDATE 专用)判断更新前的实体是否符合条件
not_match_where_before实体变更前不满足查询的 where 条件(UPDATE 专用)判断是否为新匹配的实体
match_order_by实体变更影响查询的排序结果判断更新是否改变了排序顺序
result_contains变更的实体在当前查询结果中判断是否需要更新已有结果

规则组合逻辑

刷新规则是二维数组,表示多组规则的组合:

  • 同一组规则内的条件是 AND(与) 关系
  • 不同组之间是 OR(或) 关系

示例

// 规则:[['match_where', 'result_contains'], ['not_match_where']]
// 含义:(匹配查询条件 AND 结果包含) OR (不匹配查询条件)

不同查询类型的刷新策略

CREATE 事件(创建新实体)
查询类型SQL 刷新规则JS 重算规则说明
findAll-[['match_where']]直接添加到结果集,有排序则重新排序
find-[['match_where', 'match_order_by']]合并后重新排序和截取,只在结果变化时更新
findByCursor-[['match_where', 'match_order_by']]在游标范围内增量添加,不自动补充到 limit
findOne-[['match_where', 'match_order_by']]比较新实体与当前结果,按排序规则决定是否替换
findOneOrFail-[['match_where', 'match_order_by']]比较新实体与当前结果,按排序规则决定是否替换
count-[['match_where']]简单加法: 当前计数 + 新匹配实体数
findDescendants[['match_where']]-SQL 刷新: 新实体可能改变树形结构
findAncestors[['match_where']]-SQL 刷新: 新实体可能改变树形结构
countDescendants[['match_where']]-SQL 刷新: 新实体可能增加后代数量
countAncestors[['match_where']]-SQL 刷新: 新实体可能增加祖先数量

优化说明:

  • 大部分查询类型都使用 JS 增量更新,避免重复 SQL 查询
  • find 只在结果真正变化时更新(去重优化)
  • findByCursor 在游标范围内累积结果,不会强制截取到 limit
  • findOne/findOneOrFail 当有排序规则时比较新旧实体,智能决定是否替换
  • 树形查询使用 SQL 刷新,因为需要重新计算树形关系(性能开销可接受)
UPDATE 事件(更新实体)
查询类型SQL 刷新规则JS 重算规则说明
findAll-[['match_where']], [['not_match_where', 'match_where_before']]JS 完整更新: 移除不匹配 + 更新字段 + 添加新匹配
find[['result_contains']], [['match_where', 'not_match_where_before']]-结果集受影响或有新匹配时触发 SQL 刷新
findByCursor[['result_contains']], [['match_where', 'not_match_where_before']]-结果集受影响或有新匹配时触发 SQL 刷新
findOne-[['result_contains']], [['match_where', 'not_match_where_before']]混合策略: 无排序时 JS 更新,有排序时 SQL 刷新
findOneOrFail-[['result_contains']], [['match_where', 'not_match_where_before']]混合策略: 无排序时 JS 更新,有排序时 SQL 刷新
count-[['match_where']], [['match_where_before']]JS 增减计数: 新匹配数 - 不再匹配数
findDescendants[['result_contains']], [['match_where']]-SQL 刷新: 更新可能改变树形结构
findAncestors[['result_contains']], [['match_where']]-SQL 刷新: 更新可能改变树形结构
countDescendants[['match_where']], [['match_where_before']]-SQL 刷新: 更新可能改变后代数量
countAncestors[['match_where']], [['match_where_before']]-SQL 刷新: 更新可能改变祖先数量

UPDATE 场景特殊性:

  • 实体可能从"不匹配"变为"匹配"(newly_matched - 类似 CREATE)
  • 实体可能从"匹配"变为"不匹配"(newly_unmatched - 类似 REMOVE)
  • 实体仍然匹配但字段值变化(still_matched - 需要更新字段或重排序)
  • match_where: 检查更新后是否匹配(使用 patch)
  • match_where_before: 检查更新前是否匹配(使用 inversePatch)
  • not_match_where_before: 检查更新前是否不匹配(用于识别 newly_matched)

优化说明:

  • findAll 使用 JS 完整更新,一次性处理三种情况(newly_matched, newly_unmatched, still_matched)
  • find/findByCursor 触发 SQL 刷新以正确应用 limit 和游标逻辑
  • findOne/findOneOrFail 智能决策: 简单字段更新用 JS,排序变化或新匹配时用 SQL
  • count 只在计数真正变化时更新(added_count > 0 || removed_count > 0)
REMOVE 事件(删除实体)
查询类型SQL 刷新规则JS 重算规则说明
findAll-[['result_contains']]JS 直接移除,有排序则重新排序
find[['result_contains']]-SQL 刷新以补充数据(可能还有更多在 limit 外)
findByCursor-[['result_contains']]JS 直接移除,不需要补充
findOne[['result_contains']]-当前结果被删除时 SQL 刷新获取新的第一条
findOneOrFail[['result_contains']]-当前结果被删除时 SQL 刷新获取新的第一条
count-[['match_where']]简单减法: 当前计数 - 删除实体数
findDescendants[['result_contains']], [['match_where']]-SQL 刷新: 删除可能改变树形结构
findAncestors[['result_contains']], [['match_where']]-SQL 刷新: 删除可能改变树形结构
countDescendants[['match_where']]-SQL 刷新: 删除可能减少后代数量
countAncestors[['match_where']]-SQL 刷新: 删除可能减少祖先数量

优化说明:

  • findAll/findByCursor 使用 JS 直接移除,避免 SQL 查询
  • find 需要 SQL 刷新以补充数据到 limit
  • findOne/findOneOrFail 只在当前结果被删除时才刷新
  • count 使用 match_where 规则(检查 inversePatch)确保只减少匹配条件的实体数
  • 树形查询使用 SQL 刷新,因为删除可能影响整个树形结构

整体优化策略总结

1. CREATE 事件 - 全部使用 JS 增量更新
  • 所有查询类型都使用 JS 增量更新,完全避免 SQL 重查
  • 性能提升: ~10x (新增单个实体时)
  • 原因: 新增实体是最简单的场景,只需添加到结果集,JS 计算完全可靠
2. UPDATE 事件 - 智能分类处理
  • findAll: JS 完整更新 (移除 + 更新 + 添加)
    • 优势: 一次性处理三种状态变化,避免多次通知
  • find/findByCursor: SQL 刷新
    • 原因: 需要重新应用 limit/游标逻辑,确保分页准确性
  • findOne/findOneOrFail: 混合策略
    • 无排序: JS 更新字段
    • 有排序: SQL 刷新(确保仍是第一个)
    • 有新匹配: SQL 刷新(可能有更优的结果)
  • count: JS 增减计数
    • 优化: 只在计数真正变化时更新
3. REMOVE 事件 - 按查询类型差异化
  • findAll/findByCursor: JS 直接移除
    • findAll: 无需补充,直接减少
    • findByCursor: 游标范围内减少,不强制补充
  • find: SQL 刷新
    • 原因: 需要从 limit 之外补充数据
  • findOne/findOneOrFail: SQL 刷新(当前结果被删除时)
    • 原因: 需要获取新的第一条
  • count: JS 简单减法
4. 核心设计原则
  • 优先 JS 计算: CREATE 场景和简单的 REMOVE 场景
  • 必要时 SQL: 涉及 limit、orderBy 变化、游标补充、树形结构的场景
  • 智能决策: UPDATE 的 findOne 根据是否有排序规则动态选择
  • 去重优化: find 和 count 在结果不变时不触发通知
  • 树形查询特殊处理: 因为涉及复杂的祖先/后代关系计算,统一使用 SQL 刷新确保准确性

算法决策树

规则匹配示例

示例 1:CREATE 事件 + findAll 查询

// 查询配置
const query = Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
},
orderBy: [{ field: 'priority', sort: 'desc' }]
});

// 创建新 Todo
const todo = new Todo({ title: '新任务', completed: false, priority: 10 });
await todo.save();

// 规则匹配过程:
// 1. 事件类型:CREATE
// 2. 查询类型:findAll
// 3. JS 重算规则:[['match_where']]
// 4. 检查 match_where:todo.completed === false ✅
// 5. JS 处理:将新实体添加到结果集
// 6. 由于有 orderBy,对整个结果集重新排序
// 7. 结果:通知观察者,新任务按优先级插入到正确位置

示例 2:UPDATE 事件 + findAll 查询

// 查询配置
const query = Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
}
});

// 场景 A:从不匹配变为匹配 (newly_matched)
todo.completed = false; // 之前是 true
await todo.save();
// 处理:类似 CREATE,添加到结果集

// 场景 B:从匹配变为不匹配 (newly_unmatched)
todo.completed = true; // 之前是 false
await todo.save();
// 处理:类似 REMOVE,从结果集移除

// 场景 C:仍然匹配但字段变化 (still_matched)
todo.title = '更新标题'; // completed 仍是 false
await todo.save();
// 处理:更新结果集中该实体的字段值

// 规则匹配过程:
// 1. 事件类型:UPDATE
// 2. 查询类型:findAll
// 3. JS 重算规则:[['match_where']], [['not_match_where', 'match_where_before']]
// 4. 分类实体:
// - newly_matched: match_where(patch) && !match_where(inversePatch)
// - newly_unmatched: !match_where(patch) && match_where(inversePatch)
// - still_matched: match_where(patch) && match_where(inversePatch)
// 5. 一次性处理三种情况:移除 + 更新 + 添加
// 6. 如有 orderBy,重新排序

示例 3:UPDATE 事件 + find 查询

// 查询配置(带分页)
const query = Todo.find({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
},
limit: 10
});

// 更新 Todo
todo.completed = true;
await todo.save();

// 规则匹配过程:
// 1. 事件类型:UPDATE
// 2. 查询类型:find
// 3. SQL 刷新规则:[['result_contains']], [['match_where', 'not_match_where_before']]
// 4. 检查规则组 1:result_contains (todo 在当前结果中) ✅
// 5. 结果:执行 SQL 重新查询
// 6. 原因:涉及分页,需要从 limit 之外补充新数据

示例 4:REMOVE 事件 + findAll 查询

// 查询配置
const query = Todo.findAll({
where: { combinator: 'and', rules: [] }
});

// 删除 Todo
await todo.remove();

// 规则匹配过程:
// 1. 事件类型:REMOVE
// 2. 查询类型:findAll
// 3. JS 重算规则:[['result_contains']]
// 4. 检查 result_contains:todo 在结果中 ✅
// 5. JS 处理:从结果集中移除实体
// 6. 如有 orderBy,重新排序(保持顺序)
// 7. 结果:通知观察者,任务已被移除

示例 5:REMOVE 事件 + find 查询

// 查询配置(带分页)
const query = Todo.find({
where: { combinator: 'and', rules: [] },
limit: 20
});

// 删除 Todo
await todo.remove();

// 规则匹配过程:
// 1. 事件类型:REMOVE
// 2. 查询类型:find
// 3. SQL 刷新规则:[['result_contains']]
// 4. 检查 result_contains:todo 在当前结果中 ✅
// 5. 结果:执行 SQL 重新查询
// 6. 原因:需要从数据库补充第 21 条数据填充到 limit=20

示例 6:树形查询 - findDescendants

// 定义树形实体
@TreeEntity({
name: 'Category',
properties: [{ name: 'name', type: PropertyType.string }]
})
class Category extends TreeAdjacencyListEntityBase {
name: string;
}

// 查询某个分类的所有后代
const electronicsId = 'electronics-001';
const query = Category.findDescendants({
entityId: electronicsId,
where: { combinator: 'and', rules: [] }
});

// 场景 A: 创建新的子分类
const tablets = new Category({ name: '平板电脑' });
tablets.parent = electronics;
await tablets.save();

// 规则匹配过程:
// 1. 事件类型:CREATE
// 2. 查询类型:findDescendants
// 3. SQL 刷新规则:[['match_where']]
// 4. 检查 match_where:tablets 符合查询条件 ✅
// 5. 结果:执行 SQL 重新查询
// 6. 原因:新实体改变了树形结构,需要重新计算后代关系

// 场景 B: 更新分类的父级(移动节点)
tablets.parent = otherCategory;
await tablets.save();

// 规则匹配过程:
// 1. 事件类型:UPDATE
// 2. 查询类型:findDescendants
// 3. SQL 刷新规则:[['result_contains']], [['match_where']]
// 4. 检查 result_contains:tablets 在当前结果中 ✅
// 5. 结果:执行 SQL 重新查询
// 6. 原因:节点移动改变了树形结构,需要重新计算后代关系

// 场景 C: 删除子分类
await tablets.remove();

// 规则匹配过程:
// 1. 事件类型:REMOVE
// 2. 查询类型:findDescendants
// 3. SQL 刷新规则:[['result_contains']], [['match_where']]
// 4. 检查 result_contains:tablets 在当前结果中 ✅
// 5. 结果:执行 SQL 重新查询
// 6. 原因:删除节点改变了树形结构,需要重新计算后代关系

树形查询说明:

  • findDescendants/findAncestors: 查找后代/祖先节点
  • countDescendants/countAncestors: 统计后代/祖先数量
  • 为什么使用 SQL 刷新: 树形关系计算复杂(需要递归查询),JS 增量更新难以准确处理所有情况
  • 性能权衡: SQL 刷新开销相对较小,因为树形查询通常针对特定节点,结果集不会太大

解决方案

1. 通过 JS 计算替代部分 SQL 查询

系统会分析查询类型和变更事件,根据预定义的规则判断是否可以使用 JS 来计算,如果可以,那么就直接在已有结果上进行增量更新;否则重新执行 SQL 计算。

性能优化策略

  1. 依赖追踪优化:通过 #dep_entity_type_map 快速过滤无关变更
  2. 分块处理:使用 performChunk 避免阻塞主线程
  3. 结果缓存:使用 WeakSet 快速判断实体是否在结果中
  4. 懒执行:只有在有观察者时才执行查询
  5. 智能规则匹配:根据查询类型自动选择最优的刷新策略

2. 观察已知结果来做新的计算

通过复用其他查询的结果,我们可以避免重复的 SQL 查询,提高整体性能。

场景 1:同条件查询复用

我们正在查询 Todo 表的所有数据:

// 查询全部的 todo 数据
const todos$ = Todo.findAll({
where: { combinator: 'and', rules: [] }
});

todos$.subscribe(todos => {
console.log('todos', todos);
});

接着我们又查询全部 todo 总量:

// 查询总数
const count$ = Todo.count({
where: { combinator: 'and', rules: [] }
});

count$.subscribe(count => {
console.log('count', count);
});

优化方案:由于 where 条件相同,count$ 可以直接观察 todos$ 的结果长度,避免执行 SQL 查询。

// 内部实现伪代码
if (hasMatchingFindAllQuery(countQuery.where)) {
return findAllQuery$.pipe(map(results => results.length));
}

场景 2:子集查询复用

// 查询全部的 todo 数据
const allTodos$ = Todo.findAll({
where: { combinator: 'and', rules: [] }
});

// 查询已完成的 todo 数据
const completedTodos$ = Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: true }]
}
});

优化方案completedTodos$ 的查询条件是 allTodos$ 的子集,可以通过过滤 allTodos$ 的结果来实现。

// 内部实现伪代码
if (hasMatchingParentQuery(completedTodosQuery.where)) {
return parentQuery$.pipe(
map(results => results.filter(todo => todo.completed === true))
);
}

场景 3:分页查询复用

// 查询前 20 条
const page1$ = Todo.find({
where: { combinator: 'and', rules: [] },
limit: 20,
skip: 0
});

// 查询前 10 条
const page2$ = Todo.find({
where: { combinator: 'and', rules: [] },
limit: 10,
skip: 0
});

优化方案page2$page1$ 的子集,可以直接切片复用。

// 内部实现伪代码
if (hasMatchingLargerPageQuery(page2Query)) {
return largerPageQuery$.pipe(
map(results => results.slice(0, page2Query.limit))
);
}

实现策略

方案一:观察相同条件的查询来做计算,并且在该查询销毁时执行自己的查询

  • ✅ 实现简单,条件完全匹配即可
  • ✅ 性能提升明显
  • ⚠️ 只能处理完全相同的查询条件

方案二:观察能给我们答案的查询,例如小范围查询可以借用大范围查询的结果

  • ✅ 覆盖更多优化场景
  • ✅ 资源利用率更高
  • ⚠️ 需要复杂的条件匹配算法
  • ⚠️ 需要处理父查询销毁的情况

实际应用场景

场景 4:Todo 列表实时更新

// 订阅所有待办事项
const subscription = Todo.findAll({
where: { combinator: 'and', rules: [] },
orderBy: [{ field: 'createdAt', order: 'DESC' }]
}).subscribe(todos => {
console.log('当前 Todo 列表:', todos);
console.log('总数:', todos.length);
});

// 用户操作:创建新 Todo
const todo = new Todo({ title: '买菜', completed: false });
await todo.save();
// ✅ 控制台输出:新 Todo 被添加到列表顶部

// 用户操作:修改 Todo
todo.completed = true;
await todo.save();
// ✅ 控制台输出:Todo 的 completed 状态被更新

// 用户操作:删除 Todo
await todo.remove();
// ✅ 控制台输出:Todo 从列表中移除

// 清理:取消订阅
subscription.unsubscribe();

场景 5:多条件过滤实时更新

// 订阅未完成的待办事项
const subscription = Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
}
}).subscribe(activeTodos => {
console.log('未完成的 Todo:', activeTodos);
});

// 场景分析:
// 1. 创建新的未完成 Todo
const todo1 = new Todo({ title: '学习', completed: false });
await todo1.save();
// ✅ 控制台输出:ActiveTodoList 自动添加该 Todo(符合条件)

// 2. 创建新的已完成 Todo
const todo2 = new Todo({ title: '吃饭', completed: true });
await todo2.save();
// ✅ 控制台不输出新数据(不符合条件,被过滤)

// 3. 将未完成 Todo 标记为完成
todo1.completed = true;
await todo1.save();
// ✅ 控制台输出:该 Todo 从列表中移除(不再符合条件)

// 4. 将已完成 Todo 标记为未完成
todo2.completed = false;
await todo2.save();
// ✅ 控制台输出:该 Todo 被添加到列表(现在符合条件)

// 清理
subscription.unsubscribe();

场景 6:关联查询实时更新

// 定义实体关系
@Entity({
name: 'User',
properties: [
{ name: 'name', type: PropertyType.string }
]
})
class User extends EntityBase {
name: string;
}

@Entity({
name: 'Todo',
properties: [
{ name: 'title', type: PropertyType.string },
{ name: 'userId', type: PropertyType.string }
],
relations: [
{ name: 'user', type: () => User, relationType: 'many-to-one' }
]
})
class Todo extends EntityBase {
title: string;
userId: string;
user: User;
}

// 订阅某个用户的所有 Todo
const userId = 'user-123';
const subscription = Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'userId', operator: '=', value: userId }]
}
}).subscribe(todos => {
console.log(`用户 ${userId} 的 Todo 列表:`, todos);
});

// 场景分析:
// 1. 为该用户创建新 Todo
const todo = new Todo({ title: '开会', userId: 'user-123' });
await todo.save();
// ✅ 控制台输出:新 Todo 被添加(userId 匹配)

// 2. 为其他用户创建 Todo
const otherTodo = new Todo({ title: '睡觉', userId: 'user-456' });
await otherTodo.save();
// ✅ 控制台不输出(userId 不匹配)

// 3. 将 Todo 转移给其他用户
todo.userId = 'user-456';
await todo.save();
// ✅ 控制台输出:该 Todo 从列表中移除(userId 不再匹配)

// 清理
subscription.unsubscribe();

场景 7:复杂条件查询实时更新

// 订阅高优先级且未完成的任务
const subscription = Todo.findAll({
where: {
combinator: 'and',
rules: [
{ field: 'completed', operator: '=', value: false },
{ field: 'priority', operator: '>=', value: 8 },
{ field: 'dueDate', operator: '<=', value: new Date('2025-12-31') }
]
},
orderBy: [
{ field: 'priority', order: 'DESC' },
{ field: 'dueDate', order: 'ASC' }
]
}).subscribe(todos => {
console.log('高优先级未完成任务:', todos);
});

// 场景分析:
// 1. 创建符合所有条件的 Todo
const todo1 = new Todo({
title: '紧急任务',
completed: false,
priority: 9,
dueDate: new Date('2025-10-30')
});
await todo1.save();
// ✅ 控制台输出:添加到列表(所有条件都满足)

// 2. 修改优先级,不再满足条件
todo1.priority = 5;
await todo1.save();
// ✅ 控制台输出:从列表移除(priority < 8)

// 3. 再次提高优先级
todo1.priority = 10;
await todo1.save();
// ✅ 控制台输出:重新添加到列表(重新满足条件)

// 4. 标记为完成
todo1.completed = true;
await todo1.save();
// ✅ 控制台输出:从列表移除(completed = true)

// 清理
subscription.unsubscribe();

场景 8:统计查询实时更新

// 实时统计面板
let stats = { total: 0, completed: 0, active: 0 };

// 订阅总数
const totalSub = Todo.count({
where: { combinator: 'and', rules: [] }
}).subscribe(count => {
stats.total = count;
console.log('统计数据:', stats);
console.log('完成率:', stats.total > 0 ? ((stats.completed / stats.total) * 100).toFixed(1) + '%' : '0%');
});

// 订阅已完成数
const completedSub = Todo.count({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: true }]
}
}).subscribe(count => {
stats.completed = count;
console.log('统计数据:', stats);
});

// 订阅未完成数
const activeSub = Todo.count({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
}
}).subscribe(count => {
stats.active = count;
console.log('统计数据:', stats);
});

// 场景分析:
// 1. 创建新 Todo
const todo = new Todo({ title: '任务', completed: false });
await todo.save();
// ✅ 控制台输出:total +1, active +1

// 2. 完成 Todo
todo.completed = true;
await todo.save();
// ✅ 控制台输出:completed +1, active -1, total 不变

// 3. 删除 Todo
await todo.remove();
// ✅ 控制台输出:total -1, completed -1

// 清理
totalSub.unsubscribe();
completedSub.unsubscribe();
activeSub.unsubscribe();

// 优化提示:如果已有 findAll 查询,count 查询可以直接复用其结果的 length

场景 9:树形结构查询实时更新

// 定义树形实体
@TreeEntity({
name: 'Category',
properties: [
{ name: 'name', type: PropertyType.string }
]
})
class Category extends TreeAdjacencyListEntityBase {
name: string;
}

// 订阅某个分类的所有子孙分类
const categoryId = 'electronics-001';
const subscription = Category.findDescendants({
entityId: categoryId,
where: { combinator: 'and', rules: [] }
}).subscribe(descendants => {
console.log('子孙分类:', descendants.map(c => c.name));
});

// 场景分析:
// 假设树结构:电子产品 > 手机 > 智能手机
const electronics = await Category.findOne({
where: {
combinator: 'and',
rules: [{ field: 'name', operator: '=', value: '电子产品' }]
}
});

// 1. 在该分类下创建新子分类
const tablets = new Category({ name: '平板电脑' });
tablets.parent = electronics;
await tablets.save();
// ✅ 控制台输出:新分类被添加

// 2. 将子分类移动到其他父级
const phones = await Category.findOne({
where: {
combinator: 'and',
rules: [{ field: 'name', operator: '=', value: '手机' }]
}
});
tablets.parent = phones;
await tablets.save();
// ✅ 如果 phones 也是 electronics 的后代,tablets 仍在列表中
// ✅ 如果移到完全不同的树,tablets 从列表中移除

// 3. 删除子分类
await tablets.remove();
// ✅ 控制台输出:该分类从列表中移除

// 清理
subscription.unsubscribe();

场景 10:批量操作

// 订阅 Todo 列表
const subscription = Todo.findAll({
where: { combinator: 'and', rules: [] }
}).subscribe(todos => {
console.log('Todo 列表更新:', todos.map(t => t.title));
});

// 批量创建场景
const batchCreateTodos = async (titles: string[]) => {
const todos = titles.map(title => new Todo({ title, completed: false }));
// 使用 saveMany 批量保存
await db.entityManager.saveMany(todos);
};

// 执行批量创建
await batchCreateTodos(['任务1', '任务2', '任务3']);
// ✅ 控制台输出:批量保存后,一次性收到所有新 Todo
// ✅ 性能优化:批量操作合并为一次通知

// 批量删除场景
const todosToDelete = await Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: true }]
}
}).toPromise();

// 使用 removeMany 批量删除
await db.entityManager.removeMany(todosToDelete);
// ✅ 控制台输出:批量删除后,一次性更新列表

// 清理
subscription.unsubscribe();

性能对比

传统方案 vs 增量更新方案

操作场景传统方案(SQL 重查)增量更新方案(JS 计算)性能提升
创建单个实体遍历整表重新查询直接添加到结果集~10x
更新单个实体遍历整表重新查询更新单个对象或移除/添加~10x
删除单个实体遍历整表重新查询从结果集移除~10x
批量创建(100个实体)遍历整表 100 次批量添加到结果集~100x
10个查询订阅同时存在每个查询独立执行 SQL共享事件处理,增量更新~5-10x
1000+ 实体的表查询每次变更都需要遍历 1000+ 行只处理变更的实体~100-1000x

内存使用优化

技术说明优势
WeakSet使用 resultEntityIds 存储实体 ID不阻止垃圾回收
查询缓存复用相同查询条件共享同一个 QueryTask避免重复缓存
自动清理无观察者时自动销毁 QueryTask及时释放内存
依赖计数仅追踪有订阅的实体类型减少事件处理开销
分块处理使用 performChunk 处理大量查询任务避免阻塞主线程

核心实现机制

QueryManager 架构

/**
* QueryManager 核心数据结构
*/
class QueryManager<T> {
// 查询任务缓存:key = hash(查询类型 + 查询选项)
#query_task_map: Map<string, QueryTask<T>>;

// 实体类型依赖计数:key = EntityType, value = 依赖该类型的查询数量
#dep_entity_type_map: Map<EntityType, number>;
}

/**
* QueryTask 结构
*/
interface QueryTask<T> {
type: string; // 查询类型
options: QueryTaskOptions; // 查询选项
observerCount: number; // 当前观察者数量
refreshCount: number; // 刷新次数
result?: any; // 缓存的查询结果
resultEntityIds: WeakSet; // 结果实体 ID 集合(快速查找)
resultEntities: T[]; // 结果实体数组
relationEntityTypes: Set<EntityType>; // 关联的实体类型集合
observers: Set<Observer<any>>; // 观察者集合

// 控制方法
refresh: () => void; // 手动刷新
clean: () => void; // 清理资源
firstRunner: () => void; // 首次执行
next: (data: any) => void; // 发送新数据
error: (err: any) => void; // 发送错误

// 响应式流
refresh$: Observable<number>; // 刷新信号流
destroy$: Observable<void>; // 销毁信号流
result$: Observable<any>; // 结果流
}

查询任务生命周期

增量更新策略详解

CREATE 事件处理

/**
* CREATE 事件 - 所有查询类型都使用 JS 增量更新
* 根据查询类型采用不同的优化策略
*/
function query_merge_create_cache(task: QueryTask, entities: EntityEventData[]) {
// 第一步: 过滤符合 where 条件的实体
const match_data = entities.filter(e =>
isEntityMatchWhere(e.patch, task.options.where)
);

if (match_data.length === 0) return; // 无匹配实体

const new_entities = match_data.map(d => task.serialize(d));

// 第二步: 根据查询类型执行增量更新
switch (task.type) {
case 'findAll':
// 直接添加到结果集,有排序则重新排序
new_entities.forEach(entity => task.resultEntitySet.add(entity));
let result = Array.from(task.resultEntitySet.values());

if (task.options.orderBy?.length) {
result = calculateOrderBy(result, task.options.orderBy);
}

task.next(result);
break;

case 'find':
// 合并后重新排序和截取,只在结果变化时更新
const old_result = Array.from(task.resultEntitySet.values());
const combined = [...old_result, ...new_entities];

let sorted = combined;
if (task.options.orderBy?.length) {
sorted = calculateOrderBy(combined, task.options.orderBy);
}

// 截取前 limit 条
const new_result = sorted.slice(0, task.options.limit);

// 去重优化: 只在结果真正变化时更新
if (hasResultChanged(old_result, new_result)) {
task.resultEntitySet.clear();
new_result.forEach(e => task.resultEntitySet.add(e));
task.next(new_result);
}
break;

case 'findByCursor':
// 在游标范围内增量添加,不自动补充到 limit
// 与 find 的区别: 不强制截取到 limit,允许累积
// 详见源码实现...
break;

case 'findOne':
case 'findOneOrFail':
// 比较新实体与当前结果,按排序规则决定是否替换
const current = task.result;

if (!current) {
// 无当前结果,使用第一个新实体
task.next(new_entities[0]);
} else if (task.options.orderBy?.length) {
// 有排序: 比较新实体和当前结果
const sorted_candidates = calculateOrderBy(
[current, ...new_entities],
task.options.orderBy
);

// 如果新实体排在前面,替换
if (sorted_candidates[0] !== current) {
task.next(sorted_candidates[0]);
}
}
// 无排序: 保持第一个结果不变
break;

case 'count':
// 简单加法
const current_count = task.result || 0;
task.next(current_count + new_entities.length);
break;
}
}

UPDATE 事件处理

/**
* UPDATE 事件 - 最复杂的场景
* 需要识别三种状态: newly_matched, newly_unmatched, still_matched
*/
function query_merge_update_cache(task: QueryTask, entities: EntityEventData[]) {
const where = task.options.where;

// 分类实体
const match_now = entities.filter(e =>
isEntityMatchWhere(e.patch, where) // 更新后匹配
);
const match_before = entities.filter(e =>
isEntityMatchWhere(e.inversePatch, where) // 更新前匹配
);

// 三种状态
const newly_matched_ids = new Set<string>(); // 从不匹配 -> 匹配
const newly_unmatched_ids = new Set<string>(); // 从匹配 -> 不匹配
const still_matched_ids = new Set<string>(); // 仍然匹配

match_now.forEach(e => {
if (!match_before.some(b => b.id === e.id)) {
newly_matched_ids.add(e.id);
} else {
still_matched_ids.add(e.id);
}
});

match_before.forEach(e => {
if (!match_now.some(n => n.id === e.id)) {
newly_unmatched_ids.add(e.id);
}
});

// 根据查询类型处理
switch (task.type) {
case 'findAll':
// JS 完整更新: 移除 + 更新 + 添加
const old_result = Array.from(task.resultEntitySet.values());

// 1. 移除不再匹配的实体
const after_removal = old_result.filter(e =>
!newly_unmatched_ids.has(e.id)
);

// 2. 更新仍然匹配的实体
const updated = after_removal.map(e => {
if (still_matched_ids.has(e.id)) {
const update_data = match_now.find(d => d.id === e.id);
return update_data ? task.serialize(update_data) : e;
}
return e;
});

// 3. 添加新匹配的实体
const newly_matched = entities
.filter(d => newly_matched_ids.has(d.id))
.map(d => task.serialize(d));

let result = [...updated, ...newly_matched];

// 4. 重新排序
if (task.options.orderBy?.length) {
result = calculateOrderBy(result, task.options.orderBy);
}

task.resultEntitySet.clear();
result.forEach(e => task.resultEntitySet.add(e));
task.next(result);
break;

case 'find':
case 'findByCursor':
// SQL 刷新: 结果集受影响或有新匹配时
const affected = old_result.some(e => entities.some(d => d.id === e.id));
const has_new = newly_matched_ids.size > 0;

if (affected || has_new) {
task.refresh(); // 需要重新应用 limit/游标
}
break;

case 'findOne':
case 'findOneOrFail':
// 混合策略
const current = task.result;
const current_id = current?.id;

if (newly_unmatched_ids.has(current_id)) {
task.refresh(); // 当前结果不再匹配
} else if (newly_matched_ids.size > 0) {
task.refresh(); // 有新匹配,可能更优
} else if (still_matched_ids.has(current_id)) {
if (task.options.orderBy?.length) {
task.refresh(); // 有排序,需确认仍是第一个
} else {
// 无排序,只更新字段
const update_data = entities.find(d => d.id === current_id);
if (update_data) {
task.next(task.serialize(update_data));
}
}
}
break;

case 'count':
// JS 增减计数
const current_count = task.result || 0;
const added = newly_matched_ids.size;
const removed = newly_unmatched_ids.size;

// 去重优化: 只在计数真正变化时更新
if (added > 0 || removed > 0) {
const new_count = Math.max(0, current_count + added - removed);
task.next(new_count);
}
break;
}
}

REMOVE 事件处理

/**
* REMOVE 事件 - 按查询类型差异化处理
* findAll/findByCursor 使用 JS 移除, find/findOne 使用 SQL 刷新
*/
function query_merge_remove_cache(task: QueryTask, entities: EntityEventData[]) {
const removed_ids = new Set(entities.map(e => e.id));

switch (task.type) {
case 'findAll':
// JS 直接移除,有排序则重新排序
const old_result = Array.from(task.resultEntitySet.values());

const filtered = old_result.filter(e =>
!removed_ids.has(e.id)
);

if (filtered.length === old_result.length) {
return; // 无实体被删除
}

task.resultEntitySet.clear();
filtered.forEach(e => task.resultEntitySet.add(e));

let result = filtered;
if (task.options.orderBy?.length) {
result = calculateOrderBy(filtered, task.options.orderBy);
}

task.next(result);
break;

case 'find':
// SQL 刷新: 需要补充数据到 limit
const has_removed = old_result.some(e =>
removed_ids.has(e.id)
);

if (has_removed) {
task.refresh(); // 从 limit 之外补充新数据
}
break;

case 'findByCursor':
// JS 直接移除: 游标范围内减少,不需要补充
const old_cursor_result = Array.from(task.resultEntitySet.values());

const filtered_cursor = old_cursor_result.filter(e =>
!removed_ids.has(e.id)
);

if (filtered_cursor.length === old_cursor_result.length) {
return;
}

task.resultEntitySet.clear();
filtered_cursor.forEach(e => task.resultEntitySet.add(e));
task.next(filtered_cursor);
break;

case 'findOne':
case 'findOneOrFail':
// SQL 刷新: 当前结果被删除时获取新的第一条
const current = task.result;
if (!current) return;

const current_id = current.id;

if (removed_ids.has(current_id)) {
task.refresh(); // 获取新的第一条
}
break;

case 'count':
// JS 简单减法
const current_count = task.result || 0;
const removed_count = entities.length;
const new_count = Math.max(0, current_count - removed_count);
task.next(new_count);
break;
}
}

依赖追踪机制

/**
* 分析查询依赖的实体类型
*/
function query_entity_type_dependencies(
where: WhereClause,
baseEntityType: EntityType,
result: Set<EntityType>
): void {
result.add(baseEntityType);

// 遍历查询规则
for (const rule of where.rules) {
// 如果字段是关联字段,添加关联实体类型
const relation = getRelationMetadata(baseEntityType, rule.field);
if (relation) {
result.add(relation.targetEntityType);

// 递归处理嵌套关联
if (rule.where) {
query_entity_type_dependencies(
rule.where,
relation.targetEntityType,
result
);
}
}
}
}

最佳实践

1. 合理使用查询订阅

// ✅ 好的做法:记得取消订阅
const subscription = Todo.findAll({
where: { combinator: 'and', rules: [] }
}).subscribe(todos => {
console.log('Todo 列表:', todos);
});

// 在不需要时取消订阅
subscription.unsubscribe();

// ❌ 不好的做法:忘记取消订阅导致内存泄漏
Todo.findAll({
where: { combinator: 'and', rules: [] }
}).subscribe(todos => {
console.log('Todo 列表:', todos);
});
// 没有保存 subscription,无法取消订阅

2. 避免过度订阅

// ❌ 不好的做法:为每个 Todo 项单独订阅
const todoIds = ['id-1', 'id-2', 'id-3'];
todoIds.forEach(id => {
Todo.get(id).subscribe(todo => {
console.log('Todo:', todo);
});
});
// 3 个订阅 = 3 次 SQL 查询

// ✅ 好的做法:订阅一个包含所有 Todo 的查询
Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'id', operator: 'IN', value: ['id-1', 'id-2', 'id-3'] }]
}
}).subscribe(todos => {
console.log('所有 Todo:', todos);
});
// 1 个订阅 = 1 次 SQL 查询

3. 使用合适的查询类型

// ✅ 只需要数量时使用 count
const countSub = Todo.count({
where: { combinator: 'and', rules: [] }
}).subscribe(count => {
console.log('Todo 总数:', count);
});

// ❌ 不要用 findAll 再取 length
const todosSub = Todo.findAll({
where: { combinator: 'and', rules: [] }
}).pipe(
map(todos => todos.length) // 浪费资源:查询了完整数据却只用 length
).subscribe(count => {
console.log('Todo 总数:', count);
});

// ✅ 需要分页时使用 find
const pageSub = Todo.find({
where: { combinator: 'and', rules: [] },
limit: 20,
skip: 0
}).subscribe(page => {
console.log('第一页:', page);
});

// ❌ 不要用 findAll 再手动切片
const allSub = Todo.findAll({
where: { combinator: 'and', rules: [] }
}).pipe(
map(todos => todos.slice(0, 20)) // 低效:查询了全部数据却只用前 20 条
).subscribe(page => {
console.log('第一页:', page);
});

4. 优化复杂查询

// ✅ 使用索引字段作为查询条件
@Entity({
name: 'Todo',
properties: [
{ name: 'userId', type: PropertyType.string },
{ name: 'completed', type: PropertyType.boolean }
],
indexes: [
{ properties: ['userId'] },
{ properties: ['completed'] }
]
})
class Todo extends EntityBase {
userId: string;
completed: boolean;
}

// 查询时使用索引字段
Todo.findAll({
where: {
combinator: 'and',
rules: [
{ field: 'userId', operator: '=', value: 'user-123' },
{ field: 'completed', operator: '=', value: false }
]
}
}).subscribe(todos => {
console.log('用户的未完成任务:', todos);
});

// ❌ 避免在非索引字段上做复杂查询
@Entity({
name: 'Todo',
properties: [
{ name: 'description', type: PropertyType.string }
]
})
class Todo extends EntityBase {
description: string;
}

Todo.findAll({
where: {
combinator: 'and',
rules: [
{ field: 'description', operator: 'LIKE', value: '%keyword%' } // 全表扫描
]
}
}).subscribe(todos => {
console.log('搜索结果:', todos);
});

5. 批量操作使用 saveMany/removeMany

// ✅ 好的做法:使用 saveMany 批量保存
const subscription = Todo.findAll({
where: { combinator: 'and', rules: [] }
}).subscribe(todos => {
console.log('Todo 列表更新,当前数量:', todos.length);
});

const todos = items.map(item => new Todo(item));
await db.entityManager.saveMany(todos);
// 控制台只输出一次更新通知

// ❌ 不好的做法:逐个保存
for (const item of items) {
const todo = new Todo(item);
await todo.save();
}
// 控制台输出 N 次更新通知(N = items.length)

// ✅ 批量删除
const completedTodos = await Todo.findAll({
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: true }]
}
}).toPromise();

await db.entityManager.removeMany(completedTodos);
// 控制台只输出一次更新通知

// ❌ 不好的做法:逐个删除
for (const todo of completedTodos) {
await todo.remove();
}
// 控制台输出 N 次更新通知

6. 监控查询性能

const task = repository.queryManager.createTask({
type: 'findAll',
options: {
where: { combinator: 'and', rules: [] }
}
});

task.result$.subscribe(todos => {
console.log('查询刷新次数:', task.refreshCount);
console.log('观察者数量:', task.observerCount);
console.log('结果数量:', todos.length);
console.log('结果实体Map大小:', task.resultEntitySet.size);
});

// 性能分析技巧:
// 1. refreshCount 应该很低(大部分使用 JS 增量更新)
// 2. CREATE/REMOVE 事件 refreshCount 应该为 0(完全 JS 处理)
// 3. UPDATE 事件:findAll 的 refreshCount 为 0,find 可能触发 SQL
// 4. 如果 refreshCount 异常高,检查查询条件和数据变更模式

7. 处理错误

// ✅ 好的做法:处理查询错误
Todo.findAll({
where: { combinator: 'and', rules: [] }
}).subscribe({
next: todos => console.log('数据:', todos),
error: err => {
console.error('查询错误:', err);
// 可以显示错误提示给用户
}
});

// 使用 RxJS 操作符处理错误
import { catchError, of } from 'rxjs';

Todo.findAll({
where: { combinator: 'and', rules: [] }
}).pipe(
catchError(err => {
console.error('查询错误:', err);
return of([]); // 返回空数组作为默认值
})
).subscribe(todos => {
console.log('数据:', todos);
});

常见问题

Q1: 查询结果会自动更新吗?

是的,所有通过 Repository 创建的查询都是响应式的。当相关数据发生变更时,查询结果会自动更新。

Q2: 多个组件订阅相同查询会重复执行 SQL 吗?

不会。QueryManager 会缓存相同的查询任务,多个观察者会共享同一个查询结果。只有第一个订阅者会触发 SQL 查询。

Q3: 如何手动刷新查询?

// 创建查询任务
const task = repository.queryManager.createTask({
type: 'findAll',
options: {
where: { combinator: 'and', rules: [] }
}
});

// 订阅结果
task.result$.subscribe(todos => {
console.log('Todo 列表:', todos);
});

// 手动刷新查询
task.refresh();

// 或者使用 RxJS 操作符定时刷新
import { throttleTime } from 'rxjs';

task.refresh$.pipe(
throttleTime(1000) // 限流,避免频繁刷新
).subscribe(() => {
console.log('查询已刷新');
});

Q4: 查询缓存什么时候会被清理?

当最后一个观察者取消订阅时,QueryTask 会自动清理,释放内存。

Q5: 增量更新会失败吗?什么情况下会回退到 SQL 查询?

目前的实现策略:

智能规则匹配系统

  • ✅ 系统使用预定义的刷新规则自动判断是否可以增量更新
  • ✅ 对于 findAll 查询,优先使用 JS 增量更新(性能更好)
  • ✅ 对于 findfindByCursor 等分页查询,优先使用 SQL 刷新(保证准确性)
  • ✅ 根据事件类型(CREATE/UPDATE/REMOVE)和查询类型自动选择最优策略

规则匹配逻辑

// 以 UPDATE 事件 + findAll 查询为例
// JS 重算规则:[['match_where'], ['not_match_where', 'result_contains']]
// 含义:
// - 规则 1:实体更新后满足 where 条件 → JS 添加到结果
// - 规则 2:实体不满足 where 且在结果中 → JS 从结果移除

当前限制

  • ⚠️ 目前代码中临时使用 { refresh: true, recalculate: false }
  • ⚠️ 这意味着当前所有变更都会触发 SQL 刷新

当前实现状态

  • ✅ 规则匹配系统已完全实现并启用
  • ✅ CREATE 事件: 所有查询类型使用 JS 增量更新
  • ✅ UPDATE 事件: findAll 使用 JS, find/findByCursor 使用 SQL, findOne 混合策略
  • ✅ REMOVE 事件: findAll/findByCursor 使用 JS, find/findOne 使用 SQL
  • ✅ 通过测试: 62 个测试用例全部通过 (CREATE 20个, UPDATE 22个, REMOVE 18个)

Q6: 系统如何决定使用 JS 还是 SQL?

系统通过规则匹配系统自动决策:

  1. 每个查询类型针对每种事件类型都有预定义的规则
  2. 规则分为 refresh_rules(SQL 刷新) 和 recalculate_rules(JS 重算)
  3. 优先检查 JS 重算规则,如果匹配则使用 JS
  4. 否则检查 SQL 刷新规则,如果匹配则使用 SQL
  5. 都不匹配则不处理该事件

示例:

// UPDATE 事件 + findAll 查询
recalculate_rules = [
['match_where'], // 更新后匹配条件
['not_match_where', 'match_where_before'] // 更新前匹配但更新后不匹配
];

// 如果实体满足任一规则组,使用 JS 增量更新
// 否则不做处理(不会触发 SQL 刷新)

Q7: 如何调试查询优化效果?

// 查看活跃的查询任务
const queryManager = repository.queryManager;
console.log('活跃查询数:', queryManager['#query_task_map'].size);

// 监控特定查询的刷新次数和策略
const task = queryManager.createTask({
type: 'findAll',
options: { where: { combinator: 'and', rules: [] } }
});

task.result$.subscribe(() => {
console.log('查询刷新次数:', task.refreshCount);
console.log('观察者数量:', task.observerCount);
console.log('结果实体数:', task.resultEntitySet.size);
});

// 实测性能
console.time('create-and-update');
const todo = new Todo({ title: 'Test' });
await todo.save(); // CREATE 事件
todo.title = 'Updated';
await todo.save(); // UPDATE 事件
console.timeEnd('create-and-update');
// 输出: 查询刷新次数: 0 (全部使用 JS 增量更新)

未来优化方向

1. 智能回退机制

当增量更新无法准确处理时,自动回退到 SQL 查询:

// 计划中的实现
if (canUseIncrementalUpdate(task, event)) {
query_merge_update_cache(task, entities);
} else {
// 回退到 SQL 重查
task.refresh();
}

2. 查询结果共享优化

子查询复用父查询的结果:

// 大范围查询
const allTodos$ = Todo.findAll({ where: {} });

// 小范围查询可以复用
const completedTodos$ = Todo.findAll({
where: { rules: [{ field: 'completed', operator: '=', value: true }] }
});
// 内部实现:从 allTodos$ 的结果中 filter,而不是执行新的 SQL

3. 预测性缓存

根据访问模式预加载可能需要的数据:

// 用户查看了某个 Todo 列表
Todo.findAll({ where: { userId: 'user-123' } });

// 系统预测可能会查看 Todo 详情,预加载
Todo.get(recentlyViewedTodoIds);

4. 查询合并

合并多个相似查询为一个批量查询:

// 多个组件分别查询
Todo.get('id-1');
Todo.get('id-2');
Todo.get('id-3');

// 内部合并为一个查询
Todo.find({ where: { id: { IN: ['id-1', 'id-2', 'id-3'] } } });

5. 离线优先优化

在离线场景下,优先使用本地缓存,减少对适配器的依赖。

总结

响应式查询优化通过以下机制显著提升了性能:

  1. 增量更新:使用 JS 计算替代 SQL 重查
  2. 查询缓存:相同查询复用结果,避免重复执行
  3. 依赖追踪:精确过滤相关变更,减少无效处理
  4. 分块处理:避免阻塞主线程
  5. 自动清理:及时释放不再使用的资源

这些优化让 RxDB 在 Local-First 场景下具备了出色的实时响应能力和性能表现。