为 Firebase 中的私有数据访问非规范化数据结构?

Denormalizing data structures for private data access in Firebase?

我想create data that scales(跟踪用户的私人数据)。

Firebase 文档建议像这样将子对象嵌套在父对象下:

{
  "users": {
    "google:1234567890": {
      "displayName" : "Username A",
      "provider" : "google",
      "provider_id" : "1234567890",
      "todos": {
          "rec_id1": "Walk the dog",
          "rec_id2": "Buy milk",
          "rec_id3": "Win a gold medal in the Olympics",
          ...
      }
    }, ...
  }
}

(其中 rec_id 是由 Firebase.push() 生成的唯一键。)

但是正如 Denormalizing Your Data is Normal 中提到的,我认为以这种方式构建它会更好:

{
  "users" : {
    "google:1234567890" : {
      "displayName" : "Username A",
      "provider" : "google",
      "provider_id" : "1234567890"
    }, ...
  },
  "todos" : {
    "google:1234567890" : {
      "rec_id1" : {
        "todo" : "Walk the dog"
      },
      "rec_id2" : {
        "todo" : "Buy milk"
      },
      "rec_id3" : {
        "todo" : "Win a gold medal in the Olympics"
      }, ...
    }, ...
  }
}

然后只允许用户 write/read 它自己的数据应用以下安全规则:

{
  "rules": {
    "users": {
      "$uid": {
        // grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
        ".write": "auth !== null && auth.uid === $uid",
        ".read": "auth !== null && auth.uid === $uid"
      }
    },
    "todos": {
      "$uid": {
        // grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
        ".write": "auth !== null && auth.uid === $uid",
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

由于我是这种数据库的新手,我想知道我想要构建它的方式是否有任何缺点。

是否像第一个示例中建议的那样将所有待办事项直接嵌套在用户下方会更好?

首先,一些资源,如果您还没有 运行 的话:

编辑:您显然已经 运行 了解这些资源,因为它在您的问题中有链接,但我建议您多读几次《构建您的数据》指南。

  • 编辑 2: 参见 @Frank van Puffelen posted for
    • this
  • 我还建议阅读规范化 [1][2] and denormalization [3][4],我认为您可能会混淆您所指的两者中的哪一个。

这让我们想到了您的场景...

您设置数据的两种方式实际上完成了几乎相同的事情!

不过您会注意到,第一个示例实际上在 "Structuring Your Data" 指南中被列为反模式。

  • 如果您想加载用户数据,然后在不同的时间加载该用户的待办事项,则采用第二种方式会很有用。
  • 如果您只有一个用户访问每个待办事项列表,那么您设置它的方式很好。
    • 例如,我在一个应用程序中执行此操作,我知道每个用户在历史列表中都有一个位置,并且我只想在某些情况下加载用户的历史记录。
    • /users/$userUid 提供用户数据,/history/$userUid 提供用户历史记录。
    • 它可以很容易地分段加载。
  • 但是,如果待办事项列表在不同用户之间共享并且必须从多个来源更新,则此结构没有任何好处。
  • 如果你想要共享访问,你就走在正确的轨道上,只需要使用密钥作为参考。

不同的方法是:

  • 您可以将新的 todo 对象推送到 /todos,而不是在 /todos/$uid 下显式设置待办事项对象,以便它获得新的唯一 ID(称为键)。
  • 然后,将该键添加到正确的 user 对象的 todos 子对象中。
  • 这将允许您首先加载用户的数据并仅获取用户所属待办事项的索引(键),并且
  • 然后您可以独立加载用户所属的所有 todos

这样做会:

  • 防止用户对象变大
  • 允许多个用户更新单个 todo,而无需跨多个位置更新它的子参数。
  • 通过将数据拆分到单独的路径来获得可扩展的数据。

这是指南 "Creating Data That Scales" 部分的最后一个数据示例:(我添加了一些评论)

  // An index to track Mary's memberships
  {
    "users": {
      "mchen": {
        "name": "Mary Chen",
        // index Mary's groups in her profile
        "groups": {
           // the value here doesn't matter, just that the key exists
           // these keys are used to figure out which groups should 
           // be loaded (at whatever appropriate time) for Mary,
           // without having to load all the group's data initially (just the keys). 
           "alpha": true,
           "charlie": true
        }
      },
      ...
    },
    // Here is /groups. In here, there would be a group with the key
    // 'alpha' and another one with the key 'charlie'. Once Mary's
    // data is loaded on the client, you would then proceed to load 
    // the groups from this list, since you know what keys to look for.
    "groups": { ... }
  }

这实现了 flatter structure.

如文档所述,

Yes. This is a necessary redundancy for two-way relationships. It allows us to quickly and efficiently fetch Mary's memberships, even when the list of users or groups scales into the millions, or when Security and Firebase Rules would prevent access to some of the records.


所以问题是,您的数据如何才能允许用户拥有多个可以与其他用户共享的待办事项列表?

这是一个例子:

{
  "users" : {
    "google:1234567890" : {
      "displayName" : "Username A",
      "provider" : "google",
      "provider_id" : "1234567890",
      "todoLists" : {
        "todoList1": true,
        "todoList2": true
      }
    },
    "google:0987654321" : {
      "displayName" : "Username B",
      "provider" : "google",
      "provider_id" : "0987654321",
      "todoLists" : {
        "todoList2": true
      }
    }
  },
  "todoLists" : {
    "todoList1" : {
      // 'members' user for rules
      "members" : {
        "google:1234567890" : true
      },
      "records" : {
        "rec_id1" : {
          "todo" : "Walk the dog",
          "createdAt" : "1426240376047"
        },
        "rec_id2" : {
          "todo" : "Buy milk",
          "createdAt" : "1426240376301"
        },
        "rec_id3" : {
          "todo" : "Win a gold medal in the Olympics",
          "createdAt" : "1426240376301"
        }
      }
    },
    "todoList2" : {
      "members" : {
        "google:1234567890" : true,
        "google:0987654321" : true
      },
      "records" : {
        "rec_id4" : {
          "todo" : "Get present",
          "createdAt" : "1426240388047"
        },
        "rec_id5" : {
          "todo" : "Run a mile",
          "createdAt" : "1426240399301"
        },
        "rec_id6" : {
          "todo" : "Pet a cat",
          "createdAt" : "1426240400301"
        }
      }
    }
  }
}
  • 在这种情况下,用户 A 会加载两个列表,但用户 B 只会加载第二个列表。如果您正确设置规则,一切都会很好。
  • 在操作中,首先加载用户的数据,然后加载用户todoLists中的每个todo列表,从/todoList

  • 但实际上,如果您制作的应用程序中一个用户有一个待办事项列表,而待办事项列表只有一个内容。
  • 旁注,这些"ids"可能是一个唯一的键,可以使用Firebase.push()来完成。
    • 有关使用推送生成新 ID 的信息,请参阅

总而言之, 这一切实际上都归结为您的应用如何以及何时需要您的数据、数据更新的频率以及由谁更新,以及最大限度地减少不必要的读取和观察者. Space 通常很便宜,操作(和观察者)通常不是。

最后但同样重要的是,规则和安全是另一个极其重要的考虑因素。指南的最后一部分说:

"Thus, the index is faster and a good deal more efficient. Later, when we talk about securing data, this structure will also be very important. Since Security and Firebase Rules cannot do any sort of "contains" on a list of child nodes, we'll rely on using keys like this extensively."


现在还早,我希望我不是在喋喋不休,但是当我第一次从只知道 MySql 到使用非结构化时,我 运行 遇到了同样的问题,所以我希望对您有所帮助!