Firestore - 如何构建订阅源并遵循系统

时间:2017-10-27 16:12:39

标签: java android firebase database-design google-cloud-firestore

我在我的测试社交网络应用中使用Firebase实时数据库,您可以在其中关注并接收您关注的人的帖子。传统的社交网络。 我构建了像这样的数据库 -

Users
--USER_ID_1
----name
----email
--USER_ID_2
----name
----email

Posts
--POST_ID_1
----image
----userid
----date
--POST_ID_2
----image
----userid
----date

Timeline
--User_ID_1
----POST_ID_2
------date
----POST_ID_1
------date

我还有另一个节点“Content”,它只包含所有用户帖子的id。因此,如果“A”跟在“B”之后,那么B的所有帖子ID都添加到A的时间轴。如果B发布了一些内容,那么它也会被添加到其所有关注者的时间线中。

现在这是我的实时数据库解决方案,但它显然存在一些可扩展性问题

  • 如果有人拥有10,000个关注者,则新帖子被添加到所有10,000个关注者的时间轴中。
  • 如果某人的帖子数量超过每位新关注者,则会在其时间轴中收到所有这些帖子。

这些是一些问题。

现在,我正在考虑将整个事情转移到firestore上,因为它声称“可扩展”。那么我应该如何构建我的数据库,以便在firestore中消除我在实时数据库中遇到的问题。

8 个答案:

答案 0 :(得分:24)

过一会儿我已经看到了您的问题,但我也会尽力为您提供我能想到的最佳数据库结构。因此,希望您会发现此答案有用。

我正在考虑一种模式,该模式针对usersusers that a user is followingposts具有三个顶级集合。

Firestore-root
   |
   --- users (collection)
   |     |
   |     --- uid (documents)
   |          |
   |          --- name: "User Name"
   |          |
   |          --- email: "email@email.com"
   |
   --- following (collection)
   |      |
   |      --- uid (document)
   |           |
   |           --- userFollowing (collection)
   |                 |
   |                 --- uid (documents)
   |                 |
   |                 --- uid (documents)
   |
   --- posts (collection)
         |
         --- uid (documents)
              |
              --- userPosts (collection)
                    |
                    --- postId (documents)
                    |     |
                    |     --- title: "Post Title"
                    |     |
                    |     --- date: September 03, 2018 at 6:16:58 PM UTC+3
                    |
                    --- postId (documents)
                          |
                          --- title: "Post Title"
                          |
                          --- date: September 03, 2018 at 6:16:58 PM UTC+3
  

如果某人拥有10,000个关注者,但在10,000个关注者的时间轴中添加了新帖子。

这根本没有问题,因为这就是在Firestore中修改集合的原因。根据{{​​3}}的官方文档:

  

Cloud Firestore经过优化,可以存储大量小文件。

这就是我将userFollowing添加为集合而不是添加为可以容纳其他对象的简单对象/映射的原因。请记住,根据有关modeling a Cloud Firestore database的官方文档,文档的最大大小为1 MiB (1,048,576 bytes)。如果是托收,则托收之下的文件数量没有限制。实际上,Firestore已针对此类结构进行了优化。

因此,以这种方式拥有这10,000个关注者,将可以很好地工作。此外,您可以以无需在任何地方复制任何内容的方式查询数据库。

如您所见,该数据库非常非规范化,使您可以非常简单地对其进行查询。让我们举个例子,但是在我们创建与数据库的连接并使用以下代码行来获取用户uid之前,

FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
String uid = FirebaseAuth.getInstance().getCurrentUser().getUid();

如果要查询数据库以获取用户关注的所有用户,则可以在以下引用上使用get()调用:

CollectionReference userFollowingRef = rootRef.collection("following/" + uid + "/userFollowing");

这样,您可以获取用户关注的所有用户对象。拥有他们的uid,您只需获得他们的所有帖子即可。

假设您要在时间轴上获取每个用户的最新三篇帖子。当使用非常大的数据集时,解决此问题的关键是以较小的块加载数据。我在此 limits and quota 的回答中解释了一种推荐的方法,您可以通过将查询游标与limit()方法结合使用来对查询进行分页。我还建议您查看此 post 以便更好地理解。因此,要获取每个用户的最新三篇文章,您应该考虑使用此解决方案。因此,首先您需要获取要关注的前15个用户对象,然后根据其uid来获取其最新的三篇文章。要获取单个用户的最新三篇帖子,请使用以下查询:

Query query = rootRef.collection("posts/" + uid + "/userPosts").orderBy("date", Query.Direction.DESCENDING)).limit(3);

在向下滚动时,加载其他15个用户对象并获取其最新的三篇文章,依此类推。除了date之外,您还可以向post对象添加其他属性,例如喜欢,评论,分享等的数量。

  

