- 数据库
- NoSQL
- 嵌入式数据库
- 实时数据库
【免费下载链接】rxdb
The local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/
RxDB 是一个运行在多种 JavaScript 运行时上的本地优先(local-first)数据库,它并不提供传统关系型数据库中的 JOIN 操作。本文围绕 population.md 系统讲解如何在 RxDB 中通过 Schema 的ref关键字定义跨集合引用,并使用populate()方法与_后缀 getter 快速取回被引用文档,帮助你在保持无锁、离线优先架构的同时实现一对一、一对多乃至嵌套的关系建模。读完本文,你将掌握 RxDB 关系字段的完整定义方式、两种填充取值技巧,以及底层实现与常见错误码的排查方法。
为什么需要 Population:没有 Join 的引用方案
RxDB 的设计哲学中不包含 JOIN 操作——因为 JOIN 会破坏本地优先数据库的简单性与可复制性。但业务中我们依然经常需要“一个文档引用另一个文档”的场景,比如一篇文章引用它的作者、一个用户引用他的好友列表。
为此 RxDB 提供了population(填充)机制:
- 在 RxSchema 中通过
ref关键字声明字段指向某个集合; - 之后即可通过
populate()方法或带下划线后缀_的 getter,拿到被引用的 RxDocument 实例本身。
你可以把引用关系建立在同一集合内(如“人类”引用另一个“人类”),也可以建立在同一数据库下的不同集合之间(如human集合引用human2集合)。这一用法与 mongoose 的populate概念一致,但对本地优先数据库而言,它不需要任何服务器端连接,全部在本地完成解析。
Schema 中使用 ref 定义关系
ref关键字位于字段的 Schema 描述中,其值是被引用集合的名称字符串。引用字段的取值必须是被引用文档的主键(primaryKey)值,因此ref字段的类型必须是string,或['string', 'null'](允许为空)。
下面是一个一对一引用的示例:每个人类文档通过bestFriend字段引用human集合中的另一个文档。
export const refHuman = { title: 'human related to other human', version: 0, primaryKey: 'name', properties: { name: { type: 'string', maxLength: 100 }, bestFriend: { ref: 'human', // 指向名为 human 的集合 // ref 字段的值必须是 string // 或 ['string', 'null'] // (即外键 RxDocument 的主键值) type: 'string' } } };若要表达一对多关系,只需把字段类型声明为字符串数组即可:
export const schemaWithOneToManyReference = { version: 0, primaryKey: 'name', type: 'object', properties: { name: { type: 'string', maxLength: 100 }, friends: { type: 'array', ref: 'human', // ref 声明在数组字段上 items: { type: 'string' // 数组元素为被引用文档的主键 } } } };关于ref的合法性,从仓库测试用例 population.test.ts 中可以确认以下事实:
- 允许将主键本身作为引用字段(历史上曾禁止,见 issue #2747 相关注释),例如
primaryKey: 'bestFriend'且该字段带ref是合法的; - 嵌套对象内部可以声明 ref,如
foo.bestFriend; - 数组字段的
ref既可以声明在数组字段本身上,也可以声明在items上,两种写法都受支持; ref字段的类型必须是字符串,若省略type只写ref,Schema 创建时会直接抛出错误(对应“ref-type is no string”的负向测试)。
用 populate() 方法获取被引用文档
基本用法
populate()是 RxDocument 实例上的方法,接收字段路径作为参数,返回一个 Promise,解析为被引用的 RxDocument;若引用值不存在或为空则解析为null。
await humansCollection.insert({ name: 'Alice', bestFriend: 'Carol' }); await humansCollection.insert({ name: 'Bob', bestFriend: 'Alice' }); const doc = await humansCollection.findOne('Bob').exec(); const bestFriend = await doc.populate('bestFriend'); console.dir(bestFriend); //> RxDocument[Alice]populate()同样支持点号路径填充嵌套字段,例如doc.populate('foo.bestFriend')。
用 _ 后缀 getter 直接取回
除了方法调用,RxDB 还提供了一个更简洁的写法:在字段名后面加下划线后缀_,即可像访问普通属性一样拿到填充结果。该 getter 同样返回 Promise,并同样支持嵌套对象上的_访问。
await humansCollection.insert({ name: 'Alice', bestFriend: 'Carol' }); await humansCollection.insert({ name: 'Bob', bestFriend: 'Alice' }); const doc = await humansCollection.findOne('Bob').exec(); const bestFriend = await doc.bestFriend_; // 注意字段名末尾的下划线 `_` console.dir(bestFriend); //> RxDocument[Alice]嵌套引用示例
当引用藏在嵌套对象中时,_后缀依然有效:
const myCollection = await myDatabase.addCollections({ human: { schema: { version: 0, type: 'object', properties: { name: { type: 'string' }, family: { type: 'object', properties: { mother: { type: 'string', ref: 'human' } } } } } } }); /** * 假设 myDocument 是该集合中的一份文档 */ const mother = await myDocument.family.mother_; console.dir(mother); //> RxDocument数组引用示例
当填充数组字段时,返回结果是一个RxDocument 数组,且顺序与引用主键数组的顺序完全一致:
const myCollection = await myDatabase.addCollections({ human: { schema: { version: 0, type: 'object', properties: { name: { type: 'string' }, friends: { type: 'array', ref: 'human', items: { type: 'string' } } } } } }); //[在此处插入其他 humans 文档] await myCollection.insert({ name: 'Alice', friends: [ 'Bob', 'Carol', 'Dave' ] }); const doc = await humansCollection.findOne('Alice').exec(); const friends = await doc.friends_; console.dir(friends); //> Array.<RxDocument>源码解读:populate() 的底层实现
populate()的核心实现位于 src/rx-document.ts。理解它的执行顺序有助于你排查问题:
- 定位 Schema 路径:通过
getSchemaByObjectPath()从集合的 JSON Schema 中取出目标字段的子 Schema;若路径在 Schema 中不存在,直接抛出DOC5错误。 - 提取 ref 集合名:优先读取字段自身的
ref;若字段是数组且自身没有ref,则回退读取items.ref;若两者都不存在,抛出DOC6错误。 - 校验目标集合:从
this.collection.database.collections[ref]中查找被引用集合,若未创建则抛出DOC7错误。 - 取值与短路:读取当前字段的值,若值为空则返回
null(不会去查数据库)。 - 分发查询:
- 数组字段:调用
refCollection.findByIds(value).exec(),再按原始主键数组的顺序重新排列结果,保证填充顺序与引用顺序一致; - 单值字段:调用
refCollection.findOne(value).exec()取回单个文档。
- 数组字段:调用
特别值得注意的是源码中的“先校验、后取值”策略:即使在字段值为空(falsy)的情况下,非法路径和非 ref 字段也会被如实暴露为错误,而不是被静默吞掉,这避免了手写路径时的拼写错误被悄然忽略。
_ 后缀 getter 的 Proxy 机制
_后缀 getter 之所以能工作,是因为 RxDB 对文档嵌套对象使用 Proxy 包装(见 src/rx-document.ts 的get拦截逻辑)。当访问的属性名以_结尾时,Proxy 会截掉下划线并调用doc.populate();若以$结尾则返回可观察的流(observable),普通属性则直接返回原始值。也就是说doc.bestFriend_与doc.populate('bestFriend')在语义上是等价的。
错误码速查
与 population 相关的错误码定义在 error-messages.ts,dev-mode 插件开启后会在控制台给出更友好的说明:
| 错误码 | 含义 | 修复建议 |
|---|---|---|
DOC5 | populate()的路径在 Schema 中不存在 | 检查字段名与 Schema 定义 |
DOC6 | 该字段存在但未声明ref | 为字段添加ref属性 |
DOC7 | Schema 中引用的集合不存在于当前数据库 | 创建被引用的集合 |
边界条件与最佳实践
结合仓库中的单元测试,以下行为值得注意:
- 跨集合引用:
ref可以指向任何同数据库下已创建的集合。测试 population.test.ts 展示了human引用human2且主键作为 ref 值的场景,填充结果会来自正确的目标集合。 multiInstance: false下同样可用:历史上 population 在关闭多实例模式时存在缺陷(issue #222),当前版本已修复,测试覆盖了该场景。- 数组顺序保证:当两份文档以不同顺序引用同一组主键时,填充结果各自保持自己的原始顺序(相关回归测试见 population.test.ts)。注意,这个行为依赖
populate()内部的按序重组逻辑,不要依赖findByIds的 Map 迭代顺序。 - 空值安全:引用字段未赋值或值不存在时,
populate()返回null,可放心在逻辑中做空值判断。 - Schema 测试工具:仓库的 schemas.ts 提供了带
ref的refHuman标准 Schema,humans-collection.ts 提供了createRelated/createRelatedNested辅助函数,可供你在自己的测试中参考如何构造带关系的集合。
延伸阅读
- RxDocument:掌握文档实例的完整 API
- RxCollection:了解集合的创建与查询方式
- RxSchema:深入学习 Schema 定义规则
- transactions-conflicts-revisions.md:理解本地优先场景下多文档一致性的取舍,这也是 RxDB 不提供 JOIN 的深层原因
- 数据库
- NoSQL
- 嵌入式数据库
- 实时数据库
【免费下载链接】rxdb
The local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/
相关推荐
在 Flutter 应用中使用 RxDB 作为本地优先数据库的完整指南
在 Flutter 应用中使用 RxDB 作为本地优先数据库的完整指南 在 Flutter 移动应用开发中,选择合适的数据库直接影响应用的性能、可扩展性与用户体
数据库NoSQL嵌入式数据库实时数据库暗黑破坏神2存档编辑器:可视化修改工具彻底革新游戏体验
暗黑破坏神2存档编辑器:可视化修改工具彻底革新游戏体验 你是否曾因繁琐的十六进制编辑而放弃修改暗黑2存档?是否梦想过像游戏设计师那样自由定制角色属性、装备和游戏
数据库NoSQL嵌入式数据库实时数据库F3D:如何用极简工具实现专业级3D模型预览的完整指南
F3D:如何用极简工具实现专业级3D模型预览的完整指南 你是否曾为复杂的3D软件安装包而烦恼?是否在寻找一款能快速预览各种格式3D模型的轻量级工具?今天我将为你
数据库NoSQL嵌入式数据库实时数据库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考