ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RxDB RxSchema 完全指南:用 JSON Schema 定义集合结构、主键、索引与加密

RxDB RxSchema 完全指南:用 JSON Schema 定义集合结构、主键、索引与加密 RxDB RxSchema 完全指南用 JSON Schema 定义集合结构、主键、索引与加密【免费下载链接】rxdbThe local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/项目地址: https://gitcode.com/gh_mirrors/rx/rxdbRxSchema 是 RxDB 数据模型的根基它为每个集合定义文档结构决定哪个字段充当主键、哪些字段建立索引、哪些字段需要加密。本指南基于官方文档 rx-schema.md 并结合仓库源码带你从零掌握 Schema 的定义语法、全部约束规则与实战注意事项读完即可为自己的集合设计出结构严谨、可加密、可索引、可迁移的 Schema。RxSchema 是什么在 RxDB 中每个集合collection都有且只有一个属于自己的 Schema。Schema 负责描述该集合中文档的结构并承担以下几项关键职责确定哪个字段用作主键primary key用于唯一标识单条文档确定哪些字段用作二级索引secondary indexes加速查询确定哪些字段需要加密存储在数据写入数据库之前校验文档是否符合结构约束。RxDB 的 Schema 基于JSON Schema标准定义这一点与许多你熟悉的项目一致。RxDB 在标准 JSON Schema 之上扩展了一些自有字段如final、ref、keyCompression、encrypted等这些扩展在类型定义文件 src/types/rx-schema.d.ts 中有完整声明。一个完整的 Schema 示例下面这个示例定义一个 hero英雄集合覆盖了 Schema 的绝大部分核心特性version为 0name属性是primaryKey唯一的、带索引的、必填的string字段可用于精确定位单条文档color字段对每条文档都是必填的healthpoints字段必须是 0 到 100 之间的数字secret字段存储加密值birthyear字段是final的必填且不可修改skills属性必须是对象数组每个对象含name和damage属性每位英雄最多 5 个技能允许添加附件attachments并加密存储。{ title: hero schema, version: 0, description: describes a simple hero, primaryKey: name, type: object, properties: { name: { type: string, maxLength: 100 // - the primary key must have set maxLength }, color: { type: string }, healthpoints: { type: number, minimum: 0, maximum: 100 }, secret: { type: string }, birthyear: { type: number, final: true, minimum: 1900, maximum: 2050 }, skills: { type: array, maxItems: 5, uniqueItems: true, items: { type: object, properties: { name: { type: string }, damage: { type: number } } } } }, required: [ name, color ], encrypted: [secret], attachments: { encrypted: true } }注意name字段必须设置maxLength——这是 RxDB 对主键的硬性要求原因见下文primaryKey一节。用 Schema 创建集合定义好 Schema 之后把它传给addCollections()即可创建集合await myDatabase.addCollections({ heroes: { schema: myHeroSchema } }); console.dir(myDatabase.heroes.name); // heroes从源码看传入的 Schema 会经过 createRxSchema() 的处理先运行preCreateRxSchema插件钩子然后调用fillWithDefaultSettings()补齐默认设置、normalizeRxJsonSchema()做规范化最后实例化RxSchema类。也就是说你写的精简 Schema会在底层被自动补全成完整的内部结构见下文内部元字段。versionversion字段是一个数字从0开始。当version大于 0 时使用该 Schema 创建集合必须提供migrationStrategies迁移策略否则无法创建集合。这保证了已存储的旧版本数据可以被正确迁移到新结构。迁移的完整指南见 migration-schema.md。在源码中getPreviousVersions() 会根据当前版本号计算出所有需要迁移的前序版本数组例如version: 3会得到[0, 1, 2]每个版本都需要一条对应的迁移策略。primaryKeyprimaryKey字段存放的是整个集合主键所对应的属性名。主键的值必须是string类型并且唯一、final不可修改、必填。主键有三个硬性约束值得特别注意必须设置maxLength在 RxSchema 构造函数 中如果主键字段没有maxLength会直接抛出SC39错误。原因是 RxDB 需要知道主键字段字符串表示的最大长度以便在多种RxStorage实现中构造自定义索引。主键自动成为 final 字段getFinalFields() 会把主键以及复合主键的组成字段自动加入 final 字段列表所以不需要手动给主键加final: true。主键会自动加入 required在 fillWithDefaultSettings() 中主键路径会被强制追加到required数组。复合主键composite primary key如果单个字段不足以唯一定位文档可以定义由多个属性组合而成的复合主键。RxDB 会把多个字段的值用分隔符拼接成一个字符串存储到key指定的字段中const mySchema { keyCompression: true, // set this to true, to enable the keyCompression version: 0, title: human schema with composite primary, primaryKey: { // where should the composed string be stored key: id, // fields that will be used to create the composed key fields: [ firstName, lastName ], // separator which is used to concat the fields values. separator: | }, type: object, properties: { id: { type: string, maxLength: 100 // - the primary key must have set maxLength }, firstName: { type: string }, lastName: { type: string } }, required: [ id, firstName, lastName ] };插入文档时不需要也不应该手动设置id——RxDB 会自动用firstName和lastName拼接生成。查找文档时可以用组成字段反推出主键字符串再执行查询// inserting with composite primary await myRxCollection.insert({ // id, - do not set the id, it will be filled by RxDB firstName: foo, lastName: bar }); // find by composite primary const id myRxCollection.schema.getPrimaryOfDocumentData({ firstName: foo, lastName: bar }); const myRxDocument await myRxCollection.findOne(id).exec();底层实现见 getComposedPrimaryKeyOfDocumentData()它把fields中每个字段的值取出并用separator连接如果组成字段缺失会抛出DOC18错误。而 fillPrimaryKey() 负责在写入时自动填充主键如果文档中已存在一个与计算结果不一致的id则会抛出DOC19错误防止主键被错误篡改。Indexes索引RxDB 支持在 Schema 层面定义二级索引用于加速查询。索引有以下几个规则索引只允许定义在string、integer、number类型的字段上部分RxStorage实现还允许boolean字段作为索引。根据字段类型不同必须设置相应的元属性string字段必须设置maxLengthnumber字段必须设置minimum、maximum和multipleOf。这是因为 RxDB 需要知道字段字符串表示的最大长度才能在多种存储实现中构造自定义索引。这些检查在 dev-mode 的 check-schema.ts 中有完整实现——例如字符串索引的maxLength超过 2048 会被直接拒绝。性能提示被索引字段以及主键的maxLength设得过大会在许多存储上显著拖慢性能并增大存储占用。因此maxLength只要设置到应用严格需要的程度即可不要贪大。主键自动追加RxDB 会自动把primaryKey追加到所有索引末尾以保证查询结果的确定性排序。你不需要也不应该手动把主键加进任何索引。:::note RxDB 总是会把primaryKey追加到所有索引上以保证查询结果有确定性的排序顺序。因此你不需要在任何索引中额外添加primaryKey。 :::从源码看这条规则在 fillWithDefaultSettings() 中实现每个索引数组如果不包含主键就追加主键并在索引头部插入_deleted字段这样 RxDB 内部查询时可以直接过滤已删除文档。如果 Schema 一个索引都没定义RxDB 也会自动创建默认索引[_deleted, primaryPath]保证基本的查询能力见 getDefaultIndex()。索引示例const schemaWithIndexes { version: 0, title: human schema with indexes, keyCompression: true, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 // - the primary key must have set maxLength }, firstName: { type: string, // string-fields used as an index, // must have set maxLength. maxLength: 100 }, lastName: { type: string }, active: { type: boolean }, familyName: { type: string }, balance: { type: number, // number fields used in an index, must set // minimum, maximum and multipleOf minimum: 0, maximum: 100000, multipleOf: 0.01 }, creditCards: { type: array, items: { type: object, properties: { cvc: { type: number } } } } }, required: [ id, active // - boolean fields that are used in an index must be required. ], indexes: [ firstName, // - this will create a simple index for the firstName field // - compound-index for these two fields [active, firstName], active ] };注意本例中的两个细节用作索引的firstName是 string 类型所以设置了maxLength: 100balance是 number 类型所以同时设置了minimum、maximum、multipleOf。另外active这个 boolean 字段被用作索引它必须是required的。indexes数组中单个字符串表示单字段索引数组表示复合索引compound index。对应的测试用例见 test/unit/rx-schema.test.ts其中覆盖了单索引、复合索引、嵌套字段子索引如[other.age]以及在属性对象内部定义 index 会报错 SC26等负例场景。internalIndexes在服务端使用 RxDB 时你可能希望用internalIndexes来加速内部查询。internalIndexes与普通indexes的区别在于它不会被自动加上_deleted前缀专门用于优化 RxDB 内部的元数据查询。详细说明见 rx-server.md 中的server-only indexes一节。从源码看internalIndexes会在 fillWithDefaultSettings() 中被合并进最终的索引列表。attachments附件如果集合需要使用附件附件加密功能必须在 Schema 中添加attachments属性attachments: { encrypted: true }设置encrypted: true后附件会加密存储。附件的读写、迁移等完整用法见 rx-attachment.md。default默认值默认值只能定义在一级字段top-level fields上。插入文档时未显式设置的字段会自动填充为默认值const schemaWithDefaultAge { version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 // - the primary key must have set maxLength }, firstName: { type: string }, lastName: { type: string }, age: { type: integer, default: 20 // - default will be used } }, required: [id] };上面的 Schema 中age的默认值是20插入时不传age文档会自动带上age: 20。在类型定义 src/types/rx-schema.d.ts 中明确说明default只允许出现在顶层字段TopLevelProperty上不允许用于嵌套字段。默认值的填充逻辑见 fillObjectWithDefaults()只有当字段值为undefined时才填充如果默认值本身是对象或数组会进行浅拷贝slice()/ 展开避免多个文档共享同一个可变引用。RxSchema类的defaultValuesgetter 会扫描所有带default的属性并做缓存src/rx-schema.ts。final不可变字段给字段加上final: true可以确保该字段在文档插入后永远不能被修改。final 字段自动视为必填。final 字段的额外特性final 字段不能被观察observe因为它们永远不会变化观察没有意义防止误改final 字段可以保证任何人都不会意外修改关键数据性能收益启用eventReduce算法时final 字段的存在会带来一些性能优化因为事件归约时无需追踪这些字段的变化。const schemaWithFinalAge { version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 // - the primary key must have set maxLength }, firstName: { type: string }, lastName: { type: string }, age: { type: integer, final: true } }, required: [id] };在写入链路中final 字段的不可变性由 RxSchema.validateChange() 强制保证任何对 final 字段的修改dataBefore[field] ! dataAfter[field]都会抛出DOC9错误。而 getFinalFields() 除了收集所有带final: true的属性还会自动把主键和复合主键的组成字段也归入 final——因为主键本质上就应该是不可变的。不允许的属性Schema 的约束边界Schema 不仅用于写入前的数据校验还被用来做字段级 getter 映射、观察observe、关联填充population、key 压缩等内部机制。因此并非所有符合 json-schema.org 规范的 Schema 都能被 RxDB 接受。有两类主要限制1. 字段命名规则字段名必须匹配正则^a-zA-Z?$即必须以字母开头只能包含字母、数字、下划线且不能以下划线结尾。同时additionalProperties总是会被强制设为false。好在你不必担心踩坑——传入非法 Schema 时 RxDB 会立即抛出错误。这些校验在 checkFieldNameRegex() 中实现唯一的例外是_deletedRxDB 内部字段和_id为了兼容 CouchDB 等后端而允许作为主键。2. 不能与 RxDocument 类属性冲突以下RxDocument的类属性不能作为顶层字段名否则访问文档属性时会与类方法发生冲突[ collection, _data, _propertyCache, isInstanceOfRxDocument, primaryPath, primary, revision, deleted$, deleted$$, deleted, getLatest, $, $$, get$, get$$, populate, get, toJSON, toMutableJSON, update, incrementalUpdate, updateCRDT, putAttachment, putAttachmentBase64, getAttachment, allAttachments, allAttachments$, modify, incrementalModify, patch, incrementalPatch, _saveData, remove, incrementalRemove, close, deleted, synced ]这一限制的根源可以在 getDocumentPrototype() 中看到RxDB 会依据 Schema 为每个文档在原型上定义字段 getter以及字段名$、字段名$$、字段名_等派生 getter如果字段名与文档类已有属性冲突就会破坏文档对象的行为。内部元字段顺带说明一个常常让初学者困惑的现象你写的 Schema 里明明没有_deleted、_rev、_meta、_attachments但保存的文档中却会出现这些下划线开头的字段。它们是 RxDB 自动注入的内部元字段见 fillWithDefaultSettings()_rev版本号字符串、_attachments附件对象、_deleted删除标记、_meta元数据内含lwt最后写入时间并且这些字段都会被自动加入required。因此你的业务字段不要以下划线开头以免与内部机制冲突。Schema 的hash也是基于完整 JSON 序列化计算得出的src/rx-schema.ts用于在多实例间检测 Schema 是否一致。用 TypeScript 从 Schema 推导文档类型RxDB 官方推荐的 TypeScript 做法是把 Schema 写成as const字面量再通过toTypedRxJsonSchema和ExtractDocumentTypeFromTypedRxJsonSchema自动推导出文档类型src/rx-schema.tsimport { toTypedRxJsonSchema, ExtractDocumentTypeFromTypedRxJsonSchema, RxJsonSchema } from rxdb; const todoSchemaLiteral { title: todo schema, version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 }, title: { type: string }, completed: { type: boolean }, createdAt: { type: string, format: date-time }, updatedAt: { type: string, format: date-time } }, required: [id, title, completed, createdAt, updatedAt], indexes: [updatedAt, [completed, updatedAt]] } as const; const schemaTyped toTypedRxJsonSchema(todoSchemaLiteral); export type TodoDocType ExtractDocumentTypeFromTypedRxJsonSchematypeof schemaTyped; export const todoSchema: RxJsonSchemaTodoDocType todoSchemaLiteral;这样 Schema 与 TypeScript 类型就保持了单一事实来源任何字段改动都会同步反映到类型系统中。类型定义文件 src/types/rx-schema.d.ts 中还给出了一些推荐的 Schema 设计规则集合名用复数、每条文档尽量包含createdAt/updatedAt、嵌套对象不超过 3 层、数组必须声明items子 Schema、避免type: [string, number]多类型、尽量不存null而是用非 required 字段留undefined等。常见问题 FAQ如何存储 DateRxDB 的文档中只能存储纯 JSON 数据不能直接存储 JavaScript 的new Date()实例。原因有二性能考量以及Date是可变对象——任何时刻的修改都可能引发难以排查的问题。正确的做法是定义一个带format: date-time的字符串字段{ type: string, format: date-time }存储时先把Date对象转换成字符串Date.toISOString()。由于date-time格式字符串是可排序的你可以在该字段上执行任何查询操作甚至可以把它用作索引。需要提醒的是format只有在启用了 schema 校验插件如 validate-ajv 或 validate-z-schema时才会被强制校验相关说明见 src/types/rx-schema.d.ts。如何指定 nullable可空字段JSON Schema 中通过多类型数组可以让字段可空{ type: [string, null] }使用[string, null]这类可空类型时建议总是把该字段加入required数组。因为如果可空字段不是 required它会出现三种状态字符串值、null、undefined未设置。三种状态比两种状态更让你的代码难以推理。不过在 RxDB 中更推荐的做法是完全不存null把字段定义为非 required没有值时就保持undefined未设置。不在required数组中的字段可以从文档中省略。这种方式与 RxDB 内部处理机制配合得更好数据也更干净{ version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 }, nickname: { type: string } }, required: [id] // nickname is not required, so it can be left undefined (not set) }如何存储 schemaless无结构数据RxDB 设计上要求每个集合必须有 Schema因此无法创建顶层字段完全未知的无结构集合——RxDB 必须事先知道文档的所有顶层字段才能执行校验、索引创建以及其他内部优化。但有一个变通方案在子字段层面存储任意结构的数据。做法是在 Schema 中定义一个type: object的属性这个对象是开放的内部可以放任何 JSON 数据{ version: 0, primaryKey: id, type: object, properties: { id: { type: string, maxLength: 100 }, myDynamicData: { type: object // Here you can store any JSON data // because its an open object. } }, required: [id] }为什么 RxDB 自动设置 additionalProperties: falseRxDB 会在顶层自动设置additionalProperties: false确保所有顶层字段事先已知。这一设计有三个实际收益避免与 RxDocument 类属性冲突RxDB 文档在顶层有内置的类方法如.toJSON、.save。禁止未知顶层属性可以避免意外地与这些内置方法重名完整冲突列表见上文不允许的属性一节。避免与用户自定义 ORM 方法冲突开发者可以为文档添加自定义 ORM 方法。如果顶层属性不受限制某个属性名可能恰好与自定义方法名冲突导致难以预期的行为。改善 TypeScript 类型如果 RxDB 不知道所有顶层字段文档类型将退化为any。这意味着像myDocument.toJOSN()这样的拼写错误只能在运行时暴露而无法在编译期发现。禁止未知属性后TypeScript 能提供严格的类型检查尽早拦截错误。无法修改集合的 Schema当你修改集合的 Schema 时有时会遇到类似错误Error: addCollections(): another instance created this collection with a different schema这个错误意味着你之前已经创建过该集合并写入过数据。此时直接修改 Schema很可能导致新 Schema 与集合内已保存的文档不匹配进而引发难以调试的奇怪 bug。因此 RxDB 会检查 Schema 是否发生变化并主动抛出错误。**生产环境production-mode**下修改 Schema 的正确步骤将version增加 1添加对应的 migrationStrategies把已保存的数据迁移为符合新 Schema 的格式。**开发环境development-mode**下可以通过以下任一策略简化 Schema 变更使用 memory-storage让数据库在重启时重置Schema 不会持久保存在创建新的 RxDatabase 实例之前调用removeRxDatabase(mydatabasename, RxStorage);给数据库名加时间戳后缀每次运行创建全新数据库例如name: heroesDB new Date().getTime()。附为何顶层 Schema 抱怨缺少_id主键字段如果你在 replication 过程中遇到顶层 Schema 缺少_id主键字段的错误原因是RxDB 要求每个 Schema 显式定义主键属性而其他数据库如 CouchDB 类后端使用隐式的_id字段。如果后端期望_id你必须在 Schema 中手动声明_id属性string 类型并将其设置为primaryKey。RxDB 源码对_id作为主键做了特殊放行见 check-schema.ts 的注释目的就是让 RxDB 与 CouchDB 类后端协同工作时更顺手。结语RxSchema 是 RxDB 一切能力的起点主键决定数据如何被唯一定位索引决定查询速度final和required决定数据的完整性encrypted决定敏感字段的存储安全version 迁移策略决定 Schema 如何平滑演进。掌握本文中的定义语法与约束规则后你可以在 test/unit/rx-schema.test.ts 中找到大量可运行的 Schema 正反示例用于验证也可以进一步阅读 rx-storage.md 了解不同存储实现如何消费这些 Schema 信息。【免费下载链接】rxdbThe local-first database that runs on every JS runtime and replicates with your existing backend - no vendor, no lock-in - https://rxdb.info/项目地址: https://gitcode.com/gh_mirrors/rx/rxdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表