如果某人的帖子数量多于每个新关注者在其时间轴中收到的所有帖子,则

不可能。无需执行此类操作。我已经在上面解释了原因。

编辑2019年5月20日:

另一种优化操作的方法是将用户应看到的帖子存储在该用户的文档中。

因此,如果我们举一个例子,比方说facebook,您将需要有一个包含每个用户的facebook feed的文档。但是,如果单个文档可以容纳的数据太多(video),则需要将这些数据放入集合中,如上所述。

答案 1 :(得分:1)

我浏览了一些Firebase文档,但对于在您的情况下https://firebase.google.com/docs/database/android/structure-data#fanout处建议的实施为何无效,我感到困惑。像这样:

users
--userid(somedude)
---name
---etc
---leaders: 
----someotherdude
----someotherotherdude

leaders:
--userid(someotherdude)
---datelastupdated
---followers
----somedude
----thatotherdude
---posts
----postid

posts
--postid
---date
---image
---contentid

postcontent
--contentid
---content

该指南继续提到“这是双向关系的必要冗余。即使用户或组的列表扩展到数百万,它也可以使您快速有效地获取Ada的成员身份。”似乎可扩展性完全不是Firestore的事情。

除非我丢失了某些东西,否则主要问题似乎是时间轴节点本身的存在。我知道,它使生成特定用户的时间轴的视图变得更加容易,但这是以必须维护所有这些关系为代价的,并且极大地延迟了您的项目。基于提交的用户,使用查询从与上述类似的结构快速构建时间轴是否效率低下?

答案 2 :(得分:0)

有两种情况

  1. 您应用中的用户关注者数量很少。

  2. 您应用中的用户拥有大量关注者。如果我们要将整个关注者存储在Firestore中的单个文档中的单个数组中。然后它将达到每个文档1 MiB的存储限制。


  1. 在第一种情况下,每个用户都必须保留一个文档,该文档将关注者列表存储在单个数组中的单个文档中。通过使用arrayUnion()arrayRemove(),可以有效地管理关注者列表。而且,当您要在时间轴中发布内容时,必须在发布文档中添加关注者列表。

    并使用下面给出的查询来获取帖子

    postCollectionRef.whereArrayContains("followers", userUid).orderBy("date");
    
  2. 在第二种情况下,您只需要根据关注者数组的大小或数量来破坏用户关注文档。在将数组的大小达到固定大小后,下一个关注者的ID必须添加到下一个文档中。并且第一个文档必须保留字段“ hasNext”,该字段存储布尔值。 添加新帖子时,您必须复制帖子文档,并且每个文档都包含较早中断的关注者列表。 并且我们可以进行与上面给出的相同的查询来获取文档。

答案 3 :(得分:0)

我一直在努力地为她提供建议的解决方案,主要是由于技术上的差距,所以我想出了另一种对我有用的解决方案。

对于每个用户,我都有一个文档,其中包含他们遵循的所有帐户,还包含该用户遵循的所有帐户的所有列表。

应用启动时,我会掌握该当前用户的帐户列表,并且当一个用户发布帖子时,发布对象的一部分就是跟随他们的所有用户的数组。

当用户B想要获得他们关注的人的所有帖子时,我只是向查询添加简单的whereArrayContains("followers", currentUser.uid)

我喜欢这种方法,因为它仍然允许我通过所需的任何其他参数对结果进行排序。

基于:

  • 每个文档1 mb,通过Google搜索,我似乎拥有1,048,576个角色。
  • Firestore生成的UID的事实似乎长约28个字符。
  • 对象中的其余信息不会占用太大的空间。

这种方法应该适用于拥有大约37,000个关注者的用户。

答案 4 :(得分:0)

我认为一种可能性是制作另一个名为"users_following"的顶级集合,其中包含一个名为"user_id"的文档以及一个包含该用户所关注的所有用户的数组的字段。在该"users_following"文档中,该帖子可以包含该特定用户的子集合,所有帖子或顶级集合也可以完成该工作。接下来要注意的另一件事是,必须将最近的一篇帖子存储在"users-following"文档中作为数组或映射。基本上,此归一化数据将用于填充关注您的人的供稿。但是它的缺点是,即使该人最近添加了两个帖子,或者即使您以规范化的方式存储了两到三个帖子,您也只会看到一个人的帖子,而不是一次显示所有三个帖子(例如三个帖子)连续同一用户)。但是,如果您只需要为每位用户显示一篇帖子,那仍然是一件好事。

答案 5 :(得分:0)

如果您的网络上有任何体面的活动(例如,人们关注 1,000 人,或发布 1,000 个帖子的人),则其他答案将非常昂贵

我的解决方案是为每个名为“recentPosts”的用户文档添加一个字段,该字段将是一个数组。

