响应式查询优化
背景
如果我们的查询是可观察的,那么每当数据发生变化时,查询就需要重新计算一次;随着观察的数据越来越多计算也会随来越多,我们需要对查询进行优化。
核心问题
在响应式数据库中,每次数据变更都可能触发多个查询的重新计算,这会带来以下问题:
- 性能开销大: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会记录所有的数据变化并发送通知 - 每个在使用的查询,都会记录完整的信息,原始查询语句,查询结果,查询状态,观察者数量等等信息
- 查询管理器维护了实体类型依赖映射表,用于快速判断哪些变更会影响查询
实体事件类型
响应式查询优化主要监听以下实体变更事件:
| 事件类型 | 常量 | 说明 | 触发时机 |
|---|---|---|---|
| EntityNewEvent | ENTITY_NEW_EVENT | 实体初始化 | 通过 new 创建实例时 |
| EntityCreateEvent | ENTITY_CREATE_EVENT | 实体创建成功 | 首次 save() 持久化到数据库 |
| EntityUpdateEvent | ENTITY_UPDATE_EVENT | 实体更新成功 | 修改后 save() 更新到数据库 |
| EntityRemoveEvent | ENTITY_REMOVE_EVENT | 实体删除成功 | 调用 remove() 从数据库删除 |
注意:EntityNewEvent 不会触发查询更新,因为此时实体尚未持久化到数据库。只有 CREATE/UPDATE/REMOVE 事件会触发查询的增量更新。
查询方法
| 方法 | 参数 | 说明 | 适用场景 |
|---|---|---|---|
| get | id | 根据 id 获取实体 | 精确查找单个实体 |
| findOne | where, orderBy | 查找一个实体 | 条件查询,允许不存在 |
| findOneOrFail | where, orderBy | 查找一个实体,查不到就抛出错误 | 条件查询,必须存在 |
| find | where, orderBy, limit, skip | 查找多个实体(带分页) | 分页列表查询 |
| findAll | where, orderBy | 查找所有实体(不分页) | 获取全量数据 |
| findByCursor | where, orderBy, cursor, take | 使用游标分页查找实体 | 高效分页,支持无限滚动 |
| count | where | 统计实体数量 | 获取符合条件的数据总数 |
| countAncestors | entityId, where | 统计祖先节点数量 | 树形结构,统计父级层级 |
| countDescendants | entityId, where | 统计后代节点数量 | 树形结构,统计子级层级 |
| findAncestors | entityId, where, orderBy | 查找祖先节点 | 树形结构,获取所有父级节点 |
| findDescendants | entityId, 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在游标范围内累积结果,不会强制截取到 limitfindOne/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,排序变化或新匹配时用 SQLcount只在计数真正变化时更新(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 刷新以补充数据到 limitfindOne/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 计算。
性能优化策略
- 依赖追踪优化:通过
#dep_entity_type_map快速过滤无关变更 - 分块处理:使用
performChunk避免阻塞主线程 - 结果缓存:使用
WeakSet快速判断实体是否在结果中 - 懒执行:只有在有观察者时才执行查询
- 智能规则匹配:根据查询类型自动选择最优的刷新策略
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 增量更新(性能更好) - ✅ 对于
find、findByCursor等分页查询,优先使用 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?
系统通过规则匹配系统自动决策:
- 每个查询类型针对每种事件类型都有预定义的规则
- 规则分为
refresh_rules(SQL 刷新) 和recalculate_rules(JS 重算) - 优先检查 JS 重算规则,如果匹配则使用 JS
- 否则检查 SQL 刷新规则,如果匹配则使用 SQL
- 都不匹配则不处理该事件
示例:
// 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. 离线优先优化
在离线场景下,优先使用本地缓存,减少对适配器的依赖。
总结
响应式查询优化通过以下机制显著提升了性能:
- 增量更新:使用 JS 计算替代 SQL 重查
- 查询缓存:相同查询复用结果,避免重复执行
- 依赖追踪:精确过滤相关变更,减少无效处理
- 分块处理:避免阻塞主线程
- 自动清理:及时释放不再使用的资源
这些优化让 RxDB 在 Local-First 场景下具备了出色的实时响应能力和性能表现。