本指南介绍了关于数据架构的几个主要概念,以及在您的 Firebase Realtime Database 中设计 JSON 数据结构的最佳实践。
构建一个结构合理的数据库需要预先进行大量规划。 最重要的是,您需要对如何保存数据及之后如何检索数据做好计划,尽可能地简化流程。
数据的结构形式:JSON 树
所有 Firebase Realtime Database 数据都会存储为 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 Realtime Database 允许嵌套最多 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": { ... }
}
}
现在,只需针对每个对话下载几个字节并快速提取元数据(以便在界面中列出或显示聊天室),即可遍历聊天室列表。消息可单独提取并在到达时显示,从而确保界面及时快速地响应。
创建可扩容的数据
构建应用时,最好下载列表的子集。 当列表含有成千上万条记录时,这种做法尤其常见。 当这种关系属于静态和单向关系时,您只需在父对象下嵌套子对象即可。
有时,这种关系更具动态性,或者需要对此数据进行反规范化。通常,您可以通过查询来检索该数据的子集,以便对该数据进行反规范化,如对数据进行排序和过滤中所述。
但即使这样也可能无法解决问题。以用户与群组之间的双向关系为例。用户可属于某个群组,群组可包含一系列用户。当需要确定用户属于哪些群组时,情况就会比较复杂。
我们需要的是一种巧妙的方法,不仅要列出用户所属的群组,而且只提取这些群组的数据。群组索引可在此发挥巨大作用:
// 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 的成员身份,而且即使用户列表或群组列表扩容到数百万条,或 Realtime Database 安全规则阻止访问某些记录,也不会对提取造成影响。
这种方法通过将 ID 列为键并将值设为 true 来反转数据,使得检查键就像读取 /users/$uid/groups/$group_id
然后检查它是否为 null
一样简单。与查询或扫描数据相比,索引的速度更快、效率更高。