现在,无论何时发布帖子,都有一个检测 onWrite() 的云函数,并更新其 userDocument 上该发布者的 recentPosts 数组以添加有关该帖子的信息。

因此,您可以将以下映射添加到recentPosts 数组的前面:

{
"postId": xxxxxxxxxxx,
"createdAt": tttttt
}

将recentPosts 数组限制为1,000 个对象,超过限制时删除最旧的条目。

现在,假设您正在关注 1,000 个用户并希望填充您的供稿...获取所有 1,000 个用户文档(我建议使用 in-query,以便您可以在 100 个查询中执行此操作)。这将计为 1k 次读取。

一旦您拥有 1,000 个文档,每个文档都会有一个 recentPosts 数组。将客户端上的所有这些数组合并为一个主数组并按 createdAt 排序。

现在您可能有多达 100 万个帖子的 docID,全部按时间顺序排列,仅读取 1,000 次。现在,当您的用户滚动他们的提要时,只需根据需要通过他们的 docID 查询这些文档,大概一次 10 个或其他。

您现在可以加载来自 Y 个关注者的 X 个帖子供 X + Y 次阅读。

因此,来自 100 个关注者的 2,000 个帖子只会被阅读 2,100 次。
因此,来自 1,000 个关注者的 1,000 个帖子只会获得 2,000 次阅读。
等等...


编辑 1) 进一步优化。加载 userDocuments 时,您可以使用 in query 一次对它们进行 10 个批处理...通常这没有区别,因为即使它是批处理的,它仍然是 10 次读取...但您也可以按字段进行过滤,例如recentPostsLastUpdatedAt 并检查它是否大于该用户文档的缓存值,然后任何未更新其最近发布数组的用户文档都不会被读取。理论上,这可以节省 10 倍的碱基读取时间。

编辑 2) 您也可以将侦听器附加到每个 userDocument 以获取新帖子,因为他们的最近帖子发生变化,而无需在每次需要刷新提要时查询每个关注者。 (虽然 1,000 多个快照侦听器可能是不好的做法,但我不知道它们在后台是如何工作的)

答案 6 :(得分:0)

我的可扩展想法是,用户可能有 1,000,000 多个关注者,但真正的用户关注的人不会超过 1000。我们可以简单地聚合他们的提要(一组帖子)。这是我的理论:

集合

/users
/users/{userId}/follows
/users/{userId}/feed
/posts

1.填充提要

需要先运行填充提要,并且老实说应该在云函数中。为避免成本,它只会将新帖子添加到您的供稿中,而不会将超过 10 天(或无论多旧)的帖子添加到您的供稿中。

populateFeed() - 类似这样的...

numFollowing = get('users/numFollowing');
lastUpdate = get('users/lastUpdate');
tenDaysOld = timestamp 10 days ago

// maybe chunk at 20 here...
for (numFollowing) {
  docs = db.collection('posts')
    .where('userId', '==', userId)
    .where('createdAt', '>', lastUpdate)
    .where('createdAt', '<', tenDaysOld);
  db.collection('users/${userId}/feed').batch.set(docs);

users/${userId}/lastUpdate 更新为当前时间戳...

这样一来,您就不会得到太多文档(例如只有 10 天的文档),并且不会浪费对已有文档的阅读。

2) 阅读提要

提要将是汇总的帖子。

loadFeed() - 在 populateFeed()

之后调用
db.collection('/users/${userId}/feed').orderBy('createdAt');

feed 中的文档只需要 createdAt 日期和 postId,因为您可以在前端拉取帖子,尽管您可以如果您不希望它更改,请存储所有数据:

postId: {
  createdAt: date
}

您的userDoc 还将具有:

{
  numFollowing: number,
  lastUpdate: date
}

应用应在加载时自动调用 loadFeed()。可能有一个按钮将 populateFeed() 作为可调用的云函数(最好的)或在本地运行。如果您的供稿是可观察的 firebase,它会在填充时自动更新...

只是一个想法......我认为可能有一些其他更简洁的方法来解决这个问题......

J

更新

我越想越多,实际上我确实认为可以将帖子 onWrite 上的字段更新到所有关注者提要。唯一的限制是时间,通常为 60 秒,最多可达 9min。真的,您只需要确保异步批量更新即可。查看我的 adv-firestore-functionshere

答案 7 :(得分:-3)

您需要保持关注者之间的关系:

Followers
-leading_id
-follower_id
-created_at

接下来,我认为您不需要时间表。打开供稿时,获取所有关注者并加入他们的帖子,此外,您还可以使用某种顺序并在请求中进行过滤。

在您的结构中,时间轴表重复了有关帖子的信息,我认为这对数据库来说是不正常的。