本指南涵盖了数据架构中的一些关键概念以及在 Firebase 实时数据库中构建 JSON 数据的最佳实践。
构建一个结构合理的数据库需要相当多的深谋远虑。最重要的是,您需要规划数据的保存方式和以后的检索方式,以使该过程尽可能简单。
数据的结构:它是一个 JSON 树
所有 Firebase 实时数据库数据都存储为 JSON 对象。您可以将数据库视为云托管的 JSON 树。与 SQL 数据库不同,它没有表或记录。当您将数据添加到 JSON 树时,它会成为现有 JSON 结构中具有关联键的节点。您可以提供自己的密钥,例如用户 ID 或语义名称,或者可以使用push()
为您提供它们。
如果您创建自己的密钥,它们必须采用 UTF-8 编码,最大长度为 768 字节,并且不能包含.
、 $
、 #
、 [
、 ]
、 /
或 ASCII 控制字符 0-31 或 127。您也不能在值本身中使用 ASCII 控制字符。
例如,考虑一个允许用户存储基本配置文件和联系人列表的聊天应用程序。典型的用户配置文件位于路径中,例如/users/$uid
。用户alovelace
可能有一个看起来像这样的数据库条目:
{ "users": { "alovelace": { "name": "Ada Lovelace", "contacts": { "ghopper": true }, }, "ghopper": { ... }, "eclarke": { ... } } }
尽管数据库使用 JSON 树,但存储在数据库中的数据可以表示为与可用 JSON 类型对应的某些原生类型,以帮助您编写更易于维护的代码。
数据结构的最佳实践
避免嵌套数据
由于 Firebase 实时数据库允许嵌套数据最多 32 层,您可能会认为这应该是默认结构。但是,当您在数据库中的某个位置获取数据时,您还会检索它的所有子节点。此外,当您授予某人对数据库中某个节点的读或写访问权限时,您也授予他们访问该节点下所有数据的权限。因此,在实践中,最好使数据结构尽可能扁平。
有关嵌套数据为何不好的示例,请考虑以下多重嵌套结构:
{ // This is a poorly nested data architecture, because iterating the children // of the "chats" node to get a list of conversation titles requires // potentially downloading hundreds of megabytes of messages "chats": { "one": { "title": "Historical Tech Pioneers", "messages": { "m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." }, "m2": { ... }, // a very long list of messages } }, "two": { ... } } }
使用这种嵌套设计,遍历数据会出现问题。例如,列出聊天对话的标题需要将整个chats
树(包括所有成员和消息)下载到客户端。
扁平化数据结构
如果将数据拆分为单独的路径(也称为非规范化),则可以根据需要在单独的调用中高效地下载数据。考虑这个扁平结构:
{ // Chats contains only meta info about each conversation // stored under the chats's unique ID "chats": { "one": { "title": "Historical Tech Pioneers", "lastMessage": "ghopper: Relay malfunction found. Cause: moth.", "timestamp": 1459361875666 }, "two": { ... }, "three": { ... } }, // Conversation members are easily accessible // and stored by chat conversation ID "members": { // we'll talk about indices like this below "one": { "ghopper": true, "alovelace": true, "eclarke": true }, "two": { ... }, "three": { ... } }, // Messages are separate from data we may want to iterate quickly // but still easily paginated and queried, and organized by chat // conversation ID "messages": { "one": { "m1": { "name": "eclarke", "message": "The relay seems to be malfunctioning.", "timestamp": 1459361875337 }, "m2": { ... }, "m3": { ... } }, "two": { ... }, "three": { ... } } }
现在可以通过每次对话仅下载几个字节来遍历房间列表,快速获取元数据以在 UI 中列出或显示房间。消息可以单独获取并在消息到达时显示,从而使 UI 保持响应和快速。
创建可扩展的数据
在构建应用程序时,下载列表的一个子集通常会更好。如果列表包含数千条记录,这种情况尤其常见。当这种关系是静态和单向的时,您可以简单地将子对象嵌套在父对象下。
有时,这种关系更加动态,或者可能需要对这些数据进行非规范化。很多时候,您可以通过使用查询来检索数据的子集来对数据进行反规范化,如检索数据中所述。
但即使这样可能还不够。例如,考虑用户和组之间的双向关系。用户可以属于一个组,而组包含一个用户列表。当需要决定用户属于哪个组时,事情就变得复杂了。
我们需要的是一种优雅的方式来列出用户所属的组并只获取这些组的数据。组索引在这里可以提供很大帮助:
// An index to track Ada's memberships { "users": { "alovelace": { "name": "Ada Lovelace", // Index Ada's groups in her profile "groups": { // the value here doesn't matter, just that the key exists "techpioneers": true, "womentechmakers": true } }, ... }, "groups": { "techpioneers": { "name": "Historical Tech Pioneers", "members": { "alovelace": true, "ghopper": true, "eclarke": true } }, ... } }
您可能会注意到,通过将关系存储在 Ada 的记录和组下,这会复制一些数据。现在alovelace
被索引到一个组下,而techpioneers
列在 Ada 的个人资料中。所以要从组中删除 Ada,必须在两个地方进行更新。
这是双向关系的必要冗余。它允许您快速高效地获取 Ada 的成员资格,即使用户或组列表扩展到数百万或实时数据库安全规则阻止访问某些记录时也是如此。
这种方法通过将 ID 列为键并将值设置为 true 来反转数据,使得检查键就像读取/users/$uid/groups/$group_id
并检查它是否为null
一样简单。索引比查询或扫描数据更快,效率也更高。