{}FireSchema

Firestore 最佳实践:每个开发者都应该知道的 15 条规则

Firebase 项目的数据建模、查询、安全和成本优化

正在使用 Firestore 构建项目?这 15 条规则将帮助您避免最常见的陷阱。无论您是刚开始第一个 Firebase 项目还是扩展现有项目,这些最佳实践涵盖了从数据建模到成本优化的所有内容 — 源自真实生产应用的经验。

数据建模规则

如何构建数据决定了其他一切 — 查询性能、成本和可维护性。首先把这一点做对。

1. 优先选择扁平集合而非深度嵌套

深度嵌套的子集合使查询变得更困难并造成紧耦合。保持集合层次结构浅层 — 通常为 1-2 层深。使用带有文档引用的根级集合,而不是嵌套所有内容。例如,将 orders 存储为带有 userId 字段的根集合,而不是嵌套在 users/{id}/orders 下 — 除非您只按用户查询订单。

2. 为读性能反规范化数据

在 SQL 中,您规范化以避免重复。在 Firestore 中,您反规范化以避免多次读取。如果您的界面在每条评论旁边显示用户名,请直接在每条评论文档中存储 authorName — 不要强制单独读取用户集合。接受权衡:写入更复杂,但读取快速且便宜。

3. 对无界的一对多关系使用子集合

当文档可以有数百或数千个相关项(聊天中的消息、用户的订单)时,使用子集合。与文档内的数组不同,子集合可以容纳无限项并支持带分页的高效查询。例如:chats/{chatId}/messages/{messageId}

4. 保持文档小于 20 KB 以获得最佳性能

Firestore 的硬限制是每个文档 1 MB,但目标是低于 20 KB。当您只需要几个字段时,大文档会浪费带宽 — 而且 Firestore 按文档读取收费,无论大小如何。如果文档增长,将其拆分为子集合或单独的集合。

5. 避免将数组用于可查询数据

Firestore 中的数组有局限性:您无法更新单个元素,array-contains 查询每个查询只支持一个,并且数组超过几百个项后扩展性不佳。对于标签或类别,使用 array-contains,但对于关系或增长列表,改用子集合或映射字段。

查询优化

Firestore 查询设计上很快,但前提是您要顺应其约束 — 而非对抗它们。

6. 主动创建复合索引

Firestore 为每个唯一的查询模式都需要索引。与其等待错误消息,不如提前规划索引。在 firestore.indexes.json 中定义它们,并使用 firebase deploy --only firestore:indexes 部署。每个 where + orderBy 组合都需要自己的复合索引。

7. 使用基于游标的分页,而非偏移量

永远不要在 Firestore 中使用基于偏移量的分页 — 它仍然会读取(并收费)所有跳过的文档。相反,使用 startAfter() 与前一页的最后一个文档一起使用。这既更快又更便宜,因为 Firestore 只读取您实际需要的文档。

8. 明确限制查询结果

始终向查询添加 .limit()。否则,Firestore 会返回每个匹配的文档 — 可能有数百万。即使您认为今天集合很小,六个月后也不会如此。一个好的默认值是每次查询 20-50 个文档。

9. 避免读取整个集合

如果您发现自己在没有过滤器的情况下执行 collection('users').get(),那么您的数据模型需要重新思考。对于分析使用聚合查询(count()sum()),对于显示查询始终使用 where() 过滤。读取所有文档是导致意外 Firestore 账单的头号原因。

安全规则模式

Firestore 安全规则是您唯一的服务器端验证。如果搞错了,整个数据库都会暴露。

10. 始终在规则中验证字段类型

不要只检查字段是否存在 — 验证其类型。如果您只检查 request.resource.data.age != null,恶意客户端可能会发送 age: "not a number"。使用 is stringis numberis bool 在安全规则级别强制执行类型。

11. 使用细粒度的每个集合规则

永远不要在数据库级别使用 allow read, write: if true。为每个集合编写特定规则:谁可以读取、谁可以创建、谁可以更新、谁可以删除。从拒绝所有内容开始,然后逐步开放访问权限。对子集合使用 match 模式。

12. 永远不要信任客户端数据

客户端可以发送任何想要的数据。验证必填字段、检查数据类型、强制执行枚举值,并验证引用字段指向真实文档。使用 request.auth 验证用户是他们声称的身份,使用 resource.data 与现有值进行比较。

成本优化

Firestore 定价基于读取、写入和存储。查询方式的小变化可能对账单产生重大影响。

13. 批量写入以减少操作

使用 writeBatch()runTransaction() 将多个写入合并为单个操作。500 次写入的批次与 500 次单独写入在操作方面的成本相同,但减少了网络往返并确保原子性。批次限制为最多 500 次操作。

14. 缓存频繁读取的文档

Firestore 的客户端 SDK 具有内置的离线持久性 — 启用它。对于服务器端应用程序,为很少更改但经常读取的文档实现缓存层(Redis、内存),例如应用程序配置或用户配置文件。每次缓存读取都是您不必付费的读取。

15. 使用聚合查询而非读取所有文档

需要文档计数?使用 countQuery() 而不是获取所有文档并在客户端计数。Firestore 聚合查询(count()sum()average())读取索引条目而不是完整文档,每个条目成本约为 1/1000。

额外提示:记录数据库结构

如果您的团队找不到这些最佳实践,它们就毫无用处。随着 Firestore 数据库的增长,跟踪集合结构、字段类型和验证规则变得具有挑战性。JSON Schema 为您提供了一种标准方式来描述数据库结构 — 而像 FireSchema 这样的工具会将这些 Schema 转化为团队可以参考的交互式可浏览文档。

💡 提示:为每个集合创建一个 .schema.json 文件,记录其字段、类型和验证规则。这将成为团队的唯一权威数据源。

下一步

继续学习有关 Firestore 和数据库文档的知识:

试用 FireSchema

快速开始指南